refactor(core): distinguish network errors from auth failures and refactor real-time messaging pipeline
Some checks failed
Frontend CI / ota (android) (push) Successful in 3m11s
Frontend CI / ota (ios) (push) Successful in 3m31s
Frontend CI / build-and-push-web (push) Successful in 4m38s
Frontend CI / build-android-apk (push) Has been cancelled

- 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:
lafay
2026-06-21 17:17:15 +08:00
parent 94a202506b
commit 705b365536
47 changed files with 4357 additions and 1574 deletions

View File

@@ -24,7 +24,7 @@ import { useUserStore } from '../../stores';
import { postService, authService } from '../../services';
import { tradeService } from '../../services/trade/tradeService';
import { postSyncService } from '@/services/post';
import { PostCard, SearchBar } from '../../components/business';
import { PostCard, SearchHeader, TabBar } from '../../components/business';
import { TradeCard } from '../../components/business/TradeCard/TradeCard';
import type { PostCardAction } from '../../components/business/PostCard';
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
@@ -601,66 +601,32 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{/* 搜索输入框 */}
<View
style={[
styles.searchHeader,
{
paddingTop: spacing.sm,
paddingHorizontal: responsivePadding,
paddingBottom: spacing.xs,
},
]}
>
<View style={[styles.searchShell, { maxWidth: searchBarMaxWidth }]}>
<SearchBar
value={searchText}
onChangeText={setSearchText}
onSubmit={handleSearch}
placeholder={isMarket ? "搜索商品、用户" : "搜索帖子、用户"}
autoFocus
compact
/>
</View>
<TouchableOpacity
style={[styles.cancelButton, { marginLeft: responsiveGap }]}
onPress={() => (onBack ? onBack() : router.back())}
activeOpacity={0.85}
>
<Text
variant="body"
color={colors.primary.main}
style={styles.cancelText}
>
</Text>
</TouchableOpacity>
</View>
{/* 搜索顶栏(统一组件,内置硬件返回键拦截) */}
<SearchHeader
value={searchText}
onChangeText={setSearchText}
onSubmit={handleSearch}
onCancel={onBack ? onBack : () => router.back()}
placeholder={isMarket ? "搜索商品、用户" : "搜索帖子、用户"}
compact
searchBarMaxWidth={searchBarMaxWidth}
horizontalPadding={responsivePadding}
/>
{/* Tab切换 - 与主页风格一致的下划线样式 */}
<View style={styles.tabWrapper}>
<View style={[styles.homeTabSwitcher, { paddingHorizontal: responsivePadding }]}>
{TABS.map((tab, index) => {
const isActive = activeIndex === index;
return (
<TouchableOpacity
key={tab}
activeOpacity={0.7}
style={[styles.homeTabItem, isActive && styles.homeTabItemActive]}
onPress={() => {
setActiveIndex(index);
if (currentKeyword && hasSearched) {
performSearch(currentKeyword);
}
}}
>
<Text style={isActive ? styles.homeTabTextActive : styles.homeTabText}>
{tab}
</Text>
</TouchableOpacity>
);
})}
</View>
<TabBar
tabs={TABS}
activeIndex={activeIndex}
onTabChange={(index) => {
setActiveIndex(index);
if (currentKeyword && hasSearched) {
performSearch(currentKeyword);
}
}}
variant="home"
style={{ paddingHorizontal: responsivePadding }}
/>
</View>
{/* 内容区域 */}
@@ -675,52 +641,10 @@ function createSearchScreenStyles(colors: AppColors) {
flex: 1,
backgroundColor: colors.background.default,
},
searchHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
},
searchShell: {
flex: 1,
},
cancelButton: {
borderRadius: borderRadius.full,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
},
cancelText: {
fontSize: fontSizes.md,
fontWeight: '600',
},
tabWrapper: {
backgroundColor: colors.background.default,
paddingVertical: spacing.xs,
},
homeTabSwitcher: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
homeTabItem: {
paddingVertical: spacing.sm,
alignItems: 'center',
justifyContent: 'center',
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
homeTabItemActive: {
borderBottomColor: colors.text.primary,
},
homeTabText: {
fontSize: 18,
fontWeight: '400',
color: colors.text.hint,
},
homeTabTextActive: {
fontSize: 18,
fontWeight: '700',
color: colors.text.primary,
},
suggestionsContainer: {
flex: 1,
},