refactor(core): distinguish network errors from auth failures and refactor real-time messaging pipeline
- Add isNetworkError() helper and FetchCurrentUserResult discriminated union to prevent logout on network failures
- Refactor authService to return { kind: 'user' } | { kind: 'auth_failed' } | { kind: 'network_error' }
- Update authStore to preserve login state on network errors, only logout on auth failures
- Replace WSMessageHandler with RealtimeIngestionPipeline for improved real-time message handling
- Remove MessageDeduplication service; add store-level idempotent addOrReplaceMessage for message dedup
- Add atomic systemUnreadCount operations to prevent race conditions
- Add SearchHeader component for consistent search UI across screens
- Add 'home' TabBar variant with underline style matching HomeScreen
- Add Jest test infrastructure and exclude tests from tsconfig
- Fix ChatScreen history lock being stuck when scrolling to latest messages
- Remove unused call_participant_joined/left WS event types
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* 在宽屏下使用网格布局
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
@@ -22,6 +22,7 @@ import { User } from '../../types';
|
||||
import { useAuthStore, useUserStore } from '../../stores';
|
||||
import { authService } from '../../services';
|
||||
import { Avatar, Text, Button, Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||
import { SearchBar } from '../../components/business';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useResponsive, useColumnCount } from '../../hooks';
|
||||
|
||||
@@ -53,9 +54,13 @@ const FollowListScreen: React.FC = () => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [debouncedKeyword, setDebouncedKeyword] = useState('');
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const isCurrentUser = currentUser?.id === userId;
|
||||
const title = type === 'following' ? '关注' : '粉丝';
|
||||
const isSearching = debouncedKeyword.trim().length > 0;
|
||||
|
||||
// 加载用户列表
|
||||
const loadUsers = useCallback(async (pageNum: number = 1, refresh: boolean = false) => {
|
||||
@@ -64,11 +69,12 @@ const FollowListScreen: React.FC = () => {
|
||||
try {
|
||||
const pageSize = 20;
|
||||
let userList: User[] = [];
|
||||
const trimmedKeyword = debouncedKeyword.trim();
|
||||
|
||||
if (type === 'following') {
|
||||
userList = await authService.getFollowingList(userId, pageNum, pageSize);
|
||||
userList = await authService.getFollowingList(userId, pageNum, pageSize, trimmedKeyword);
|
||||
} else {
|
||||
userList = await authService.getFollowersList(userId, pageNum, pageSize);
|
||||
userList = await authService.getFollowersList(userId, pageNum, pageSize, trimmedKeyword);
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
@@ -84,12 +90,33 @@ const FollowListScreen: React.FC = () => {
|
||||
}
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}, [userId, type, hasMore]);
|
||||
}, [userId, type, hasMore, debouncedKeyword]);
|
||||
|
||||
// 初始加载
|
||||
// 初始加载(包括 type/userId 切换以及防抖后关键词变化)
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setHasMore(true);
|
||||
loadUsers(1, true);
|
||||
}, [userId, type]);
|
||||
// 仅在 userId/type/debouncedKeyword 变化时重新加载
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userId, type, debouncedKeyword]);
|
||||
|
||||
// 关键词输入防抖(300ms)
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setDebouncedKeyword(keyword);
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [keyword]);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = useCallback(() => {
|
||||
@@ -248,7 +275,18 @@ const FollowListScreen: React.FC = () => {
|
||||
// 渲染空状态
|
||||
const renderEmpty = () => {
|
||||
if (loading) return <Loading />;
|
||||
|
||||
|
||||
if (isSearching) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="没有找到匹配的用户"
|
||||
description="换个关键词试试"
|
||||
icon="account-search-outline"
|
||||
variant="modern"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyText = type === 'following'
|
||||
? '还没有关注任何人'
|
||||
: '还没有粉丝';
|
||||
@@ -271,17 +309,34 @@ const FollowListScreen: React.FC = () => {
|
||||
<View style={styles.headerAccent} />
|
||||
<View style={styles.headerMainRow}>
|
||||
<Text variant="h2" style={styles.headerTitle}>{title}</Text>
|
||||
<View style={styles.headerCountPill}>
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
共 {users.length}
|
||||
</Text>
|
||||
</View>
|
||||
{!isSearching ? (
|
||||
<View style={styles.headerCountPill}>
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
共 {users.length}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.headerCountPill}>
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
搜索结果
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}>
|
||||
{type === 'following'
|
||||
? (isCurrentUser ? '你已关注的用户' : 'TA关注的用户')
|
||||
: (isCurrentUser ? '关注你的用户' : 'TA的粉丝')}
|
||||
</Text>
|
||||
<View style={styles.headerSearchWrap}>
|
||||
<SearchBar
|
||||
value={keyword}
|
||||
onChangeText={setKeyword}
|
||||
onSubmit={() => setDebouncedKeyword(keyword)}
|
||||
placeholder={type === 'following' ? '搜索关注' : '搜索粉丝'}
|
||||
compact
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -407,6 +462,9 @@ function createFollowListStyles(colors: AppColors) {
|
||||
headerSubtitle: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
headerSearchWrap: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
userItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
Reference in New Issue
Block a user