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:
@@ -214,6 +214,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
handleReachLatestEdge,
|
||||
jumpToLatestMessages,
|
||||
setBrowsingHistory,
|
||||
clearHistoryLock,
|
||||
} = useChatScreen(props);
|
||||
|
||||
const stableOnBack = useCallback(() => router.back(), [router]);
|
||||
@@ -412,15 +413,12 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
setBrowsingHistory(true);
|
||||
}
|
||||
|
||||
// 仅用户手势主动回到底部时才解除历史阅读锁(避免加载阶段误解锁)
|
||||
const isScrollingTowardLatest = contentOffset.y < lastScrollYRef.current;
|
||||
if (
|
||||
isUserDraggingRef.current &&
|
||||
!loadingMore &&
|
||||
isScrollingTowardLatest &&
|
||||
contentOffset.y <= 40
|
||||
) {
|
||||
handleReachLatestEdge();
|
||||
// 接近最新消息端时退出历史阅读模式
|
||||
// 修复:不再要求 isUserDraggingRef — 动量滚动结束后、layout settle 后、
|
||||
// 或用户轻触回到底部时都能正确解锁,防止浏览历史锁永久为 true
|
||||
// 导致新消息不自动跟随(仅自己发的才显示的问题)
|
||||
if (!loadingMore && contentOffset.y <= 100) {
|
||||
clearHistoryLock();
|
||||
}
|
||||
|
||||
// inverted 下历史端在“列表尾部”,对应 offset 增大且距离尾部变小
|
||||
@@ -460,14 +458,18 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
loading,
|
||||
messages.length,
|
||||
loadMoreHistory,
|
||||
handleReachLatestEdge,
|
||||
showEdgeLoadingIndicator,
|
||||
setBrowsingHistory,
|
||||
]);
|
||||
|
||||
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||||
handleMessageListContentSizeChange(contentWidth, contentHeight);
|
||||
}, [handleMessageListContentSizeChange]);
|
||||
// 安全检查:loadMoreHistory 完成后,如果用户仍在底部附近,清除浏览历史锁
|
||||
// 防止 layout settle 后没有触发滚动事件导致锁无法释放
|
||||
if (!loadingMore && scrollPositionRef.current.scrollY <= 100) {
|
||||
clearHistoryLock();
|
||||
}
|
||||
}, [handleMessageListContentSizeChange, loadingMore, clearHistoryLock]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
|
||||
|
||||
@@ -61,7 +61,7 @@ import { ConversationListRow } from './components/ConversationListRow';
|
||||
// 导入 NotificationsScreen 用于在内部显示
|
||||
import { NotificationsScreen } from './NotificationsScreen';
|
||||
// 导入扫码组件
|
||||
import { SearchBar, TabBar } from '../../components/business';
|
||||
import { SearchBar, SearchHeader, TabBar } from '../../components/business';
|
||||
import { QRCodeScanner } from '../../components/business/QRCodeScanner';
|
||||
|
||||
// 系统通知会话特殊ID
|
||||
@@ -668,33 +668,27 @@ export const MessageListScreen: React.FC = () => {
|
||||
// 渲染搜索模式UI(扁平化现代化设计)
|
||||
const renderSearchMode = () => (
|
||||
<View style={styles.searchModeContainer}>
|
||||
{/* 搜索头部 */}
|
||||
<View style={[styles.searchHeader, { paddingTop: spacing.sm }]}>
|
||||
<View style={styles.searchShell}>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onSubmit={() => performSearch(searchText)}
|
||||
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={handleCloseSearch}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.cancelText}>取消</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{/* 搜索顶栏(统一组件,内置硬件返回键拦截) */}
|
||||
<SearchHeader
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onSubmit={() => performSearch(searchText)}
|
||||
onCancel={handleCloseSearch}
|
||||
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
|
||||
compact
|
||||
/>
|
||||
|
||||
{/* Tab切换 - 现代化风格 */}
|
||||
{/* Tab切换 - 与主页一致的下划线风格 */}
|
||||
<View style={styles.searchTabContainer}>
|
||||
<TabBar
|
||||
tabs={['聊天记录', '用户']}
|
||||
activeIndex={activeSearchTab === 'chat' ? 0 : 1}
|
||||
onTabChange={(index) => setActiveSearchTab(index === 0 ? 'chat' : 'user')}
|
||||
variant="modern"
|
||||
variant="home"
|
||||
style={{
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1048,32 +1042,10 @@ function createMessageListStyles(colors: AppColors) {
|
||||
},
|
||||
searchModeContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
searchHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: `${colors.divider}70`,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
searchShell: {
|
||||
flex: 1,
|
||||
},
|
||||
cancelButton: {
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
cancelText: {
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '600',
|
||||
color: colors.primary.main,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
searchTabContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
backgroundColor: colors.background.default,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: `${colors.divider}50`,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
borderRadius,
|
||||
useAppColors,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
@@ -26,8 +25,8 @@ import { extractTextFromSegments, UserDTO } from '../../types/dto';
|
||||
import { messageRepository } from '../../database';
|
||||
import { userManager } from '../../stores/user';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { Avatar, Text, EmptyState, AppBackButton } from '../../components/common';
|
||||
import { SearchBar } from '../../components/business';
|
||||
import { Avatar, Text, EmptyState } from '../../components/common';
|
||||
import { SearchHeader } from '../../components/business';
|
||||
import HighlightText from '../../components/common/HighlightText';
|
||||
import { formatTime } from '../../utils/formatTime';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
@@ -233,24 +232,16 @@ export const MessageSearchScreen: React.FC = () => {
|
||||
}, [loading, results.length, styles, colors]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<View style={styles.header}>
|
||||
<AppBackButton onPress={() => router.back()} />
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{conversationName}
|
||||
</Text>
|
||||
<View style={styles.headerSpacer} />
|
||||
</View>
|
||||
|
||||
<View style={styles.searchWrap}>
|
||||
<SearchBar
|
||||
value={keyword}
|
||||
onChangeText={handleTextChanged}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="搜索聊天记录"
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
{/* 搜索顶栏(与 HomeScreen 搜索页一致的统一组件) */}
|
||||
<SearchHeader
|
||||
value={keyword}
|
||||
onChangeText={handleTextChanged}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => router.back()}
|
||||
placeholder={`搜索 ${conversationName} 的聊天记录`}
|
||||
compact
|
||||
/>
|
||||
|
||||
<FlashList
|
||||
data={results}
|
||||
@@ -272,29 +263,6 @@ function createMessageSearchStyles(colors: AppColors) {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
headerTitle: {
|
||||
flex: 1,
|
||||
fontSize: fontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
textAlign: 'center',
|
||||
marginHorizontal: spacing.sm,
|
||||
},
|
||||
headerSpacer: {
|
||||
width: 40,
|
||||
},
|
||||
searchWrap: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
resultItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -649,6 +649,17 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
isBrowsingHistoryRef.current = browsing;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 无条件清除浏览历史锁与自动跟随抑制(动量滚动结束、layout settle、
|
||||
* 轻触回到底部等场景使用)。区别于 handleReachLatestEdge,后者受加载锁保护。
|
||||
*/
|
||||
const clearHistoryLock = useCallback(() => {
|
||||
if (isBrowsingHistoryRef.current || suppressAutoFollowRef.current) {
|
||||
isBrowsingHistoryRef.current = false;
|
||||
suppressAutoFollowRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 进入聊天详情后立即清未读(不依赖滚动位置)
|
||||
useEffect(() => {
|
||||
if (!conversationId) return;
|
||||
@@ -1653,5 +1664,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
handleReachLatestEdge,
|
||||
jumpToLatestMessages,
|
||||
setBrowsingHistory,
|
||||
clearHistoryLock,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user