diff --git a/src/components/call/CallScreen.tsx b/src/components/call/CallScreen.tsx index ce1b727..5b43f32 100644 --- a/src/components/call/CallScreen.tsx +++ b/src/components/call/CallScreen.tsx @@ -8,7 +8,7 @@ import { StatusBar, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { callStore } from '../../stores/callStore'; +import { callStore } from '../../stores/call'; import { RTCView } from 'react-native-webrtc'; const formatDuration = (seconds: number): string => { diff --git a/src/components/call/CallScreen.web.tsx b/src/components/call/CallScreen.web.tsx index 76bc8d7..c3ecc48 100644 --- a/src/components/call/CallScreen.web.tsx +++ b/src/components/call/CallScreen.web.tsx @@ -7,7 +7,7 @@ import { StatusBar, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { callStore } from '../../stores/callStore'; +import { callStore } from '../../stores/call'; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); diff --git a/src/components/call/FloatingCallWindow.tsx b/src/components/call/FloatingCallWindow.tsx index 99e2709..ba43fbc 100644 --- a/src/components/call/FloatingCallWindow.tsx +++ b/src/components/call/FloatingCallWindow.tsx @@ -7,7 +7,7 @@ import { Image, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { callStore } from '../../stores/callStore'; +import { callStore } from '../../stores/call'; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); diff --git a/src/components/call/IncomingCallModal.tsx b/src/components/call/IncomingCallModal.tsx index d9f2d57..6de9320 100644 --- a/src/components/call/IncomingCallModal.tsx +++ b/src/components/call/IncomingCallModal.tsx @@ -12,7 +12,7 @@ import { Platform, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { callStore } from '../../stores/callStore'; +import { callStore } from '../../stores/call'; import { blurActiveElement } from '../../infrastructure/platform'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); diff --git a/src/hooks/usePrefetch.ts b/src/hooks/usePrefetch.ts index a4aa89c..699b964 100644 --- a/src/hooks/usePrefetch.ts +++ b/src/hooks/usePrefetch.ts @@ -10,11 +10,11 @@ */ import { useCallback, useEffect, useRef } from 'react'; -import { postManager } from '../stores/postManager'; -import { groupManager } from '../stores/groupManager'; -import { userManager } from '../stores/userManager'; -import { messageManager } from '../stores/messageManager'; -import type { PostListTab } from '../stores/postListSources'; +import { postManager } from '../stores/post'; +import { groupManager } from '../stores/group'; +import { userManager } from '../stores/user'; +import { messageManager } from '../stores/message'; +import type { PostListTab } from '../stores/post'; // ==================== 预取配置 ==================== diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts index e263792..4df90ad 100644 --- a/src/navigation/hrefs.ts +++ b/src/navigation/hrefs.ts @@ -2,7 +2,7 @@ * Expo Router 路径构建(单一事实来源,避免魔法字符串散落) */ import type { SystemMessageResponse } from '../types/dto'; -import { routePayloadCache } from '../stores/routePayloadCache'; +import { routePayloadCache } from '../stores'; export function hrefPostDetail(postId: string, scrollToComments?: boolean): string { const q = scrollToComments ? '?scrollToComments=1' : ''; diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index 399396a..3b965bc 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -33,7 +33,7 @@ import { useRegisterStep, useRegisterFormData, useRegisterCountdown, -} from '../../stores/registerStore'; +} from '../../stores/auth'; import * as hrefs from '../../navigation/hrefs'; import { showPrompt } from '@/services/ui'; import { RegisterStepIndicator } from '../../components/common'; diff --git a/src/screens/auth/VerificationFormScreen.tsx b/src/screens/auth/VerificationFormScreen.tsx index 083641b..d980b0a 100644 --- a/src/screens/auth/VerificationFormScreen.tsx +++ b/src/screens/auth/VerificationFormScreen.tsx @@ -21,7 +21,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { useAppColors, type AppColors } from '../../theme'; -import { useVerificationStore } from '../../stores/verificationStore'; +import { useVerificationStore } from '../../stores/auth'; import { showPrompt } from '@/services/ui'; import { uploadService } from '@/services/upload'; import type { UserIdentity } from '../../types/dto'; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index f04657c..4d2e7a7 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -27,7 +27,7 @@ import { PagerView } from '../../components/common'; import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme'; import { Post } from '../../types'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; -import { useCurrentUser } from '../../stores/authStore'; +import { useCurrentUser } from '../../stores/auth'; import { channelService, postService } from '../../services'; import { PostCard, TabBar, SearchBar } from '../../components/business'; import type { PostCardAction } from '../../components/business/PostCard'; @@ -466,9 +466,9 @@ export const HomeScreen: React.FC = () => { // 宽屏下内容最大宽度 const contentMaxWidth = useMemo(() => { - if (isWideScreen) return 800; - if (isDesktop) return 720; - if (isTablet) return 640; + if (isWideScreen) return 1200; + if (isDesktop) return 1000; + if (isTablet) return 800; return width; // 移动端使用全宽 }, [width, isTablet, isDesktop, isWideScreen]); diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 3191d4e..6438ba1 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -35,7 +35,7 @@ import { } from '../../theme'; import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types'; import { useUserStore } from '../../stores'; -import { useCurrentUser } from '../../stores/authStore'; +import { useCurrentUser } from '../../stores/auth'; import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services'; import { postSyncService } from '@/services/post'; import { useCursorPagination } from '../../hooks/useCursorPagination'; diff --git a/src/screens/message/GroupInfoScreen.tsx b/src/screens/message/GroupInfoScreen.tsx index 7823c33..b54f8cf 100644 --- a/src/screens/message/GroupInfoScreen.tsx +++ b/src/screens/message/GroupInfoScreen.tsx @@ -49,8 +49,8 @@ import { User } from '../../types'; import { blurActiveElement } from '../../infrastructure/platform'; import { firstRouteParam } from '../../navigation/paramUtils'; import * as hrefs from '../../navigation/hrefs'; -import { messageManager } from '../../stores/messageManager'; -import { groupManager } from '../../stores/groupManager'; +import { messageManager } from '../../stores/message'; +import { groupManager } from '../../stores/group'; import MutualFollowSelectorModal from './components/MutualFollowSelectorModal'; import { AppBackButton } from '../../components/common'; diff --git a/src/screens/message/GroupInviteDetailScreen.tsx b/src/screens/message/GroupInviteDetailScreen.tsx index a903afc..810a608 100644 --- a/src/screens/message/GroupInviteDetailScreen.tsx +++ b/src/screens/message/GroupInviteDetailScreen.tsx @@ -5,9 +5,9 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { Avatar, Text } from '../../components/common'; -import { routePayloadCache } from '../../stores/routePayloadCache'; +import { routePayloadCache } from '../../stores'; import { groupService } from '@/services/message'; -import { groupManager } from '../../stores/groupManager'; +import { groupManager } from '../../stores/group'; import { GroupMemberResponse } from '../../types/dto'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; diff --git a/src/screens/message/GroupMembersScreen.tsx b/src/screens/message/GroupMembersScreen.tsx index 74cb4a9..600348c 100644 --- a/src/screens/message/GroupMembersScreen.tsx +++ b/src/screens/message/GroupMembersScreen.tsx @@ -27,11 +27,11 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from ' import { useAuthStore } from '../../stores'; import { groupService } from '@/services/message'; import { - createRemoteGroupMemberListSource, + createCursorGroupMemberListSource, GroupMemberListSourceKind, - IGroupMemberListPagedSource, -} from '../../stores/groupMemberListSources'; -import { enrichGroupMembersWithUserProfiles } from '../../stores/groupMemberProfileResolver'; + ICursorGroupMemberListPagedSource, +} from '../../stores/group'; +import { enrichGroupMembersWithUserProfiles } from '../../stores/group'; import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer, AppBackButton } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useCursorPagination } from '../../hooks/useCursorPagination'; @@ -75,8 +75,8 @@ const GroupMembersScreen: React.FC = () => { return GRID_CONFIG.mobile; }, [width]); - const memberListSource = useMemo(() => { - return createRemoteGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); + const memberListSource = useMemo(() => { + return createCursorGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); }, [groupId]); // 使用统一数据源 + 游标 Hook 管理成员列表 diff --git a/src/screens/message/GroupRequestDetailScreen.tsx b/src/screens/message/GroupRequestDetailScreen.tsx index c5e6290..87719ee 100644 --- a/src/screens/message/GroupRequestDetailScreen.tsx +++ b/src/screens/message/GroupRequestDetailScreen.tsx @@ -7,10 +7,10 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { Avatar, Text } from '../../components/common'; import * as hrefs from '../../navigation/hrefs'; -import { routePayloadCache } from '../../stores/routePayloadCache'; +import { routePayloadCache } from '../../stores'; import { groupService } from '@/services/message'; -import { groupManager } from '../../stores/groupManager'; -import { userManager } from '../../stores/userManager'; +import { groupManager } from '../../stores/group'; +import { userManager } from '../../stores/user'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; const GroupRequestDetailScreen: React.FC = () => { diff --git a/src/screens/message/JoinGroupScreen.tsx b/src/screens/message/JoinGroupScreen.tsx index 4bdc0de..64e920a 100644 --- a/src/screens/message/JoinGroupScreen.tsx +++ b/src/screens/message/JoinGroupScreen.tsx @@ -17,7 +17,7 @@ import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from ' import Avatar from '../../components/common/Avatar'; import Text from '../../components/common/Text'; import { groupService } from '@/services/message'; -import { groupManager } from '../../stores/groupManager'; +import { groupManager } from '../../stores/group'; import { GroupResponse, JoinType } from '../../types/dto'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import { EmptyState } from '../../components/common'; diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index 0985dfa..7eb4944 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -37,7 +37,7 @@ import { Text, ResponsiveContainer } from '../../components/common'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import * as hrefs from '../../navigation/hrefs'; import { useMessageManagerSystemUnreadCount } from '../../stores'; -import { messageManager } from '../../stores/messageManager'; +import { messageManager } from '../../stores/message'; const MESSAGE_TYPES = [ { key: 'all', title: '全部' }, diff --git a/src/screens/message/PrivateChatInfoScreen.tsx b/src/screens/message/PrivateChatInfoScreen.tsx index f0bfb52..7b68f32 100644 --- a/src/screens/message/PrivateChatInfoScreen.tsx +++ b/src/screens/message/PrivateChatInfoScreen.tsx @@ -22,8 +22,8 @@ import { authService } from '@/services/auth'; import { messageService } from '@/services/message'; import { ApiError } from '@/services/core'; import { messageRepository, conversationRepository } from '@/database'; -import { messageManager } from '../../stores/messageManager'; -import { userManager } from '../../stores/userManager'; +import { messageManager } from '../../stores/message'; +import { userManager } from '../../stores/user'; import { Avatar, Text, Button, Loading, Divider } from '../../components/common'; import { User } from '../../types'; import * as hrefs from '../../navigation/hrefs'; diff --git a/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx index 72d77b5..52d3c8f 100644 --- a/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx +++ b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx @@ -20,7 +20,7 @@ import { Avatar, Text } from '../../../../components/common'; import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; -import { groupManager } from '../../../../stores/groupManager'; +import { groupManager } from '../../../../stores/group'; import { groupService } from '@/services/message'; const { width: screenWidth } = Dimensions.get('window'); diff --git a/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx b/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx index 8dbe339..939f995 100644 --- a/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx +++ b/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx @@ -20,7 +20,7 @@ import { Avatar, Text } from '../../../../components/common'; import { useAppColors, spacing, fontSizes, shadows, borderRadius, type AppColors } from '../../../../theme'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; -import { groupManager } from '../../../../stores/groupManager'; +import { groupManager } from '../../../../stores/group'; import { groupService } from '@/services/message'; const { width: screenWidth } = Dimensions.get('window'); diff --git a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx index 4075500..755cc9c 100644 --- a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx +++ b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx @@ -33,7 +33,7 @@ import { } from '../../../../types/dto'; import { spacing, useAppColors, type AppColors } from '../../../../theme'; import { SenderInfo } from './types'; -import { useChatSettingsStore } from '../../../../stores/chatSettingsStore'; +import { useChatSettingsStore } from '../../../../stores/settings'; const IMAGE_MAX_WIDTH = 200; const IMAGE_MAX_HEIGHT = 260; diff --git a/src/screens/message/components/ChatScreen/bubbleStyles.ts b/src/screens/message/components/ChatScreen/bubbleStyles.ts index eff1a56..5e9ee93 100644 --- a/src/screens/message/components/ChatScreen/bubbleStyles.ts +++ b/src/screens/message/components/ChatScreen/bubbleStyles.ts @@ -5,7 +5,7 @@ import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; import { spacing, type AppColors } from '../../../../theme'; -import { useChatSettingsStore } from '../../../../stores/chatSettingsStore'; +import { useChatSettingsStore } from '../../../../stores/settings'; // 默认圆角,但会从 store 读取 export const BUBBLE_RADIUS = 16; diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index 102f9d5..bd3cb2f 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import { StyleSheet, Dimensions } from 'react-native'; import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme'; -import { useChatSettingsStore, CHAT_THEMES } from '../../../../stores/chatSettingsStore'; +import { useChatSettingsStore, CHAT_THEMES } from '../../../../stores/settings'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); diff --git a/src/screens/message/components/ChatScreen/useChatScreen.ts b/src/screens/message/components/ChatScreen/useChatScreen.ts index 466c5e6..9522eeb 100644 --- a/src/screens/message/components/ChatScreen/useChatScreen.ts +++ b/src/screens/message/components/ChatScreen/useChatScreen.ts @@ -28,8 +28,8 @@ import { ApiError } from '@/services/core'; // 【新架构】使用 MessageManager import { useChat, useGroupTyping, useGroupMuted, messageManager, callStore } from '../../../../stores'; import { groupService } from '@/services/message'; -import { userManager } from '../../../../stores/userManager'; -import { groupManager } from '../../../../stores/groupManager'; +import { userManager } from '../../../../stores/user'; +import { groupManager } from '../../../../stores/group'; import * as hrefs from '../../../../navigation/hrefs'; import { firstRouteParam } from '../../../../navigation/paramUtils'; import { diff --git a/src/screens/profile/ChatSettingsScreen.tsx b/src/screens/profile/ChatSettingsScreen.tsx index 975cd1f..eba1175 100644 --- a/src/screens/profile/ChatSettingsScreen.tsx +++ b/src/screens/profile/ChatSettingsScreen.tsx @@ -26,7 +26,7 @@ import { useChatSettingsStore, useChatSettingsActions, CHAT_THEMES, -} from '../../stores/chatSettingsStore'; +} from '../../stores/settings'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_MAX_WIDTH = 720; diff --git a/src/screens/profile/UserScreen.tsx b/src/screens/profile/UserScreen.tsx index fee5570..7eef9d7 100644 --- a/src/screens/profile/UserScreen.tsx +++ b/src/screens/profile/UserScreen.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useLocalSearchParams } from 'expo-router'; import UserProfileScreen from './UserProfileScreen'; -import { useCurrentUser } from '../../stores/authStore'; +import { useCurrentUser } from '../../stores/auth'; export const UserScreen: React.FC = () => { const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>(); diff --git a/src/screens/profile/VerificationSettingsScreen.tsx b/src/screens/profile/VerificationSettingsScreen.tsx index 97d8255..1065b5c 100644 --- a/src/screens/profile/VerificationSettingsScreen.tsx +++ b/src/screens/profile/VerificationSettingsScreen.tsx @@ -22,7 +22,7 @@ import { useVerificationStore, useVerificationStatus, useVerificationLoading, -} from '../../stores/verificationStore'; +} from '../../stores/auth'; import { showPrompt } from '@/services/ui'; import * as hrefs from '../../navigation/hrefs'; diff --git a/src/screens/profile/useUserProfile.ts b/src/screens/profile/useUserProfile.ts index 976062a..37add8c 100644 --- a/src/screens/profile/useUserProfile.ts +++ b/src/screens/profile/useUserProfile.ts @@ -10,10 +10,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { spacing, type AppColors } from '../../theme'; import { Post, User } from '../../types'; import { useUserStore } from '../../stores'; -import { useCurrentUser } from '../../stores/authStore'; +import { useCurrentUser } from '../../stores/auth'; import { postService, authService, messageService } from '../../services'; import { postSyncService } from '@/services/post'; -import { userManager } from '../../stores/userManager'; +import { userManager } from '../../stores/user'; import * as hrefs from '../../navigation/hrefs'; import { PostCardAction } from '../../components/business/PostCard'; diff --git a/src/screens/schedule/CourseDetailScreen.tsx b/src/screens/schedule/CourseDetailScreen.tsx index 36791c1..fdaf7da 100644 --- a/src/screens/schedule/CourseDetailScreen.tsx +++ b/src/screens/schedule/CourseDetailScreen.tsx @@ -5,7 +5,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, borderRadius, fontSizes, shadows, useAppColors, type AppColors } from '../../theme'; -import { routePayloadCache } from '../../stores/routePayloadCache'; +import { routePayloadCache } from '../../stores'; import { scheduleService } from '@/services/platform'; const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index d481e69..6c011bb 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -42,7 +42,7 @@ import { hasCourseInWeek, } from '../../types/schedule'; import * as hrefs from '../../navigation/hrefs'; -import { routePayloadCache } from '../../stores/routePayloadCache'; +import { routePayloadCache } from '../../stores'; import { scheduleService } from '@/services/platform'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); diff --git a/src/stores/authStore.ts b/src/stores/auth/authStore.ts similarity index 98% rename from src/stores/authStore.ts rename to src/stores/auth/authStore.ts index 0b421ba..b49f2cd 100644 --- a/src/stores/authStore.ts +++ b/src/stores/auth/authStore.ts @@ -12,10 +12,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { create } from 'zustand'; -import { User } from '../types'; -import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services'; +import { User } from '../../types'; +import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services'; import { wsService } from '@/services/core'; -import { callStore } from './callStore'; +import { callStore } from '../call/callStore'; import { useSessionStore } from './sessionStore'; import { switchDatabase, @@ -233,7 +233,7 @@ export const useAuthStore = create((set) => ({ // 7. 清除 sessionStore 的 userId useSessionStore.getState().setUserId(null); // 8. 清除 userManager 的内存缓存 - const { userManager } = await import('./userManager'); + const { userManager } = await import('../user'); userManager.invalidateUsers(); } catch (error) { console.error('[AuthStore] 登出失败:', error); diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts new file mode 100644 index 0000000..4134495 --- /dev/null +++ b/src/stores/auth/index.ts @@ -0,0 +1,10 @@ +/** + * 认证模块统一导出 + */ + +export { useAuthStore, useCurrentUser, useIsAuthenticated, useAuthLoading } from './authStore'; +export { useSessionStore, getCurrentUserId } from './sessionStore'; +export { useRegisterStore, useRegisterStep, useRegisterFormData, useRegisterCountdown } from './registerStore'; +export { useVerificationStore, useVerificationStatus, useIsVerified, useHasPendingVerification, useVerificationLoading } from './verificationStore'; + +export type { RegisterStep, RegisterFormData } from './registerStore'; \ No newline at end of file diff --git a/src/stores/registerStore.ts b/src/stores/auth/registerStore.ts similarity index 100% rename from src/stores/registerStore.ts rename to src/stores/auth/registerStore.ts diff --git a/src/stores/sessionStore.ts b/src/stores/auth/sessionStore.ts similarity index 100% rename from src/stores/sessionStore.ts rename to src/stores/auth/sessionStore.ts diff --git a/src/stores/verificationStore.ts b/src/stores/auth/verificationStore.ts similarity index 99% rename from src/stores/verificationStore.ts rename to src/stores/auth/verificationStore.ts index 2b5b707..98853a1 100644 --- a/src/stores/verificationStore.ts +++ b/src/stores/auth/verificationStore.ts @@ -11,7 +11,7 @@ import { SubmitVerificationRequest, UserIdentity, VerificationStatus, -} from '../types/dto'; +} from '../../types/dto'; interface VerificationState { // 状态 diff --git a/src/stores/callStore.ts b/src/stores/call/callStore.ts similarity index 99% rename from src/stores/callStore.ts rename to src/stores/call/callStore.ts index 6bd6584..8b116e7 100644 --- a/src/stores/callStore.ts +++ b/src/stores/call/callStore.ts @@ -8,9 +8,9 @@ import { WSErrorMessage, } from '@/services/core'; import { webrtcManager, ICEServer } from '@/services/webrtc'; -import { getCurrentUserId } from './sessionStore'; -import { useUserStore } from './userStore'; -import { userManager } from './userManager'; +import { getCurrentUserId } from '../auth/sessionStore'; +import { useUserStore } from '../userStore'; +import { userManager } from '../user'; export type CallStatus = | 'idle' // 空闲状态 diff --git a/src/stores/call/index.ts b/src/stores/call/index.ts new file mode 100644 index 0000000..5d762d2 --- /dev/null +++ b/src/stores/call/index.ts @@ -0,0 +1,6 @@ +/** + * 通话模块统一导出 + */ + +export { callStore } from './callStore'; +export type { CallType, CallSession, CallStatus, IncomingCallInfo } from './callStore'; \ No newline at end of file diff --git a/src/stores/group/GroupManager.ts b/src/stores/group/GroupManager.ts index c910834..fa05377 100644 --- a/src/stores/group/GroupManager.ts +++ b/src/stores/group/GroupManager.ts @@ -17,7 +17,7 @@ import { RemoteGroupListSourceKind, GROUP_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE, -} from '../groupListSources'; +} from './sources'; const dedupe = createDedupe(() => useGroupManagerStore); diff --git a/src/stores/group/hooks.ts b/src/stores/group/hooks.ts index 8ea5b52..c8b306c 100644 --- a/src/stores/group/hooks.ts +++ b/src/stores/group/hooks.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { useGroupManagerStore } from './groupStore'; import { groupManager } from './GroupManager'; import type { GroupResponse, GroupMemberResponse } from '../../types/dto'; -import { GROUP_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE } from '../groupListSources'; +import { GROUP_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE } from './sources'; export interface UseGroupsResult { groups: GroupResponse[]; diff --git a/src/stores/group/index.ts b/src/stores/group/index.ts index 9e0231f..963291f 100644 --- a/src/stores/group/index.ts +++ b/src/stores/group/index.ts @@ -23,5 +23,32 @@ export { type UseGroupMembersResult, } from './hooks'; +// ==================== 数据源 ==================== +export { + GROUP_LIST_PAGE_SIZE, + GROUP_MEMBER_LIST_PAGE_SIZE, + type GroupListPage, + type IGroupListPagedSource, + type GroupMemberListPage, + type IGroupMemberListPagedSource, + type GroupMemberListSourceKind, + type ICursorGroupMemberListPagedSource, + type RemoteGroupListSourceKind, + NetworkCursorGroupListPagedSource, + NetworkOffsetGroupListPagedSource, + SqliteGroupListPagedSource, + NetworkCursorGroupMemberListPagedSource, + NetworkOffsetGroupMemberListPagedSource, + SqliteGroupMemberListPagedSource, + CursorGroupMemberListSource, + OffsetGroupMemberListSource, + createRemoteGroupListSource, + createRemoteGroupMemberListSource, + createCursorGroupMemberListSource, +} from './sources'; + +// ==================== 工具 ==================== +export { enrichGroupMembersWithUserProfiles } from './profileResolver'; + // ==================== 默认导出 ==================== export { default } from './GroupManager'; \ No newline at end of file diff --git a/src/stores/groupMemberProfileResolver.ts b/src/stores/group/profileResolver.ts similarity index 92% rename from src/stores/groupMemberProfileResolver.ts rename to src/stores/group/profileResolver.ts index bfc8ac6..ad60569 100644 --- a/src/stores/groupMemberProfileResolver.ts +++ b/src/stores/group/profileResolver.ts @@ -1,6 +1,6 @@ -import { GroupMemberResponse, UserDTO } from '../types/dto'; +import { GroupMemberResponse, UserDTO } from '../../types/dto'; import { userCacheRepository } from '@/database'; -import { userManager } from './userManager'; +import { userManager } from '../user'; /** * 为群成员列表补全用户资料(昵称、头像、用户名)。 diff --git a/src/stores/groupListSources.ts b/src/stores/group/sources.ts similarity index 66% rename from src/stores/groupListSources.ts rename to src/stores/group/sources.ts index ef04a5f..9ac1927 100644 --- a/src/stores/groupListSources.ts +++ b/src/stores/group/sources.ts @@ -1,5 +1,5 @@ /** - * 群组列表数据源抽象:游标、偏移分页实现同一契约,GroupManager 只依赖接口。 + * 群组数据源抽象:群组列表 + 群组成员列表的游标/偏移分页实现。 */ import { @@ -8,7 +8,8 @@ import { GroupListResponse, GroupMemberListResponse, CursorPaginationRequest, -} from '../types/dto'; + CursorPaginationResponse, +} from '../../types/dto'; import { groupService } from '@/services/message'; import { groupCacheRepository } from '@/database'; @@ -17,26 +18,17 @@ export const GROUP_MEMBER_LIST_PAGE_SIZE = 50; // ==================== 群组列表数据源 ==================== -/** 单次拉取结果(一页或一批) */ export interface GroupListPage { items: GroupResponse[]; - /** 在当前源上是否还能再 loadNext 一页 */ hasMore: boolean; } -/** - * 分页式群组列表源:restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。 - */ export interface IGroupListPagedSource { restart(): void; loadNext(): Promise; - /** 至少成功 loadNext 过一次且仍有下一页时为 true */ readonly hasMore: boolean; } -/** - * 远端群组列表(游标分页) - */ export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource { private nextCursor: string | null = null; private hasMoreAfterLastLoad = false; @@ -73,9 +65,6 @@ export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource } } -/** - * 远端群组列表(偏移分页) - */ export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource { private nextPage = 1; private hasMoreAfterLastLoad = false; @@ -110,9 +99,6 @@ export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource } } -/** - * SQLite 群组列表缓存(单批,无后续页) - */ export class SqliteGroupListPagedSource implements IGroupListPagedSource { private consumed = false; @@ -139,26 +125,19 @@ export class SqliteGroupListPagedSource implements IGroupListPagedSource { } } -// ==================== 群组成员列表数据源 ==================== +// ==================== 群组成员列表数据源(Manager 内部用) ==================== -/** 成员列表单次拉取结果 */ export interface GroupMemberListPage { items: GroupMemberResponse[]; hasMore: boolean; } -/** - * 分页式群组成员列表源 - */ export interface IGroupMemberListPagedSource { restart(): void; loadNext(): Promise; readonly hasMore: boolean; } -/** - * 远端群组成员列表(游标分页) - */ export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource { private nextCursor: string | null = null; private hasMoreAfterLastLoad = false; @@ -197,9 +176,6 @@ export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberList } } -/** - * 远端群组成员列表(偏移分页) - */ export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource { private nextPage = 1; private hasMoreAfterLastLoad = false; @@ -240,9 +216,6 @@ export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberList } } -/** - * SQLite 群组成员列表缓存(单批,无后续页) - */ export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSource { private consumed = false; private readonly groupId: string; @@ -274,9 +247,115 @@ export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSo } } +// ==================== 群组成员列表数据源(Screen 层用,返回 CursorPaginationResponse) ==================== + +export type GroupMemberListSourceKind = 'cursor' | 'offset'; + +export interface ICursorGroupMemberListPagedSource { + restart(): void; + loadNext(): Promise>; + readonly hasMore: boolean; +} + +export class CursorGroupMemberListSource implements ICursorGroupMemberListPagedSource { + private nextCursor: string | null = null; + private hasMoreAfterLastLoad = false; + private loadedOnce = false; + private readonly groupId: string; + private readonly pageSize: number; + + constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { + this.groupId = groupId; + this.pageSize = pageSize; + } + + restart(): void { + this.nextCursor = null; + this.hasMoreAfterLastLoad = false; + this.loadedOnce = false; + } + + get hasMore(): boolean { + return this.loadedOnce && this.hasMoreAfterLastLoad; + } + + async loadNext(): Promise> { + const response = await groupService.getGroupMembersCursor( + this.groupId, + this.nextCursor == null + ? { page_size: this.pageSize } + : { cursor: this.nextCursor, page_size: this.pageSize } + ); + + this.nextCursor = response.next_cursor ?? null; + this.hasMoreAfterLastLoad = Boolean(response.has_more && response.next_cursor != null); + this.loadedOnce = true; + + return { + list: response.list || [], + next_cursor: response.next_cursor ?? null, + prev_cursor: response.prev_cursor ?? null, + has_more: response.has_more ?? false, + }; + } +} + +export class OffsetGroupMemberListSource implements ICursorGroupMemberListPagedSource { + private nextPage = 1; + private hasMoreAfterLastLoad = false; + private loadedOnce = false; + private readonly groupId: string; + private readonly pageSize: number; + + constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { + this.groupId = groupId; + this.pageSize = pageSize; + } + + restart(): void { + this.nextPage = 1; + this.hasMoreAfterLastLoad = false; + this.loadedOnce = false; + } + + get hasMore(): boolean { + return this.loadedOnce && this.hasMoreAfterLastLoad; + } + + async loadNext(): Promise> { + const response = await groupService.getMembers(this.groupId, this.nextPage, this.pageSize); + const list = response.list || []; + const currentPage = response.page ?? this.nextPage; + const totalPages = response.total_pages ?? currentPage; + const hasMore = currentPage < totalPages; + const nextPageCursor = hasMore ? String(currentPage + 1) : null; + const prevPageCursor = currentPage > 1 ? String(currentPage - 1) : null; + + this.nextPage = currentPage + 1; + this.hasMoreAfterLastLoad = hasMore; + this.loadedOnce = true; + + return { + list, + next_cursor: nextPageCursor, + prev_cursor: prevPageCursor, + has_more: hasMore, + }; + } +} + +export function createCursorGroupMemberListSource( + kind: GroupMemberListSourceKind, + groupId: string, + pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE +): ICursorGroupMemberListPagedSource { + return kind === 'offset' + ? new OffsetGroupMemberListSource(groupId, pageSize) + : new CursorGroupMemberListSource(groupId, pageSize); +} + // ==================== 工厂函数 ==================== -/** 远端群组列表分页策略 */ export type RemoteGroupListSourceKind = 'cursor' | 'offset'; export function createRemoteGroupListSource( diff --git a/src/stores/groupManager.ts b/src/stores/groupManager.ts deleted file mode 100644 index 753939e..0000000 --- a/src/stores/groupManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @deprecated 此文件已废弃,请从 './group' 导入 - * 保留此文件仅为向后兼容 - * - * 迁移说明: - * - groupManager 现在位于 ./group/GroupManager.ts - * - 使用 Zustand 进行状态管理 - * - 推荐使用 hooks: useGroups, useGroup, useGroupMembers - */ - -export { - groupManager, - GroupManager, - useGroupManagerStore, - useGroups, - useGroup, - useGroupMembers, - type GroupManagerState, - type GroupManagerActions, - type GroupManagerStore, -} from './group'; \ No newline at end of file diff --git a/src/stores/groupMemberListSources.ts b/src/stores/groupMemberListSources.ts deleted file mode 100644 index 2e08bee..0000000 --- a/src/stores/groupMemberListSources.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { groupService } from '@/services/message'; -import { CursorPaginationResponse, GroupMemberResponse } from '../types/dto'; - -export const GROUP_MEMBER_LIST_PAGE_SIZE = 50; - -export type GroupMemberListSourceKind = 'cursor' | 'offset'; - -export interface IGroupMemberListPagedSource { - restart(): void; - loadNext(): Promise>; - readonly hasMore: boolean; -} - -export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource { - private nextCursor: string | null = null; - private hasMoreAfterLastLoad = false; - private loadedOnce = false; - private readonly groupId: string; - private readonly pageSize: number; - - constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { - this.groupId = groupId; - this.pageSize = pageSize; - } - - restart(): void { - this.nextCursor = null; - this.hasMoreAfterLastLoad = false; - this.loadedOnce = false; - } - - get hasMore(): boolean { - return this.loadedOnce && this.hasMoreAfterLastLoad; - } - - async loadNext(): Promise> { - const response = await groupService.getGroupMembersCursor( - this.groupId, - this.nextCursor == null - ? { page_size: this.pageSize } - : { cursor: this.nextCursor, page_size: this.pageSize } - ); - - this.nextCursor = response.next_cursor ?? null; - this.hasMoreAfterLastLoad = Boolean(response.has_more && response.next_cursor != null); - this.loadedOnce = true; - - return { - list: response.list || [], - next_cursor: response.next_cursor ?? null, - prev_cursor: response.prev_cursor ?? null, - has_more: response.has_more ?? false, - }; - } -} - -export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource { - private nextPage = 1; - private hasMoreAfterLastLoad = false; - private loadedOnce = false; - private readonly groupId: string; - private readonly pageSize: number; - - constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { - this.groupId = groupId; - this.pageSize = pageSize; - } - - restart(): void { - this.nextPage = 1; - this.hasMoreAfterLastLoad = false; - this.loadedOnce = false; - } - - get hasMore(): boolean { - return this.loadedOnce && this.hasMoreAfterLastLoad; - } - - async loadNext(): Promise> { - const response = await groupService.getMembers(this.groupId, this.nextPage, this.pageSize); - const list = response.list || []; - const currentPage = response.page ?? this.nextPage; - const totalPages = response.total_pages ?? currentPage; - const hasMore = currentPage < totalPages; - const nextPageCursor = hasMore ? String(currentPage + 1) : null; - const prevPageCursor = currentPage > 1 ? String(currentPage - 1) : null; - - this.nextPage = currentPage + 1; - this.hasMoreAfterLastLoad = hasMore; - this.loadedOnce = true; - - return { - list, - next_cursor: nextPageCursor, - prev_cursor: prevPageCursor, - has_more: hasMore, - }; - } -} - -export function createRemoteGroupMemberListSource( - kind: GroupMemberListSourceKind, - groupId: string, - pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE -): IGroupMemberListPagedSource { - return kind === 'offset' - ? new NetworkOffsetGroupMemberListPagedSource(groupId, pageSize) - : new NetworkCursorGroupMemberListPagedSource(groupId, pageSize); -} - diff --git a/src/stores/index.ts b/src/stores/index.ts index ffa0652..25c0b1b 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -2,9 +2,14 @@ * 状态管理 Store 导出 */ -export { useAuthStore, useCurrentUser, useIsAuthenticated, useAuthLoading } from './authStore'; +// ==================== Auth 模块 ==================== +export { useAuthStore, useCurrentUser, useIsAuthenticated, useAuthLoading } from './auth'; +export { useSessionStore, getCurrentUserId } from './auth'; +export { useRegisterStore, useRegisterStep, useRegisterFormData, useRegisterCountdown } from './auth'; +export { useVerificationStore, useVerificationStatus, useIsVerified, useHasPendingVerification, useVerificationLoading } from './auth'; +export type { RegisterStep, RegisterFormData } from './auth'; -// 聊天设置 Store +// ==================== Settings 模块 ==================== export { useChatSettingsStore, useChatSettingsActions, @@ -13,92 +18,8 @@ export { useChatTheme, useChatNightMode, CHAT_THEMES, -} from './chatSettingsStore'; -export type { ChatSettings, ChatThemeId } from './chatSettingsStore'; - -export { - useUserStore, - usePosts, - useNotifications, - useNotificationBadge, - useMessageUnreadCount, - useSearchHistory -} from './userStore'; - -// MessageManager 新架构导出(已替换 conversationStore) -export { messageManager, MessageManager, useMessageStore } from './message'; -export type { - MessageManagerConversationListDeps, -} from './message'; -export { - CONVERSATION_LIST_PAGE_SIZE, - type ConversationListPage, - type IConversationListPagedSource, - type RemoteConversationListSourceKind, - NetworkRemoteConversationListPagedSource, - SqliteConversationListPagedSource, - createRemoteConversationListSource, -} from './conversationListSources'; -export { - GROUP_MEMBER_LIST_PAGE_SIZE, - type GroupMemberListSourceKind, - type IGroupMemberListPagedSource, - NetworkCursorGroupMemberListPagedSource, - NetworkOffsetGroupMemberListPagedSource, - createRemoteGroupMemberListSource, -} from './groupMemberListSources'; -export { enrichGroupMembersWithUserProfiles } from './groupMemberProfileResolver'; - -// ==================== 新架构 Manager 导出 ==================== - -// User 模块(userManager 已迁移到 Zustand) -export { - userManager, - UserManager, - useUserManagerStore, - useCurrentUser as useCurrentUserFromManager, - useUser, - useUsers, - type UserManagerState, - type UserManagerActions, - type UserManagerStore, -} from './user'; - -// Group 模块(groupManager 已迁移到 Zustand) -export { - groupManager, - GroupManager, - useGroupManagerStore, - useGroups, - useGroup, - useGroupMembers, - type GroupManagerState, - type GroupManagerActions, - type GroupManagerStore, -} from './group'; - -// Post 模块(postManager 已迁移到 Zustand) -export { - postManager, - PostManager, - usePostManagerStore, - usePostList, - usePostDetail, - type PostManagerState, - type PostManagerActions, - type PostManagerStore, -} from './post'; - -// ==================== 其他 Store ==================== - -export { - useHomeTabBarVisibilityStore, -} from './homeTabBarVisibilityStore'; -export { - useHomeTabPressStore, -} from './homeTabPressStore'; -export { callStore } from './callStore'; -export type { CallType, CallSession, CallStatus } from './callStore'; +} from './settings'; +export type { ChatSettings, ChatThemeId } from './settings'; export { useAppColors, useThemePreference, @@ -107,9 +28,40 @@ export { usePaperThemeFromStore, ThemeBootstrap, useThemeStore, - type ThemePreference, - type ResolvedScheme, -} from './themeStore'; + getSystemScheme, +} from './settings'; +export type { ThemePreference, ResolvedScheme } from './settings'; + +// ==================== UI 模块 ==================== +export { useHomeTabBarVisibilityStore } from './ui'; +export { useHomeTabPressStore } from './ui'; + +// ==================== Call 模块 ==================== +export { callStore } from './call'; +export type { CallType, CallSession, CallStatus, IncomingCallInfo } from './call'; + +// ==================== User Store(首页/通知/搜索等) ==================== +export { + useUserStore, + usePosts, + useNotifications, + useNotificationBadge, + useMessageUnreadCount, + useSearchHistory, +} from './userStore'; + +// ==================== Message 模块 ==================== +export { messageManager, MessageManager, useMessageStore } from './message'; +export type { MessageManagerConversationListDeps } from './message'; +export { + CONVERSATION_LIST_PAGE_SIZE, + type ConversationListPage, + type IConversationListPagedSource, + type RemoteConversationListSourceKind, + NetworkRemoteConversationListPagedSource, + SqliteConversationListPagedSource, + createRemoteConversationListSource, +} from './message'; export { useConversations as useMessageManagerConversations, useMessages, @@ -127,10 +79,61 @@ export { useSystemUnreadCount as useMessageManagerSystemUnreadCount, useGroupTyping, useGroupMuted, -} from './messageManagerHooks'; +} from './message'; + +// ==================== Group 模块 ==================== +export { + groupManager, + GroupManager, + useGroupManagerStore, + useGroups, + useGroup, + useGroupMembers, + type GroupManagerState, + type GroupManagerActions, + type GroupManagerStore, +} from './group'; +export { + GROUP_MEMBER_LIST_PAGE_SIZE, + type GroupMemberListSourceKind, + type IGroupMemberListPagedSource, + NetworkCursorGroupMemberListPagedSource, + NetworkOffsetGroupMemberListPagedSource, + createRemoteGroupMemberListSource, + enrichGroupMembersWithUserProfiles, +} from './group'; + +// ==================== Post 模块 ==================== +export { + postManager, + PostManager, + usePostManagerStore, + usePostList, + usePostDetail, + type PostManagerState, + type PostManagerActions, + type PostManagerStore, +} from './post'; +export { + type PostListTab, + type IPostListPagedSource, + createRemotePostListSource, +} from './post'; + +// ==================== User 模块(userManager) ==================== +export { + userManager, + UserManager, + useUserManagerStore, + useCurrentUser as useCurrentUserFromManager, + useUser, + useUsers, + type UserManagerState, + type UserManagerActions, + type UserManagerStore, +} from './user'; // ==================== 工具函数 ==================== - export { CacheEntry, createCacheEntry, @@ -138,4 +141,5 @@ export { isCacheValid, getCacheRemainingTTL, DEFAULT_TTL, -} from './utils/cache'; \ No newline at end of file +} from './utils/cache'; +export { routePayloadCache } from './utils/routePayloadCache'; diff --git a/src/stores/message/MessageManager.ts b/src/stores/message/MessageManager.ts index 5b35a0e..0688b16 100644 --- a/src/stores/message/MessageManager.ts +++ b/src/stores/message/MessageManager.ts @@ -12,7 +12,7 @@ */ import type { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../../types/dto'; -import { useAuthStore } from '../authStore'; +import { useAuthStore } from '../auth/authStore'; import type { MessageManagerConversationListDeps } from './types'; // 导入服务 diff --git a/src/stores/message/baseHooks.ts b/src/stores/message/baseHooks.ts new file mode 100644 index 0000000..4638d03 --- /dev/null +++ b/src/stores/message/baseHooks.ts @@ -0,0 +1,482 @@ +/** + * 消息模块 React Hooks + * + * 提供React组件与消息状态管理的集成 + * 所有hooks都基于zustand store的selector机制 + * 确保组件能实时获取状态更新 + * + * 重构说明: + * - 移除了 SubscriptionManager,改用 Zustand selector + * - Zustand 会自动处理依赖追踪和组件重渲染 + * - 不需要手动管理订阅/取消订阅 + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useShallow } from 'zustand/react/shallow'; +import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; +import { useMessageStore, normalizeConversationId } from './store'; + +// ==================== useConversations - 获取会话列表 ==================== + +interface UseConversationsReturn { + conversations: ConversationResponse[]; + isLoading: boolean; + refresh: () => Promise; + loadMore: () => Promise; + hasMore: boolean; +} + +/** + * 获取会话列表 + * 用于 MessageListScreen 等需要显示会话列表的组件 + * 使用 Zustand selector 自动订阅状态变化 + */ +export function useConversations( + fetchConversations: (forceRefresh: boolean, source: string) => Promise, + loadMoreConversations: () => Promise, + canLoadMore: () => boolean, + initialize: () => Promise +): UseConversationsReturn { + // 使用 Zustand selector 直接订阅状态 + const conversations = useMessageStore(state => state.conversationList); + const isLoading = useMessageStore(state => state.isLoadingConversations); + const [hasMore, setHasMore] = useState(() => canLoadMore()); + + useEffect(() => { + // 初始化 + initialize().catch(error => { + console.error('[useConversations] 初始化失败:', error); + }); + + // 冷启动兜底:延迟做一次强制刷新 + const coldStartSyncTimer = setTimeout(() => { + fetchConversations(true, 'hooks-initial-refresh').catch(error => { + console.error('[useConversations] 冷启动强制刷新失败:', error); + }); + }, 1200); + + return () => { + clearTimeout(coldStartSyncTimer); + }; + }, []); + + // 监听 hasMore 变化 + useEffect(() => { + setHasMore(canLoadMore()); + }, [conversations, canLoadMore]); + + const refresh = useCallback(async () => { + await fetchConversations(true, 'hooks-manual-refresh'); + }, [fetchConversations]); + + const loadMore = useCallback(async () => { + await loadMoreConversations(); + }, [loadMoreConversations]); + + return { + conversations, + isLoading, + refresh, + loadMore, + hasMore, + }; +} + +// ==================== useMessages - 获取指定会话的消息 ==================== + +interface UseMessagesReturn { + messages: MessageResponse[]; + isLoading: boolean; + hasMore: boolean; + loadMore: () => Promise; + refresh: () => Promise; +} + +/** + * 获取指定会话的消息 + * 使用 Zustand selector 自动订阅消息变化 + */ +export function useMessages( + conversationId: string, + fetchMessages: (conversationId: string) => Promise, + loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise +): UseMessagesReturn { + const normalizedConversationId = normalizeConversationId(conversationId); + const store = useMessageStore(); + + // 使用 useShallow 避免每次返回新数组引用导致的无限循环 + const messages = useMessageStore( + useShallow(state => state.messagesMap.get(normalizedConversationId) || []) + ); + const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId)); + const [hasMore, setHasMore] = useState(true); + + useEffect(() => { + // 激活会话 + fetchMessages(normalizedConversationId).catch(error => { + console.error('[useMessages] 激活会话失败:', error); + }); + + return () => { + // 清理活动会话 + if (store.getActiveConversation() === normalizedConversationId) { + store.setCurrentConversation(null); + } + }; + }, [normalizedConversationId]); + + const loadMore = useCallback(async () => { + const currentMessages = messages; + if (currentMessages.length === 0) return; + + // 消息数组在 store 内保持 seq 升序,首项即最早消息 + const minSeq = currentMessages[0]?.seq; + if (minSeq === undefined) return; + + const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20); + setHasMore(loadedMessages.length > 0); + }, [conversationId, messages, loadMoreMessages]); + + const refresh = useCallback(async () => { + await fetchMessages(conversationId); + }, [conversationId, fetchMessages]); + + return { + messages, + isLoading: isLoadingMessages, + hasMore, + loadMore, + refresh, + }; +} + +// ==================== useSendMessage - 发送消息 ==================== + +interface UseSendMessageReturn { + sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise; + isSending: boolean; +} + +/** + * 发送消息 + */ +export function useSendMessage( + conversationId: string, + sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise +): UseSendMessageReturn { + const [isSending, setIsSending] = useState(false); + + const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => { + setIsSending(true); + try { + const message = await sendMessageService(conversationId, segments, options); + return message; + } catch (error) { + console.error('[useSendMessage] 发送消息失败:', error); + return null; + } finally { + setIsSending(false); + } + }, [conversationId, sendMessageService]); + + return { + sendMessage, + isSending, + }; +} + +// ==================== useMarkAsRead - 标记已读 ==================== + +interface UseMarkAsReadReturn { + markAsRead: (seq: number) => Promise; + markAllAsRead: () => Promise; +} + +/** + * 标记已读 + */ +export function useMarkAsRead( + conversationId: string, + markAsReadService: (conversationId: string, seq: number) => Promise, + markAllAsReadService: () => Promise +): UseMarkAsReadReturn { + const markAsRead = useCallback(async (seq: number) => { + await markAsReadService(conversationId, seq); + }, [conversationId, markAsReadService]); + + const markAllAsRead = useCallback(async () => { + await markAllAsReadService(); + }, [markAllAsReadService]); + + return { + markAsRead, + markAllAsRead, + }; +} + +// ==================== useUnreadCount - 获取未读数 ==================== + +interface UseUnreadCountReturn { + totalUnreadCount: number; + systemUnreadCount: number; + refresh: () => Promise; +} + +/** + * 获取未读数 + * 使用 Zustand selector 直接订阅未读数变化 + */ +export function useUnreadCount(fetchUnreadCount: () => Promise): UseUnreadCountReturn { + // 使用 Zustand selector 直接订阅未读数 + const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); + const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); + + useEffect(() => { + // 初始获取 + fetchUnreadCount(); + }, []); + + const refresh = useCallback(async () => { + await fetchUnreadCount(); + }, [fetchUnreadCount]); + + return { + totalUnreadCount, + systemUnreadCount, + refresh, + }; +} + +// ==================== useSystemUnreadCount - 获取系统消息未读数 ==================== + +interface UseSystemUnreadCountReturn { + systemUnreadCount: number; + setSystemUnreadCount: (count: number) => void; + incrementSystemUnreadCount: () => void; + decrementSystemUnreadCount: (count?: number) => void; +} + +/** + * 获取系统消息未读数 + * 使用 Zustand selector 直接订阅 + */ +export function useSystemUnreadCount( + setSystemUnreadCountService: (count: number) => void, + incrementSystemUnreadCountService: () => void, + decrementSystemUnreadCountService: (count?: number) => void +): UseSystemUnreadCountReturn { + // 使用 Zustand selector 直接订阅 + const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); + + const setSystemUnreadCount = useCallback((count: number) => { + setSystemUnreadCountService(count); + }, [setSystemUnreadCountService]); + + const incrementSystemUnreadCount = useCallback(() => { + incrementSystemUnreadCountService(); + }, [incrementSystemUnreadCountService]); + + const decrementSystemUnreadCount = useCallback((count = 1) => { + decrementSystemUnreadCountService(count); + }, [decrementSystemUnreadCountService]); + + return { + systemUnreadCount, + setSystemUnreadCount, + incrementSystemUnreadCount, + decrementSystemUnreadCount, + }; +} + +// ==================== useConversation - 获取单个会话 ==================== + +interface UseConversationReturn { + conversation: ConversationResponse | null; + refresh: () => Promise; +} + +/** + * 获取单个会话 + * 使用 Zustand selector 直接订阅会话变化 + */ +export function useConversation( + conversationId: string, + fetchConversationDetail: (conversationId: string) => Promise +): UseConversationReturn { + // 使用 useShallow 避免每次返回新对象引用导致的无限循环 + const conversation = useMessageStore( + useShallow(state => state.conversations.get(conversationId) || null) + ); + + const refresh = useCallback(async () => { + await fetchConversationDetail(conversationId); + }, [conversationId, fetchConversationDetail]); + + return { + conversation, + refresh, + }; +} + +// ==================== useMessageManager - 通用状态 ==================== + +interface UseMessageManagerReturn { + isConnected: boolean; +} + +/** + * 通用消息管理状态 + * 使用 Zustand selector 直接订阅连接状态 + */ +export function useMessageManager(initialize: () => Promise): UseMessageManagerReturn { + // 使用 Zustand selector 直接订阅连接状态 + const isConnected = useMessageStore(state => state.isWSConnected); + + useEffect(() => { + initialize().catch(error => { + console.error('[useMessageManager] 初始化失败:', error); + }); + }, []); + + return { + isConnected, + }; +} + +// ==================== useCreateConversation - 创建会话 ==================== + +interface UseCreateConversationReturn { + createConversation: (userId: string) => Promise; + isCreating: boolean; +} + +/** + * 创建会话 + */ +export function useCreateConversation( + createConversationService: (userId: string) => Promise +): UseCreateConversationReturn { + const [isCreating, setIsCreating] = useState(false); + + const createConversation = useCallback(async (userId: string) => { + setIsCreating(true); + try { + const conversation = await createConversationService(userId); + return conversation; + } catch (error) { + console.error('[useCreateConversation] 创建会话失败:', error); + return null; + } finally { + setIsCreating(false); + } + }, [createConversationService]); + + return { + createConversation, + isCreating, + }; +} + +// ==================== useUpdateConversation - 更新会话 ==================== + +interface UseUpdateConversationReturn { + updateConversation: (conversationId: string, updates: Partial) => void; +} + +/** + * 更新会话 + */ +export function useUpdateConversation( + updateConversationService: (conversationId: string, updates: Partial) => void +): UseUpdateConversationReturn { + const updateConversation = useCallback((conversationId: string, updates: Partial) => { + updateConversationService(conversationId, updates); + }, [updateConversationService]); + + return { + updateConversation, + }; +} + +// ==================== useGroupTyping - 群聊输入状态 ==================== + +interface UseGroupTypingReturn { + typingUsers: string[]; +} + +/** + * 获取群聊输入状态 + * 使用 Zustand selector 直接订阅 + */ +export function useGroupTyping(groupId: string): UseGroupTypingReturn { + // 使用 useShallow 避免每次返回新数组引用导致的无限循环 + const typingUsers = useMessageStore( + useShallow(state => state.typingUsersMap.get(groupId) || []) + ); + + return { + typingUsers, + }; +} + +// ==================== useGroupMuted - 群聊禁言状态 ==================== + +interface UseGroupMutedReturn { + isMuted: boolean; + setMutedStatus: (muted: boolean) => void; +} + +/** + * 获取群聊禁言状态 + * 使用 Zustand selector 直接订阅 + */ +export function useGroupMuted(groupId: string): UseGroupMutedReturn { + // 使用 Zustand selector 直接订阅禁言状态 + const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); + const store = useMessageStore(); + + const setMutedStatus = useCallback((muted: boolean) => { + store.setMutedStatus(groupId, muted); + }, [groupId, store]); + + return { + isMuted, + setMutedStatus, + }; +} + +// ==================== useConnectionStatus - 连接状态 ==================== + +interface UseConnectionStatusReturn { + isConnected: boolean; +} + +/** + * 获取连接状态 + * 使用 Zustand selector 直接订阅 + */ +export function useConnectionStatus(): UseConnectionStatusReturn { + const isConnected = useMessageStore(state => state.isWSConnected); + + return { + isConnected, + }; +} + +// ==================== useIsInitialized - 初始化状态 ==================== + +interface UseIsInitializedReturn { + isInitialized: boolean; +} + +/** + * 获取初始化状态 + * 使用 Zustand selector 直接订阅 + */ +export function useIsInitialized(): UseIsInitializedReturn { + const isInitialized = useMessageStore(state => state.isInitialized); + + return { + isInitialized, + }; +} diff --git a/src/stores/message/hooks.ts b/src/stores/message/hooks.ts index 4638d03..6e0c0e4 100644 --- a/src/stores/message/hooks.ts +++ b/src/stores/message/hooks.ts @@ -1,12 +1,12 @@ /** - * 消息模块 React Hooks - * - * 提供React组件与消息状态管理的集成 - * 所有hooks都基于zustand store的selector机制 + * MessageManager React Hooks + * + * 提供React组件与MessageManager的集成 + * 所有hooks都基于Zustand selector机制 * 确保组件能实时获取状态更新 * * 重构说明: - * - 移除了 SubscriptionManager,改用 Zustand selector + * - 移除了手动订阅机制,改用 Zustand selector * - Zustand 会自动处理依赖追踪和组件重渲染 * - 不需要手动管理订阅/取消订阅 */ @@ -14,7 +14,8 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; -import { useMessageStore, normalizeConversationId } from './store'; +import { messageManager } from './MessageManager'; +import { useMessageStore } from './store'; // ==================== useConversations - 获取会话列表 ==================== @@ -28,29 +29,23 @@ interface UseConversationsReturn { /** * 获取会话列表 - * 用于 MessageListScreen 等需要显示会话列表的组件 * 使用 Zustand selector 自动订阅状态变化 */ -export function useConversations( - fetchConversations: (forceRefresh: boolean, source: string) => Promise, - loadMoreConversations: () => Promise, - canLoadMore: () => boolean, - initialize: () => Promise -): UseConversationsReturn { +export function useConversations(): UseConversationsReturn { // 使用 Zustand selector 直接订阅状态 const conversations = useMessageStore(state => state.conversationList); const isLoading = useMessageStore(state => state.isLoadingConversations); - const [hasMore, setHasMore] = useState(() => canLoadMore()); + const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations()); useEffect(() => { - // 初始化 - initialize().catch(error => { + // 初始化时确保 MessageManager 已初始化 + messageManager.initialize().catch(error => { console.error('[useConversations] 初始化失败:', error); }); - // 冷启动兜底:延迟做一次强制刷新 + // 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表 const coldStartSyncTimer = setTimeout(() => { - fetchConversations(true, 'hooks-initial-refresh').catch(error => { + messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { console.error('[useConversations] 冷启动强制刷新失败:', error); }); }, 1200); @@ -62,16 +57,16 @@ export function useConversations( // 监听 hasMore 变化 useEffect(() => { - setHasMore(canLoadMore()); - }, [conversations, canLoadMore]); + setHasMore(messageManager.canLoadMoreConversations()); + }, [conversations]); const refresh = useCallback(async () => { - await fetchConversations(true, 'hooks-manual-refresh'); - }, [fetchConversations]); + await messageManager.refreshConversations(true, 'hooks-manual-refresh'); + }, []); const loadMore = useCallback(async () => { - await loadMoreConversations(); - }, [loadMoreConversations]); + await messageManager.loadMoreConversations(); + }, []); return { conversations, @@ -96,50 +91,64 @@ interface UseMessagesReturn { * 获取指定会话的消息 * 使用 Zustand selector 自动订阅消息变化 */ -export function useMessages( - conversationId: string, - fetchMessages: (conversationId: string) => Promise, - loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise -): UseMessagesReturn { - const normalizedConversationId = normalizeConversationId(conversationId); - const store = useMessageStore(); +export function useMessages(conversationId: string | null): UseMessagesReturn { + const normalizedConversationId = conversationId ? String(conversationId) : null; // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const messages = useMessageStore( - useShallow(state => state.messagesMap.get(normalizedConversationId) || []) + useShallow(state => + normalizedConversationId ? (state.messagesMap.get(normalizedConversationId) || []) : [] + ) + ); + const isLoadingMessages = useMessageStore(state => + normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false ); - const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId)); const [hasMore, setHasMore] = useState(true); + const loadMoreInFlightRef = useRef(false); useEffect(() => { - // 激活会话 - fetchMessages(normalizedConversationId).catch(error => { + if (!normalizedConversationId) { + return; + } + + // 架构入口:激活会话并完成初始化/同步 + messageManager.activateConversation(normalizedConversationId).catch(error => { console.error('[useMessages] 激活会话失败:', error); }); return () => { // 清理活动会话 - if (store.getActiveConversation() === normalizedConversationId) { - store.setCurrentConversation(null); + if (messageManager.getActiveConversation() === normalizedConversationId) { + messageManager.setActiveConversation(null); } }; }, [normalizedConversationId]); const loadMore = useCallback(async () => { - const currentMessages = messages; + if (!conversationId || !hasMore || loadMoreInFlightRef.current) return; + + const currentMessages = messageManager.getMessages(conversationId); if (currentMessages.length === 0) return; - // 消息数组在 store 内保持 seq 升序,首项即最早消息 - const minSeq = currentMessages[0]?.seq; - if (minSeq === undefined) return; + // 消息数组在 MessageManager 内保持 seq 升序,首项即最早消息 + const minSeq = currentMessages[0]?.seq ?? Math.min(...currentMessages.map(m => m.seq)); - const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20); - setHasMore(loadedMessages.length > 0); - }, [conversationId, messages, loadMoreMessages]); + loadMoreInFlightRef.current = true; + const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20); + loadMoreInFlightRef.current = false; + + // 如果没有加载到新消息,说明没有更多了 + if (loadedMessages.length === 0) { + setHasMore(false); + } + }, [conversationId, hasMore]); const refresh = useCallback(async () => { - await fetchMessages(conversationId); - }, [conversationId, fetchMessages]); + if (!conversationId) return; + + await messageManager.fetchMessages(conversationId); + setHasMore(true); + }, [conversationId]); return { messages, @@ -160,24 +169,23 @@ interface UseSendMessageReturn { /** * 发送消息 */ -export function useSendMessage( - conversationId: string, - sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise -): UseSendMessageReturn { +export function useSendMessage(conversationId: string | null): UseSendMessageReturn { const [isSending, setIsSending] = useState(false); - const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => { - setIsSending(true); - try { - const message = await sendMessageService(conversationId, segments, options); - return message; - } catch (error) { - console.error('[useSendMessage] 发送消息失败:', error); - return null; - } finally { - setIsSending(false); - } - }, [conversationId, sendMessageService]); + const sendMessage = useCallback( + async (segments: MessageSegment[], options?: { replyToId?: string }): Promise => { + if (!conversationId) return null; + + setIsSending(true); + try { + const message = await messageManager.sendMessage(conversationId, segments, options); + return message; + } finally { + setIsSending(false); + } + }, + [conversationId] + ); return { sendMessage, @@ -190,27 +198,42 @@ export function useSendMessage( interface UseMarkAsReadReturn { markAsRead: (seq: number) => Promise; markAllAsRead: () => Promise; + isMarking: boolean; } /** * 标记已读 */ -export function useMarkAsRead( - conversationId: string, - markAsReadService: (conversationId: string, seq: number) => Promise, - markAllAsReadService: () => Promise -): UseMarkAsReadReturn { - const markAsRead = useCallback(async (seq: number) => { - await markAsReadService(conversationId, seq); - }, [conversationId, markAsReadService]); +export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn { + const [isMarking, setIsMarking] = useState(false); + + const markAsRead = useCallback( + async (seq: number) => { + if (!conversationId) return; + + setIsMarking(true); + try { + await messageManager.markAsRead(conversationId, seq); + } finally { + setIsMarking(false); + } + }, + [conversationId] + ); const markAllAsRead = useCallback(async () => { - await markAllAsReadService(); - }, [markAllAsReadService]); + setIsMarking(true); + try { + await messageManager.markAllAsRead(); + } finally { + setIsMarking(false); + } + }, []); return { markAsRead, markAllAsRead, + isMarking, }; } @@ -219,101 +242,78 @@ export function useMarkAsRead( interface UseUnreadCountReturn { totalUnreadCount: number; systemUnreadCount: number; - refresh: () => Promise; } /** - * 获取未读数 + * 获取未读消息数 * 使用 Zustand selector 直接订阅未读数变化 */ -export function useUnreadCount(fetchUnreadCount: () => Promise): UseUnreadCountReturn { +export function useUnreadCount(): UseUnreadCountReturn { // 使用 Zustand selector 直接订阅未读数 const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); useEffect(() => { - // 初始获取 - fetchUnreadCount(); + // 初始化时获取最新未读数 + messageManager.fetchUnreadCount(); }, []); - const refresh = useCallback(async () => { - await fetchUnreadCount(); - }, [fetchUnreadCount]); - return { totalUnreadCount, systemUnreadCount, - refresh, }; } -// ==================== useSystemUnreadCount - 获取系统消息未读数 ==================== - -interface UseSystemUnreadCountReturn { - systemUnreadCount: number; - setSystemUnreadCount: (count: number) => void; - incrementSystemUnreadCount: () => void; - decrementSystemUnreadCount: (count?: number) => void; -} +// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ==================== /** - * 获取系统消息未读数 + * 获取总未读消息数(会话未读 + 系统消息未读) * 使用 Zustand selector 直接订阅 */ -export function useSystemUnreadCount( - setSystemUnreadCountService: (count: number) => void, - incrementSystemUnreadCountService: () => void, - decrementSystemUnreadCountService: (count?: number) => void -): UseSystemUnreadCountReturn { - // 使用 Zustand selector 直接订阅 +export function useTotalUnreadCount(): number { + const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - const setSystemUnreadCount = useCallback((count: number) => { - setSystemUnreadCountService(count); - }, [setSystemUnreadCountService]); + useEffect(() => { + // 初始化时获取最新未读数 + messageManager.fetchUnreadCount(); + }, []); - const incrementSystemUnreadCount = useCallback(() => { - incrementSystemUnreadCountService(); - }, [incrementSystemUnreadCountService]); - - const decrementSystemUnreadCount = useCallback((count = 1) => { - decrementSystemUnreadCountService(count); - }, [decrementSystemUnreadCountService]); - - return { - systemUnreadCount, - setSystemUnreadCount, - incrementSystemUnreadCount, - decrementSystemUnreadCount, - }; + return totalUnreadCount + systemUnreadCount; } -// ==================== useConversation - 获取单个会话 ==================== +// ==================== useConversation - 获取单个会话信息 ==================== interface UseConversationReturn { conversation: ConversationResponse | null; + isLoading: boolean; refresh: () => Promise; } /** - * 获取单个会话 + * 获取单个会话信息 * 使用 Zustand selector 直接订阅会话变化 */ -export function useConversation( - conversationId: string, - fetchConversationDetail: (conversationId: string) => Promise -): UseConversationReturn { +export function useConversation(conversationId: string | null): UseConversationReturn { // 使用 useShallow 避免每次返回新对象引用导致的无限循环 const conversation = useMessageStore( - useShallow(state => state.conversations.get(conversationId) || null) + useShallow(state => + conversationId ? (state.conversations.get(conversationId) || null) : null + ) ); + const [isLoading, setIsLoading] = useState(false); const refresh = useCallback(async () => { - await fetchConversationDetail(conversationId); - }, [conversationId, fetchConversationDetail]); + if (!conversationId) return; + + setIsLoading(true); + await messageManager.fetchConversationDetail(conversationId); + setIsLoading(false); + }, [conversationId]); return { conversation, + isLoading, refresh, }; } @@ -322,24 +322,28 @@ export function useConversation( interface UseMessageManagerReturn { isConnected: boolean; + isInitialized: boolean; } /** - * 通用消息管理状态 + * 通用MessageManager状态 * 使用 Zustand selector 直接订阅连接状态 */ -export function useMessageManager(initialize: () => Promise): UseMessageManagerReturn { - // 使用 Zustand selector 直接订阅连接状态 +export function useMessageManager(): UseMessageManagerReturn { + // 使用 Zustand selector 直接订阅状态 const isConnected = useMessageStore(state => state.isWSConnected); + const isInitialized = useMessageStore(state => state.isInitialized); useEffect(() => { - initialize().catch(error => { + // 初始化 + messageManager.initialize().catch(error => { console.error('[useMessageManager] 初始化失败:', error); }); }, []); return { isConnected, + isInitialized, }; } @@ -351,25 +355,20 @@ interface UseCreateConversationReturn { } /** - * 创建会话 + * 创建私聊会话 */ -export function useCreateConversation( - createConversationService: (userId: string) => Promise -): UseCreateConversationReturn { +export function useCreateConversation(): UseCreateConversationReturn { const [isCreating, setIsCreating] = useState(false); - const createConversation = useCallback(async (userId: string) => { + const createConversation = useCallback(async (userId: string): Promise => { setIsCreating(true); try { - const conversation = await createConversationService(userId); + const conversation = await messageManager.createConversation(userId); return conversation; - } catch (error) { - console.error('[useCreateConversation] 创建会话失败:', error); - return null; } finally { setIsCreating(false); } - }, [createConversationService]); + }, []); return { createConversation, @@ -384,20 +383,130 @@ interface UseUpdateConversationReturn { } /** - * 更新会话 + * 本地更新会话信息 */ -export function useUpdateConversation( - updateConversationService: (conversationId: string, updates: Partial) => void -): UseUpdateConversationReturn { +export function useUpdateConversation(): UseUpdateConversationReturn { const updateConversation = useCallback((conversationId: string, updates: Partial) => { - updateConversationService(conversationId, updates); - }, [updateConversationService]); + messageManager.updateConversation(conversationId, updates); + }, []); return { updateConversation, }; } +// ==================== useSystemUnreadCount - 系统消息未读数操作 ==================== + +interface UseSystemUnreadCountReturn { + setSystemUnreadCount: (count: number) => void; + incrementSystemUnreadCount: () => void; + decrementSystemUnreadCount: (count?: number) => void; +} + +/** + * 系统消息未读数操作 + */ +export function useSystemUnreadCount(): UseSystemUnreadCountReturn { + const setSystemUnreadCount = useCallback((count: number) => { + messageManager.setSystemUnreadCount(count); + }, []); + + const incrementSystemUnreadCount = useCallback(() => { + messageManager.incrementSystemUnreadCount(); + }, []); + + const decrementSystemUnreadCount = useCallback((count?: number) => { + messageManager.decrementSystemUnreadCount(count); + }, []); + + return { + setSystemUnreadCount, + incrementSystemUnreadCount, + decrementSystemUnreadCount, + }; +} + +// ==================== useMessageListRefresh - MessageListScreen焦点刷新 ==================== + +interface UseMessageListRefreshReturn { + refresh: () => Promise; + isRefreshing: boolean; +} + +/** + * MessageListScreen焦点刷新Hook + */ +export function useMessageListRefresh(): UseMessageListRefreshReturn { + const [isRefreshing, setIsRefreshing] = useState(false); + const lastRefreshTimeRef = useRef(0); + + const refresh = useCallback(async () => { + // 防止重复刷新(最小间隔500ms) + const now = Date.now(); + if (now - lastRefreshTimeRef.current < 500) { + return; + } + lastRefreshTimeRef.current = now; + + setIsRefreshing(true); + try { + await messageManager.refreshConversations(true, 'hooks-load-more-fallback'); + } catch (error) { + console.error('[useMessageListRefresh] 刷新失败:', error); + } finally { + setIsRefreshing(false); + } + }, []); + + return { + refresh, + isRefreshing, + }; +} + +// ==================== 便捷hooks组合 ==================== + +/** + * ChatScreen专用Hook组合 + */ +interface UseChatReturn { + // 消息相关 + messages: MessageResponse[]; + isLoadingMessages: boolean; + hasMoreMessages: boolean; + loadMoreMessages: () => Promise; + refreshMessages: () => Promise; + + // 发送消息 + sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise; + isSending: boolean; + + // 已读相关 + markAsRead: (seq: number) => Promise; + + // 会话信息 + conversation: ConversationResponse | null; +} + +export function useChat(conversationId: string | null): UseChatReturn { + const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId); + const { sendMessage, isSending } = useSendMessage(conversationId); + const { markAsRead } = useMarkAsRead(conversationId); + const { conversation } = useConversation(conversationId); + + return { + messages, + isLoadingMessages, + hasMoreMessages, + loadMoreMessages: loadMore, + refreshMessages: refresh, + sendMessage, + isSending, + markAsRead, + conversation, + }; +} + // ==================== useGroupTyping - 群聊输入状态 ==================== interface UseGroupTypingReturn { @@ -408,75 +517,70 @@ interface UseGroupTypingReturn { * 获取群聊输入状态 * 使用 Zustand selector 直接订阅 */ -export function useGroupTyping(groupId: string): UseGroupTypingReturn { +export function useGroupTyping(groupId: string | null): UseGroupTypingReturn { // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const typingUsers = useMessageStore( - useShallow(state => state.typingUsersMap.get(groupId) || []) + useShallow(state => + groupId ? (state.typingUsersMap.get(groupId) || []) : [] + ) ); - return { - typingUsers, - }; + return { typingUsers }; } // ==================== useGroupMuted - 群聊禁言状态 ==================== interface UseGroupMutedReturn { isMuted: boolean; - setMutedStatus: (muted: boolean) => void; + setMuted: (muted: boolean) => void; } /** * 获取群聊禁言状态 * 使用 Zustand selector 直接订阅 */ -export function useGroupMuted(groupId: string): UseGroupMutedReturn { +export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn { // 使用 Zustand selector 直接订阅禁言状态 - const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); - const store = useMessageStore(); + const isMuted = useMessageStore(state => + groupId ? (state.mutedStatusMap.get(groupId) || false) : false + ); - const setMutedStatus = useCallback((muted: boolean) => { - store.setMutedStatus(groupId, muted); - }, [groupId, store]); + const setMuted = useCallback((muted: boolean) => { + if (groupId) { + messageManager.setMutedStatus(groupId, muted); + } + }, [groupId]); - return { - isMuted, - setMutedStatus, - }; -} - -// ==================== useConnectionStatus - 连接状态 ==================== - -interface UseConnectionStatusReturn { - isConnected: boolean; + return { isMuted, setMuted }; } /** - * 获取连接状态 - * 使用 Zustand selector 直接订阅 + * MessageListScreen专用Hook组合 */ -export function useConnectionStatus(): UseConnectionStatusReturn { - const isConnected = useMessageStore(state => state.isWSConnected); +interface UseMessageListReturn { + conversations: ConversationResponse[]; + isLoading: boolean; + refresh: () => Promise; + loadMore: () => Promise; + hasMore: boolean; + totalUnreadCount: number; + systemUnreadCount: number; + markAllAsRead: () => Promise; +} + +export function useMessageList(): UseMessageListReturn { + const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations(); + const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); + const { markAllAsRead } = useMarkAsRead(null); return { - isConnected, - }; -} - -// ==================== useIsInitialized - 初始化状态 ==================== - -interface UseIsInitializedReturn { - isInitialized: boolean; -} - -/** - * 获取初始化状态 - * 使用 Zustand selector 直接订阅 - */ -export function useIsInitialized(): UseIsInitializedReturn { - const isInitialized = useMessageStore(state => state.isInitialized); - - return { - isInitialized, + conversations, + isLoading, + refresh, + loadMore, + hasMore, + totalUnreadCount, + systemUnreadCount, + markAllAsRead, }; } diff --git a/src/stores/message/index.ts b/src/stores/message/index.ts index ba76428..a7b8fd5 100644 --- a/src/stores/message/index.ts +++ b/src/stores/message/index.ts @@ -72,7 +72,7 @@ export { WSMessageHandler, } from './services'; -// ==================== Hooks 导出 ==================== +// ==================== Hooks 导出(便捷版,使用 messageManager 单例) ==================== export { // 会话相关 useConversations, @@ -87,6 +87,7 @@ export { // 未读数相关 useUnreadCount, + useTotalUnreadCount, useSystemUnreadCount, // 群聊相关 @@ -95,9 +96,21 @@ export { // 通用 useMessageManager, - useConnectionStatus, - useIsInitialized, + useMessageListRefresh, + useChat, + useMessageList, } from './hooks'; +// ==================== 数据源导出 ==================== +export { + CONVERSATION_LIST_PAGE_SIZE, + type ConversationListPage, + type IConversationListPagedSource, + type RemoteConversationListSourceKind, + NetworkRemoteConversationListPagedSource, + SqliteConversationListPagedSource, + createRemoteConversationListSource, +} from './sources'; + // ==================== 默认导出 ==================== export { default } from './MessageManager'; diff --git a/src/stores/message/services/MessageSyncService.ts b/src/stores/message/services/MessageSyncService.ts index e5ed0f4..fc8891c 100644 --- a/src/stores/message/services/MessageSyncService.ts +++ b/src/stores/message/services/MessageSyncService.ts @@ -16,7 +16,7 @@ import { SqliteConversationListPagedSource, createRemoteConversationListSource, CONVERSATION_LIST_PAGE_SIZE, -} from '../../conversationListSources'; +} from '../sources'; import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types'; import { useMessageStore, normalizeConversationId, mergeMessagesById } from '../store'; import { ReadReceiptManager } from './ReadReceiptManager'; diff --git a/src/stores/conversationListSources.ts b/src/stores/message/sources.ts similarity index 98% rename from src/stores/conversationListSources.ts rename to src/stores/message/sources.ts index 83fb2b7..0683147 100644 --- a/src/stores/conversationListSources.ts +++ b/src/stores/message/sources.ts @@ -2,7 +2,7 @@ * 会话列表数据源抽象:游标、常规页码、SQLite 等实现同一契约,MessageManager 只依赖接口。 */ -import { ConversationResponse } from '../types/dto'; +import { ConversationResponse } from '../../types/dto'; import { messageService } from '@/services/message'; import { conversationRepository } from '@/database'; diff --git a/src/stores/message/types.ts b/src/stores/message/types.ts index 2744f26..62c0c53 100644 --- a/src/stores/message/types.ts +++ b/src/stores/message/types.ts @@ -1,5 +1,5 @@ import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto'; -import type { IConversationListPagedSource } from '../conversationListSources'; +import type { IConversationListPagedSource } from './sources'; export interface MessageManagerState { conversations: Map; diff --git a/src/stores/messageManager.ts b/src/stores/messageManager.ts deleted file mode 100644 index 6a963c0..0000000 --- a/src/stores/messageManager.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * MessageManager - 消息管理核心模块 - * - * ⚠️ 此文件已废弃,保留仅用于向后兼容 - * - * 新代码应该使用: - * import { messageManager } from '@/stores/message' - * import type { MessageEvent, MessageSubscriber } from '@/stores/message' - * - * 此文件将在未来版本中移除 - */ - -// 从新模块重导出所有内容 -export { - // 主管理器 - messageManager, - MessageManager, - - // 消息去重服务 - MessageDeduplication, - messageDeduplication, - - // 用户缓存服务 - UserCacheService, - userCacheService, - - // 已读回执管理器 - ReadReceiptManager, - - // 会话操作服务 - ConversationOperations, - - // 消息发送服务 - MessageSendService, - - // 消息同步服务 - MessageSyncService, - - // WebSocket 消息处理器 - WSMessageHandler, - - // 常量 - READ_STATE_PROTECTION_DELAY, - PROCESSED_MESSAGE_ID_EXPIRY, - PROCESSED_MESSAGE_ID_MAX_SIZE, - MAX_FLUSH_ITERATIONS, - CONVERSATION_REFRESH_THROTTLE, - MIN_RECONNECT_SYNC_INTERVAL, -} from './message'; - -// 重导出类型 -export type { - MessageManagerState, - ReadStateRecord, - MessageManagerConversationListDeps, - HandleNewMessageOptions, - IMessageDeduplication, - IUserCacheService, - IReadReceiptManager, - IConversationOperations, - IMessageSendService, - IMessageSyncService, - IWSMessageHandler, -} from './message'; - -// 默认导出 -export { default } from './message'; diff --git a/src/stores/messageManagerHooks.ts b/src/stores/messageManagerHooks.ts deleted file mode 100644 index 35fc326..0000000 --- a/src/stores/messageManagerHooks.ts +++ /dev/null @@ -1,586 +0,0 @@ -/** - * MessageManager React Hooks - * - * 提供React组件与MessageManager的集成 - * 所有hooks都基于Zustand selector机制 - * 确保组件能实时获取状态更新 - * - * 重构说明: - * - 移除了手动订阅机制,改用 Zustand selector - * - Zustand 会自动处理依赖追踪和组件重渲染 - * - 不需要手动管理订阅/取消订阅 - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useShallow } from 'zustand/react/shallow'; -import { ConversationResponse, MessageResponse, MessageSegment } from '../types/dto'; -import { messageManager } from './messageManager'; -import { useMessageStore } from './message'; - -// ==================== useConversations - 获取会话列表 ==================== - -interface UseConversationsReturn { - conversations: ConversationResponse[]; - isLoading: boolean; - refresh: () => Promise; - loadMore: () => Promise; - hasMore: boolean; -} - -/** - * 获取会话列表 - * 使用 Zustand selector 自动订阅状态变化 - */ -export function useConversations(): UseConversationsReturn { - // 使用 Zustand selector 直接订阅状态 - const conversations = useMessageStore(state => state.conversationList); - const isLoading = useMessageStore(state => state.isLoadingConversations); - const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations()); - - useEffect(() => { - // 初始化时确保 MessageManager 已初始化 - messageManager.initialize().catch(error => { - console.error('[useConversations] 初始化失败:', error); - }); - - // 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表 - const coldStartSyncTimer = setTimeout(() => { - messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { - console.error('[useConversations] 冷启动强制刷新失败:', error); - }); - }, 1200); - - return () => { - clearTimeout(coldStartSyncTimer); - }; - }, []); - - // 监听 hasMore 变化 - useEffect(() => { - setHasMore(messageManager.canLoadMoreConversations()); - }, [conversations]); - - const refresh = useCallback(async () => { - await messageManager.refreshConversations(true, 'hooks-manual-refresh'); - }, []); - - const loadMore = useCallback(async () => { - await messageManager.loadMoreConversations(); - }, []); - - return { - conversations, - isLoading, - refresh, - loadMore, - hasMore, - }; -} - -// ==================== useMessages - 获取指定会话的消息 ==================== - -interface UseMessagesReturn { - messages: MessageResponse[]; - isLoading: boolean; - hasMore: boolean; - loadMore: () => Promise; - refresh: () => Promise; -} - -/** - * 获取指定会话的消息 - * 使用 Zustand selector 自动订阅消息变化 - */ -export function useMessages(conversationId: string | null): UseMessagesReturn { - const normalizedConversationId = conversationId ? String(conversationId) : null; - - // 使用 useShallow 避免每次返回新数组引用导致的无限循环 - const messages = useMessageStore( - useShallow(state => - normalizedConversationId ? (state.messagesMap.get(normalizedConversationId) || []) : [] - ) - ); - const isLoadingMessages = useMessageStore(state => - normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false - ); - const [hasMore, setHasMore] = useState(true); - const loadMoreInFlightRef = useRef(false); - - useEffect(() => { - if (!normalizedConversationId) { - return; - } - - // 架构入口:激活会话并完成初始化/同步 - messageManager.activateConversation(normalizedConversationId).catch(error => { - console.error('[useMessages] 激活会话失败:', error); - }); - - return () => { - // 清理活动会话 - if (messageManager.getActiveConversation() === normalizedConversationId) { - messageManager.setActiveConversation(null); - } - }; - }, [normalizedConversationId]); - - const loadMore = useCallback(async () => { - if (!conversationId || !hasMore || loadMoreInFlightRef.current) return; - - const currentMessages = messageManager.getMessages(conversationId); - if (currentMessages.length === 0) return; - - // 消息数组在 MessageManager 内保持 seq 升序,首项即最早消息 - const minSeq = currentMessages[0]?.seq ?? Math.min(...currentMessages.map(m => m.seq)); - - loadMoreInFlightRef.current = true; - const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20); - loadMoreInFlightRef.current = false; - - // 如果没有加载到新消息,说明没有更多了 - if (loadedMessages.length === 0) { - setHasMore(false); - } - }, [conversationId, hasMore]); - - const refresh = useCallback(async () => { - if (!conversationId) return; - - await messageManager.fetchMessages(conversationId); - setHasMore(true); - }, [conversationId]); - - return { - messages, - isLoading: isLoadingMessages, - hasMore, - loadMore, - refresh, - }; -} - -// ==================== useSendMessage - 发送消息 ==================== - -interface UseSendMessageReturn { - sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise; - isSending: boolean; -} - -/** - * 发送消息 - */ -export function useSendMessage(conversationId: string | null): UseSendMessageReturn { - const [isSending, setIsSending] = useState(false); - - const sendMessage = useCallback( - async (segments: MessageSegment[], options?: { replyToId?: string }): Promise => { - if (!conversationId) return null; - - setIsSending(true); - try { - const message = await messageManager.sendMessage(conversationId, segments, options); - return message; - } finally { - setIsSending(false); - } - }, - [conversationId] - ); - - return { - sendMessage, - isSending, - }; -} - -// ==================== useMarkAsRead - 标记已读 ==================== - -interface UseMarkAsReadReturn { - markAsRead: (seq: number) => Promise; - markAllAsRead: () => Promise; - isMarking: boolean; -} - -/** - * 标记已读 - */ -export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn { - const [isMarking, setIsMarking] = useState(false); - - const markAsRead = useCallback( - async (seq: number) => { - if (!conversationId) return; - - setIsMarking(true); - try { - await messageManager.markAsRead(conversationId, seq); - } finally { - setIsMarking(false); - } - }, - [conversationId] - ); - - const markAllAsRead = useCallback(async () => { - setIsMarking(true); - try { - await messageManager.markAllAsRead(); - } finally { - setIsMarking(false); - } - }, []); - - return { - markAsRead, - markAllAsRead, - isMarking, - }; -} - -// ==================== useUnreadCount - 获取未读数 ==================== - -interface UseUnreadCountReturn { - totalUnreadCount: number; - systemUnreadCount: number; -} - -/** - * 获取未读消息数 - * 使用 Zustand selector 直接订阅未读数变化 - */ -export function useUnreadCount(): UseUnreadCountReturn { - // 使用 Zustand selector 直接订阅未读数 - const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); - const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - - useEffect(() => { - // 初始化时获取最新未读数 - messageManager.fetchUnreadCount(); - }, []); - - return { - totalUnreadCount, - systemUnreadCount, - }; -} - -// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ==================== - -/** - * 获取总未读消息数(会话未读 + 系统消息未读) - * 使用 Zustand selector 直接订阅 - */ -export function useTotalUnreadCount(): number { - const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); - const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); - - useEffect(() => { - // 初始化时获取最新未读数 - messageManager.fetchUnreadCount(); - }, []); - - return totalUnreadCount + systemUnreadCount; -} - -// ==================== useConversation - 获取单个会话信息 ==================== - -interface UseConversationReturn { - conversation: ConversationResponse | null; - isLoading: boolean; - refresh: () => Promise; -} - -/** - * 获取单个会话信息 - * 使用 Zustand selector 直接订阅会话变化 - */ -export function useConversation(conversationId: string | null): UseConversationReturn { - // 使用 useShallow 避免每次返回新对象引用导致的无限循环 - const conversation = useMessageStore( - useShallow(state => - conversationId ? (state.conversations.get(conversationId) || null) : null - ) - ); - const [isLoading, setIsLoading] = useState(false); - - const refresh = useCallback(async () => { - if (!conversationId) return; - - setIsLoading(true); - await messageManager.fetchConversationDetail(conversationId); - setIsLoading(false); - }, [conversationId]); - - return { - conversation, - isLoading, - refresh, - }; -} - -// ==================== useMessageManager - 通用状态 ==================== - -interface UseMessageManagerReturn { - isConnected: boolean; - isInitialized: boolean; -} - -/** - * 通用MessageManager状态 - * 使用 Zustand selector 直接订阅连接状态 - */ -export function useMessageManager(): UseMessageManagerReturn { - // 使用 Zustand selector 直接订阅状态 - const isConnected = useMessageStore(state => state.isWSConnected); - const isInitialized = useMessageStore(state => state.isInitialized); - - useEffect(() => { - // 初始化 - messageManager.initialize().catch(error => { - console.error('[useMessageManager] 初始化失败:', error); - }); - }, []); - - return { - isConnected, - isInitialized, - }; -} - -// ==================== useCreateConversation - 创建会话 ==================== - -interface UseCreateConversationReturn { - createConversation: (userId: string) => Promise; - isCreating: boolean; -} - -/** - * 创建私聊会话 - */ -export function useCreateConversation(): UseCreateConversationReturn { - const [isCreating, setIsCreating] = useState(false); - - const createConversation = useCallback(async (userId: string): Promise => { - setIsCreating(true); - try { - const conversation = await messageManager.createConversation(userId); - return conversation; - } finally { - setIsCreating(false); - } - }, []); - - return { - createConversation, - isCreating, - }; -} - -// ==================== useUpdateConversation - 更新会话 ==================== - -interface UseUpdateConversationReturn { - updateConversation: (conversationId: string, updates: Partial) => void; -} - -/** - * 本地更新会话信息 - */ -export function useUpdateConversation(): UseUpdateConversationReturn { - const updateConversation = useCallback((conversationId: string, updates: Partial) => { - messageManager.updateConversation(conversationId, updates); - }, []); - - return { - updateConversation, - }; -} - -// ==================== useSystemUnreadCount - 系统消息未读数操作 ==================== - -interface UseSystemUnreadCountReturn { - setSystemUnreadCount: (count: number) => void; - incrementSystemUnreadCount: () => void; - decrementSystemUnreadCount: (count?: number) => void; -} - -/** - * 系统消息未读数操作 - */ -export function useSystemUnreadCount(): UseSystemUnreadCountReturn { - const setSystemUnreadCount = useCallback((count: number) => { - messageManager.setSystemUnreadCount(count); - }, []); - - const incrementSystemUnreadCount = useCallback(() => { - messageManager.incrementSystemUnreadCount(); - }, []); - - const decrementSystemUnreadCount = useCallback((count?: number) => { - messageManager.decrementSystemUnreadCount(count); - }, []); - - return { - setSystemUnreadCount, - incrementSystemUnreadCount, - decrementSystemUnreadCount, - }; -} - -// ==================== useMessageListRefresh - MessageListScreen焦点刷新 ==================== - -interface UseMessageListRefreshReturn { - refresh: () => Promise; - isRefreshing: boolean; -} - -/** - * MessageListScreen焦点刷新Hook - */ -export function useMessageListRefresh(): UseMessageListRefreshReturn { - const [isRefreshing, setIsRefreshing] = useState(false); - const lastRefreshTimeRef = useRef(0); - - const refresh = useCallback(async () => { - // 防止重复刷新(最小间隔500ms) - const now = Date.now(); - if (now - lastRefreshTimeRef.current < 500) { - return; - } - lastRefreshTimeRef.current = now; - - setIsRefreshing(true); - try { - await messageManager.refreshConversations(true, 'hooks-load-more-fallback'); - } catch (error) { - console.error('[useMessageListRefresh] 刷新失败:', error); - } finally { - setIsRefreshing(false); - } - }, []); - - return { - refresh, - isRefreshing, - }; -} - -// ==================== 便捷hooks组合 ==================== - -/** - * ChatScreen专用Hook组合 - */ -interface UseChatReturn { - // 消息相关 - messages: MessageResponse[]; - isLoadingMessages: boolean; - hasMoreMessages: boolean; - loadMoreMessages: () => Promise; - refreshMessages: () => Promise; - - // 发送消息 - sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise; - isSending: boolean; - - // 已读相关 - markAsRead: (seq: number) => Promise; - - // 会话信息 - conversation: ConversationResponse | null; -} - -export function useChat(conversationId: string | null): UseChatReturn { - const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId); - const { sendMessage, isSending } = useSendMessage(conversationId); - const { markAsRead } = useMarkAsRead(conversationId); - const { conversation } = useConversation(conversationId); - - return { - messages, - isLoadingMessages, - hasMoreMessages, - loadMoreMessages: loadMore, - refreshMessages: refresh, - sendMessage, - isSending, - markAsRead, - conversation, - }; -} - -// ==================== useGroupTyping - 群聊输入状态 ==================== - -interface UseGroupTypingReturn { - typingUsers: string[]; -} - -/** - * 获取群聊输入状态 - * 使用 Zustand selector 直接订阅 - */ -export function useGroupTyping(groupId: string | null): UseGroupTypingReturn { - // 使用 useShallow 避免每次返回新数组引用导致的无限循环 - const typingUsers = useMessageStore( - useShallow(state => - groupId ? (state.typingUsersMap.get(groupId) || []) : [] - ) - ); - - return { typingUsers }; -} - -// ==================== useGroupMuted - 群聊禁言状态 ==================== - -interface UseGroupMutedReturn { - isMuted: boolean; - setMuted: (muted: boolean) => void; -} - -/** - * 获取群聊禁言状态 - * 使用 Zustand selector 直接订阅 - */ -export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn { - // 使用 Zustand selector 直接订阅禁言状态 - const isMuted = useMessageStore(state => - groupId ? (state.mutedStatusMap.get(groupId) || false) : false - ); - - const setMuted = useCallback((muted: boolean) => { - if (groupId) { - messageManager.setMutedStatus(groupId, muted); - } - }, [groupId]); - - return { isMuted, setMuted }; -} - -/** - * MessageListScreen专用Hook组合 - */ -interface UseMessageListReturn { - conversations: ConversationResponse[]; - isLoading: boolean; - refresh: () => Promise; - loadMore: () => Promise; - hasMore: boolean; - totalUnreadCount: number; - systemUnreadCount: number; - markAllAsRead: () => Promise; -} - -export function useMessageList(): UseMessageListReturn { - const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations(); - const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); - const { markAllAsRead } = useMarkAsRead(null); - - return { - conversations, - isLoading, - refresh, - loadMore, - hasMore, - totalUnreadCount, - systemUnreadCount, - markAllAsRead, - }; -} diff --git a/src/stores/post/PostManager.ts b/src/stores/post/PostManager.ts index 4b05d3e..9873e3d 100644 --- a/src/stores/post/PostManager.ts +++ b/src/stores/post/PostManager.ts @@ -16,7 +16,7 @@ import { createRemotePostListSource, RemotePostListSourceKind, POST_LIST_PAGE_SIZE, -} from '../postListSources'; +} from './sources'; function buildListKey(type: string, page: number, pageSize: number): string { return `list:${type}:${page}:${pageSize}`; diff --git a/src/stores/post/hooks.ts b/src/stores/post/hooks.ts index 4ce752e..cfe5570 100644 --- a/src/stores/post/hooks.ts +++ b/src/stores/post/hooks.ts @@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef } from 'react'; import { usePostManagerStore } from './postStore'; import { postManager } from './PostManager'; import type { Post } from '../../types'; -import { POST_LIST_PAGE_SIZE } from '../postListSources'; -import type { PostListTab } from '../postListSources'; +import { POST_LIST_PAGE_SIZE } from './sources'; +import type { PostListTab } from './sources'; export interface UsePostListResult { posts: Post[]; diff --git a/src/stores/post/index.ts b/src/stores/post/index.ts index c54f703..6e6dd4f 100644 --- a/src/stores/post/index.ts +++ b/src/stores/post/index.ts @@ -29,5 +29,17 @@ export { type UsePostDetailResult, } from './hooks'; +// ==================== 数据源 ==================== +export { + POST_LIST_PAGE_SIZE, + type PostListTab, + type PostListPage, + type IPostListPagedSource, + type RemotePostListSourceKind, + NetworkCursorPostListPagedSource, + NetworkOffsetPostListPagedSource, + createRemotePostListSource, +} from './sources'; + // ==================== 默认导出 ==================== export { default } from './PostManager'; \ No newline at end of file diff --git a/src/stores/postListSources.ts b/src/stores/post/sources.ts similarity index 97% rename from src/stores/postListSources.ts rename to src/stores/post/sources.ts index e0dc59f..34629ba 100644 --- a/src/stores/postListSources.ts +++ b/src/stores/post/sources.ts @@ -2,9 +2,9 @@ * 帖子列表数据源抽象:游标、偏移分页实现同一契约,PostManager 只依赖接口。 */ -import { Post } from '../types'; +import { Post } from '../../types'; import { postService } from '@/services/post'; -import type { CursorPaginationRequest } from '../types/dto'; +import type { CursorPaginationRequest } from '../../types/dto'; export const POST_LIST_PAGE_SIZE = 20; diff --git a/src/stores/postManager.ts b/src/stores/postManager.ts deleted file mode 100644 index e9067b4..0000000 --- a/src/stores/postManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @deprecated 此文件已废弃,请从 './post' 导入 - * 保留此文件仅为向后兼容 - * - * 迁移说明: - * - postManager 现在位于 ./post/PostManager.ts - * - 使用 Zustand 进行状态管理 - * - 推荐使用 hooks: usePostList, usePostDetail - */ - -export { - postManager, - PostManager, - usePostManagerStore, - usePostList, - usePostDetail, - type PostManagerState, - type PostManagerActions, - type PostManagerStore, -} from './post'; \ No newline at end of file diff --git a/src/stores/chatSettingsStore.ts b/src/stores/settings/chatSettingsStore.ts similarity index 100% rename from src/stores/chatSettingsStore.ts rename to src/stores/settings/chatSettingsStore.ts diff --git a/src/stores/settings/index.ts b/src/stores/settings/index.ts new file mode 100644 index 0000000..fb30719 --- /dev/null +++ b/src/stores/settings/index.ts @@ -0,0 +1,26 @@ +/** + * 设置模块统一导出 + */ + +export { + useChatSettingsStore, + useChatSettingsActions, + useChatFontSize, + useChatMessageRadius, + useChatTheme, + useChatNightMode, + CHAT_THEMES, +} from './chatSettingsStore'; +export type { ChatSettings, ChatThemeId } from './chatSettingsStore'; + +export { + useAppColors, + useThemePreference, + useResolvedColorScheme, + useSetThemePreference, + usePaperThemeFromStore, + ThemeBootstrap, + useThemeStore, + getSystemScheme, +} from './themeStore'; +export type { ThemePreference, ResolvedScheme } from './themeStore'; \ No newline at end of file diff --git a/src/stores/themeStore.ts b/src/stores/settings/themeStore.ts similarity index 96% rename from src/stores/themeStore.ts rename to src/stores/settings/themeStore.ts index 2064bae..5173b7c 100644 --- a/src/stores/themeStore.ts +++ b/src/stores/settings/themeStore.ts @@ -2,8 +2,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Appearance, AppState, AppStateStatus } from 'react-native'; import { create } from 'zustand'; import { useEffect, useRef } from 'react'; -import { lightColors, darkColors, type AppColors } from '../theme/palettes'; -import { buildPaperTheme } from '../theme/paperTheme'; +import { lightColors, darkColors, type AppColors } from '../../theme/palettes'; +import { buildPaperTheme } from '../../theme/paperTheme'; import type { MD3Theme } from 'react-native-paper'; const STORAGE_KEY = 'app_theme_preference'; diff --git a/src/stores/homeTabBarVisibilityStore.ts b/src/stores/ui/homeTabBarVisibilityStore.ts similarity index 100% rename from src/stores/homeTabBarVisibilityStore.ts rename to src/stores/ui/homeTabBarVisibilityStore.ts diff --git a/src/stores/homeTabPressStore.ts b/src/stores/ui/homeTabPressStore.ts similarity index 100% rename from src/stores/homeTabPressStore.ts rename to src/stores/ui/homeTabPressStore.ts diff --git a/src/stores/ui/index.ts b/src/stores/ui/index.ts new file mode 100644 index 0000000..e7df0e4 --- /dev/null +++ b/src/stores/ui/index.ts @@ -0,0 +1,6 @@ +/** + * UI 状态模块统一导出 + */ + +export { useHomeTabBarVisibilityStore } from './homeTabBarVisibilityStore'; +export { useHomeTabPressStore } from './homeTabPressStore'; \ No newline at end of file diff --git a/src/stores/user/hooks.ts b/src/stores/user/hooks.ts index 2078fcd..4065ea5 100644 --- a/src/stores/user/hooks.ts +++ b/src/stores/user/hooks.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { useUserManagerStore } from './userStore'; import { userManager } from './UserManager'; import type { UserDTO } from '../../types/dto'; -import { useAuthStore } from '../authStore'; +import { useAuthStore } from '../auth/authStore'; export interface UseCurrentUserResult { user: UserDTO | null; diff --git a/src/stores/userManager.ts b/src/stores/userManager.ts deleted file mode 100644 index 724a384..0000000 --- a/src/stores/userManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @deprecated 此文件已废弃,请从 './user' 导入 - * 保留此文件仅为向后兼容 - * - * 迁移说明: - * - userManager 现在位于 ./user/UserManager.ts - * - 使用 Zustand 进行状态管理 - * - 推荐使用 hooks: useCurrentUser, useUser, useUsers - */ - -export { - userManager, - UserManager, - useUserManagerStore, - useCurrentUser, - useUser, - useUsers, - type UserManagerState, - type UserManagerActions, - type UserManagerStore, -} from './user'; \ No newline at end of file diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index 8e220b8..32d7402 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -12,8 +12,8 @@ import { postService, notificationService, } from '../services'; -import { userManager } from './userManager'; -import { messageManager } from './messageManager'; +import { userManager } from './user'; +import { messageManager } from './message'; interface UserState { // 状态 diff --git a/src/stores/routePayloadCache.ts b/src/stores/utils/routePayloadCache.ts similarity index 90% rename from src/stores/routePayloadCache.ts rename to src/stores/utils/routePayloadCache.ts index 7767358..fdaf379 100644 --- a/src/stores/routePayloadCache.ts +++ b/src/stores/utils/routePayloadCache.ts @@ -2,8 +2,8 @@ * 不可放入 URL 的路由载荷(系统消息对象、课表课程对象等)的短时缓存。 * 深链仅带 id;从通知列表等入口跳转前先 stash,目标屏按 id 读取。 */ -import type { SystemMessageResponse } from '../types/dto'; -import type { Course } from '../types/schedule'; +import type { SystemMessageResponse } from '../../types/dto'; +import type { Course } from '../../types/schedule'; const systemById = new Map(); const courseById = new Map(); diff --git a/src/theme/index.ts b/src/theme/index.ts index 758eda3..5ee08b0 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -17,7 +17,7 @@ export { useThemeStore, type ThemePreference, type ResolvedScheme, -} from '../stores/themeStore'; +} from '../stores/settings'; import { lightColors } from './palettes'; import { buildPaperTheme } from './paperTheme';