refactor(stores): reorganize flat store files into modular directory structure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m29s
Frontend CI / ota-android (push) Successful in 10m35s
Frontend CI / build-android-apk (push) Successful in 37m48s

Migrate from flat file organization to modular directory structure under src/stores/:

- auth/: authStore, registerStore, verificationStore, sessionStore
- call/: callStore
- settings/: chatSettingsStore, themeStore
- ui/: homeTabBarVisibilityStore, homeTabPressStore
- utils/: routePayloadCache
- group/sources.ts, group/profileResolver.ts
- message/sources.ts
- post/sources.ts

Update all import paths across components and screens to use new module paths. Maintain backward compatibility through deprecated re-export files for gradual migration.
This commit is contained in:
lafay
2026-04-13 04:56:58 +08:00
parent 4f31926eb5
commit 57d7c7405c
69 changed files with 1163 additions and 1219 deletions

View File

@@ -8,7 +8,7 @@ import {
StatusBar, StatusBar,
} from 'react-native'; } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore'; import { callStore } from '../../stores/call';
import { RTCView } from 'react-native-webrtc'; import { RTCView } from 'react-native-webrtc';
const formatDuration = (seconds: number): string => { const formatDuration = (seconds: number): string => {

View File

@@ -7,7 +7,7 @@ import {
StatusBar, StatusBar,
} from 'react-native'; } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore'; import { callStore } from '../../stores/call';
const formatDuration = (seconds: number): string => { const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);

View File

@@ -7,7 +7,7 @@ import {
Image, Image,
} from 'react-native'; } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore'; import { callStore } from '../../stores/call';
const formatDuration = (seconds: number): string => { const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);

View File

@@ -12,7 +12,7 @@ import {
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore'; import { callStore } from '../../stores/call';
import { blurActiveElement } from '../../infrastructure/platform'; import { blurActiveElement } from '../../infrastructure/platform';
const { height: SCREEN_HEIGHT } = Dimensions.get('window'); const { height: SCREEN_HEIGHT } = Dimensions.get('window');

View File

@@ -10,11 +10,11 @@
*/ */
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { postManager } from '../stores/postManager'; import { postManager } from '../stores/post';
import { groupManager } from '../stores/groupManager'; import { groupManager } from '../stores/group';
import { userManager } from '../stores/userManager'; import { userManager } from '../stores/user';
import { messageManager } from '../stores/messageManager'; import { messageManager } from '../stores/message';
import type { PostListTab } from '../stores/postListSources'; import type { PostListTab } from '../stores/post';
// ==================== 预取配置 ==================== // ==================== 预取配置 ====================

View File

@@ -2,7 +2,7 @@
* Expo Router 路径构建(单一事实来源,避免魔法字符串散落) * Expo Router 路径构建(单一事实来源,避免魔法字符串散落)
*/ */
import type { SystemMessageResponse } from '../types/dto'; import type { SystemMessageResponse } from '../types/dto';
import { routePayloadCache } from '../stores/routePayloadCache'; import { routePayloadCache } from '../stores';
export function hrefPostDetail(postId: string, scrollToComments?: boolean): string { export function hrefPostDetail(postId: string, scrollToComments?: boolean): string {
const q = scrollToComments ? '?scrollToComments=1' : ''; const q = scrollToComments ? '?scrollToComments=1' : '';

View File

@@ -33,7 +33,7 @@ import {
useRegisterStep, useRegisterStep,
useRegisterFormData, useRegisterFormData,
useRegisterCountdown, useRegisterCountdown,
} from '../../stores/registerStore'; } from '../../stores/auth';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { showPrompt } from '@/services/ui'; import { showPrompt } from '@/services/ui';
import { RegisterStepIndicator } from '../../components/common'; import { RegisterStepIndicator } from '../../components/common';

View File

@@ -21,7 +21,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { useAppColors, type AppColors } from '../../theme'; import { useAppColors, type AppColors } from '../../theme';
import { useVerificationStore } from '../../stores/verificationStore'; import { useVerificationStore } from '../../stores/auth';
import { showPrompt } from '@/services/ui'; import { showPrompt } from '@/services/ui';
import { uploadService } from '@/services/upload'; import { uploadService } from '@/services/upload';
import type { UserIdentity } from '../../types/dto'; import type { UserIdentity } from '../../types/dto';

View File

@@ -27,7 +27,7 @@ import { PagerView } from '../../components/common';
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme'; import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
import { Post } from '../../types'; import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore'; import { useCurrentUser } from '../../stores/auth';
import { channelService, postService } from '../../services'; import { channelService, postService } from '../../services';
import { PostCard, TabBar, SearchBar } from '../../components/business'; import { PostCard, TabBar, SearchBar } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard'; import type { PostCardAction } from '../../components/business/PostCard';
@@ -466,9 +466,9 @@ export const HomeScreen: React.FC = () => {
// 宽屏下内容最大宽度 // 宽屏下内容最大宽度
const contentMaxWidth = useMemo(() => { const contentMaxWidth = useMemo(() => {
if (isWideScreen) return 800; if (isWideScreen) return 1200;
if (isDesktop) return 720; if (isDesktop) return 1000;
if (isTablet) return 640; if (isTablet) return 800;
return width; // 移动端使用全宽 return width; // 移动端使用全宽
}, [width, isTablet, isDesktop, isWideScreen]); }, [width, isTablet, isDesktop, isWideScreen]);

View File

@@ -35,7 +35,7 @@ import {
} from '../../theme'; } from '../../theme';
import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types'; import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types';
import { useUserStore } from '../../stores'; import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore'; import { useCurrentUser } from '../../stores/auth';
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services'; import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
import { postSyncService } from '@/services/post'; import { postSyncService } from '@/services/post';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';

View File

@@ -49,8 +49,8 @@ import { User } from '../../types';
import { blurActiveElement } from '../../infrastructure/platform'; import { blurActiveElement } from '../../infrastructure/platform';
import { firstRouteParam } from '../../navigation/paramUtils'; import { firstRouteParam } from '../../navigation/paramUtils';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores/messageManager'; import { messageManager } from '../../stores/message';
import { groupManager } from '../../stores/groupManager'; import { groupManager } from '../../stores/group';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal'; import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
import { AppBackButton } from '../../components/common'; import { AppBackButton } from '../../components/common';

View File

@@ -5,9 +5,9 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Avatar, Text } from '../../components/common'; import { Avatar, Text } from '../../components/common';
import { routePayloadCache } from '../../stores/routePayloadCache'; import { routePayloadCache } from '../../stores';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { groupManager } from '../../stores/groupManager'; import { groupManager } from '../../stores/group';
import { GroupMemberResponse } from '../../types/dto'; import { GroupMemberResponse } from '../../types/dto';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';

View File

@@ -27,11 +27,11 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { import {
createRemoteGroupMemberListSource, createCursorGroupMemberListSource,
GroupMemberListSourceKind, GroupMemberListSourceKind,
IGroupMemberListPagedSource, ICursorGroupMemberListPagedSource,
} from '../../stores/groupMemberListSources'; } from '../../stores/group';
import { enrichGroupMembersWithUserProfiles } from '../../stores/groupMemberProfileResolver'; import { enrichGroupMembersWithUserProfiles } from '../../stores/group';
import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer, AppBackButton } from '../../components/common'; import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer, AppBackButton } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';
@@ -75,8 +75,8 @@ const GroupMembersScreen: React.FC = () => {
return GRID_CONFIG.mobile; return GRID_CONFIG.mobile;
}, [width]); }, [width]);
const memberListSource = useMemo<IGroupMemberListPagedSource>(() => { const memberListSource = useMemo<ICursorGroupMemberListPagedSource>(() => {
return createRemoteGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); return createCursorGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50);
}, [groupId]); }, [groupId]);
// 使用统一数据源 + 游标 Hook 管理成员列表 // 使用统一数据源 + 游标 Hook 管理成员列表

View File

@@ -7,10 +7,10 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { Avatar, Text } from '../../components/common'; import { Avatar, Text } from '../../components/common';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { routePayloadCache } from '../../stores/routePayloadCache'; import { routePayloadCache } from '../../stores';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { groupManager } from '../../stores/groupManager'; import { groupManager } from '../../stores/group';
import { userManager } from '../../stores/userManager'; import { userManager } from '../../stores/user';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared'; import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
const GroupRequestDetailScreen: React.FC = () => { const GroupRequestDetailScreen: React.FC = () => {

View File

@@ -17,7 +17,7 @@ import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '
import Avatar from '../../components/common/Avatar'; import Avatar from '../../components/common/Avatar';
import Text from '../../components/common/Text'; import Text from '../../components/common/Text';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { groupManager } from '../../stores/groupManager'; import { groupManager } from '../../stores/group';
import { GroupResponse, JoinType } from '../../types/dto'; import { GroupResponse, JoinType } from '../../types/dto';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';
import { EmptyState } from '../../components/common'; import { EmptyState } from '../../components/common';

View File

@@ -37,7 +37,7 @@ import { Text, ResponsiveContainer } from '../../components/common';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { useMessageManagerSystemUnreadCount } from '../../stores'; import { useMessageManagerSystemUnreadCount } from '../../stores';
import { messageManager } from '../../stores/messageManager'; import { messageManager } from '../../stores/message';
const MESSAGE_TYPES = [ const MESSAGE_TYPES = [
{ key: 'all', title: '全部' }, { key: 'all', title: '全部' },

View File

@@ -22,8 +22,8 @@ import { authService } from '@/services/auth';
import { messageService } from '@/services/message'; import { messageService } from '@/services/message';
import { ApiError } from '@/services/core'; import { ApiError } from '@/services/core';
import { messageRepository, conversationRepository } from '@/database'; import { messageRepository, conversationRepository } from '@/database';
import { messageManager } from '../../stores/messageManager'; import { messageManager } from '../../stores/message';
import { userManager } from '../../stores/userManager'; import { userManager } from '../../stores/user';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common'; import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { User } from '../../types'; import { User } from '../../types';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';

View File

@@ -20,7 +20,7 @@ import { Avatar, Text } from '../../../../components/common';
import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme'; import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme';
import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto';
import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { useBreakpointGTE } from '../../../../hooks/useResponsive';
import { groupManager } from '../../../../stores/groupManager'; import { groupManager } from '../../../../stores/group';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth } = Dimensions.get('window');

View File

@@ -20,7 +20,7 @@ import { Avatar, Text } from '../../../../components/common';
import { useAppColors, spacing, fontSizes, shadows, borderRadius, type AppColors } from '../../../../theme'; import { useAppColors, spacing, fontSizes, shadows, borderRadius, type AppColors } from '../../../../theme';
import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto';
import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { useBreakpointGTE } from '../../../../hooks/useResponsive';
import { groupManager } from '../../../../stores/groupManager'; import { groupManager } from '../../../../stores/group';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth } = Dimensions.get('window');

View File

@@ -33,7 +33,7 @@ import {
} from '../../../../types/dto'; } from '../../../../types/dto';
import { spacing, useAppColors, type AppColors } from '../../../../theme'; import { spacing, useAppColors, type AppColors } from '../../../../theme';
import { SenderInfo } from './types'; import { SenderInfo } from './types';
import { useChatSettingsStore } from '../../../../stores/chatSettingsStore'; import { useChatSettingsStore } from '../../../../stores/settings';
const IMAGE_MAX_WIDTH = 200; const IMAGE_MAX_WIDTH = 200;
const IMAGE_MAX_HEIGHT = 260; const IMAGE_MAX_HEIGHT = 260;

View File

@@ -5,7 +5,7 @@
import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
import { spacing, type AppColors } from '../../../../theme'; import { spacing, type AppColors } from '../../../../theme';
import { useChatSettingsStore } from '../../../../stores/chatSettingsStore'; import { useChatSettingsStore } from '../../../../stores/settings';
// 默认圆角,但会从 store 读取 // 默认圆角,但会从 store 读取
export const BUBBLE_RADIUS = 16; export const BUBBLE_RADIUS = 16;

View File

@@ -6,7 +6,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { StyleSheet, Dimensions } from 'react-native'; import { StyleSheet, Dimensions } from 'react-native';
import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme'; 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'); const { width: SCREEN_WIDTH } = Dimensions.get('window');

View File

@@ -28,8 +28,8 @@ import { ApiError } from '@/services/core';
// 【新架构】使用 MessageManager // 【新架构】使用 MessageManager
import { useChat, useGroupTyping, useGroupMuted, messageManager, callStore } from '../../../../stores'; import { useChat, useGroupTyping, useGroupMuted, messageManager, callStore } from '../../../../stores';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { userManager } from '../../../../stores/userManager'; import { userManager } from '../../../../stores/user';
import { groupManager } from '../../../../stores/groupManager'; import { groupManager } from '../../../../stores/group';
import * as hrefs from '../../../../navigation/hrefs'; import * as hrefs from '../../../../navigation/hrefs';
import { firstRouteParam } from '../../../../navigation/paramUtils'; import { firstRouteParam } from '../../../../navigation/paramUtils';
import { import {

View File

@@ -26,7 +26,7 @@ import {
useChatSettingsStore, useChatSettingsStore,
useChatSettingsActions, useChatSettingsActions,
CHAT_THEMES, CHAT_THEMES,
} from '../../stores/chatSettingsStore'; } from '../../stores/settings';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CARD_MAX_WIDTH = 720; const CARD_MAX_WIDTH = 720;

View File

@@ -7,7 +7,7 @@
import React from 'react'; import React from 'react';
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams } from 'expo-router';
import UserProfileScreen from './UserProfileScreen'; import UserProfileScreen from './UserProfileScreen';
import { useCurrentUser } from '../../stores/authStore'; import { useCurrentUser } from '../../stores/auth';
export const UserScreen: React.FC = () => { export const UserScreen: React.FC = () => {
const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>(); const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>();

View File

@@ -22,7 +22,7 @@ import {
useVerificationStore, useVerificationStore,
useVerificationStatus, useVerificationStatus,
useVerificationLoading, useVerificationLoading,
} from '../../stores/verificationStore'; } from '../../stores/auth';
import { showPrompt } from '@/services/ui'; import { showPrompt } from '@/services/ui';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';

View File

@@ -10,10 +10,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { spacing, type AppColors } from '../../theme'; import { spacing, type AppColors } from '../../theme';
import { Post, User } from '../../types'; import { Post, User } from '../../types';
import { useUserStore } from '../../stores'; import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore'; import { useCurrentUser } from '../../stores/auth';
import { postService, authService, messageService } from '../../services'; import { postService, authService, messageService } from '../../services';
import { postSyncService } from '@/services/post'; import { postSyncService } from '@/services/post';
import { userManager } from '../../stores/userManager'; import { userManager } from '../../stores/user';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { PostCardAction } from '../../components/business/PostCard'; import { PostCardAction } from '../../components/business/PostCard';

View File

@@ -5,7 +5,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, fontSizes, shadows, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, shadows, useAppColors, type AppColors } from '../../theme';
import { routePayloadCache } from '../../stores/routePayloadCache'; import { routePayloadCache } from '../../stores';
import { scheduleService } from '@/services/platform'; import { scheduleService } from '@/services/platform';
const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];

View File

@@ -42,7 +42,7 @@ import {
hasCourseInWeek, hasCourseInWeek,
} from '../../types/schedule'; } from '../../types/schedule';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { routePayloadCache } from '../../stores/routePayloadCache'; import { routePayloadCache } from '../../stores';
import { scheduleService } from '@/services/platform'; import { scheduleService } from '@/services/platform';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');

View File

@@ -12,10 +12,10 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand'; import { create } from 'zustand';
import { User } from '../types'; import { User } from '../../types';
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services'; import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services';
import { wsService } from '@/services/core'; import { wsService } from '@/services/core';
import { callStore } from './callStore'; import { callStore } from '../call/callStore';
import { useSessionStore } from './sessionStore'; import { useSessionStore } from './sessionStore';
import { import {
switchDatabase, switchDatabase,
@@ -233,7 +233,7 @@ export const useAuthStore = create<AuthState>((set) => ({
// 7. 清除 sessionStore 的 userId // 7. 清除 sessionStore 的 userId
useSessionStore.getState().setUserId(null); useSessionStore.getState().setUserId(null);
// 8. 清除 userManager 的内存缓存 // 8. 清除 userManager 的内存缓存
const { userManager } = await import('./userManager'); const { userManager } = await import('../user');
userManager.invalidateUsers(); userManager.invalidateUsers();
} catch (error) { } catch (error) {
console.error('[AuthStore] 登出失败:', error); console.error('[AuthStore] 登出失败:', error);

10
src/stores/auth/index.ts Normal file
View File

@@ -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';

View File

@@ -11,7 +11,7 @@ import {
SubmitVerificationRequest, SubmitVerificationRequest,
UserIdentity, UserIdentity,
VerificationStatus, VerificationStatus,
} from '../types/dto'; } from '../../types/dto';
interface VerificationState { interface VerificationState {
// 状态 // 状态

View File

@@ -8,9 +8,9 @@ import {
WSErrorMessage, WSErrorMessage,
} from '@/services/core'; } from '@/services/core';
import { webrtcManager, ICEServer } from '@/services/webrtc'; import { webrtcManager, ICEServer } from '@/services/webrtc';
import { getCurrentUserId } from './sessionStore'; import { getCurrentUserId } from '../auth/sessionStore';
import { useUserStore } from './userStore'; import { useUserStore } from '../userStore';
import { userManager } from './userManager'; import { userManager } from '../user';
export type CallStatus = export type CallStatus =
| 'idle' // 空闲状态 | 'idle' // 空闲状态

6
src/stores/call/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* 通话模块统一导出
*/
export { callStore } from './callStore';
export type { CallType, CallSession, CallStatus, IncomingCallInfo } from './callStore';

View File

@@ -17,7 +17,7 @@ import {
RemoteGroupListSourceKind, RemoteGroupListSourceKind,
GROUP_LIST_PAGE_SIZE, GROUP_LIST_PAGE_SIZE,
GROUP_MEMBER_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE,
} from '../groupListSources'; } from './sources';
const dedupe = createDedupe(() => useGroupManagerStore); const dedupe = createDedupe(() => useGroupManagerStore);

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
import { useGroupManagerStore } from './groupStore'; import { useGroupManagerStore } from './groupStore';
import { groupManager } from './GroupManager'; import { groupManager } from './GroupManager';
import type { GroupResponse, GroupMemberResponse } from '../../types/dto'; 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 { export interface UseGroupsResult {
groups: GroupResponse[]; groups: GroupResponse[];

View File

@@ -23,5 +23,32 @@ export {
type UseGroupMembersResult, type UseGroupMembersResult,
} from './hooks'; } 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'; export { default } from './GroupManager';

View File

@@ -1,6 +1,6 @@
import { GroupMemberResponse, UserDTO } from '../types/dto'; import { GroupMemberResponse, UserDTO } from '../../types/dto';
import { userCacheRepository } from '@/database'; import { userCacheRepository } from '@/database';
import { userManager } from './userManager'; import { userManager } from '../user';
/** /**
* *

View File

@@ -1,5 +1,5 @@
/** /**
* GroupManager * + /
*/ */
import { import {
@@ -8,7 +8,8 @@ import {
GroupListResponse, GroupListResponse,
GroupMemberListResponse, GroupMemberListResponse,
CursorPaginationRequest, CursorPaginationRequest,
} from '../types/dto'; CursorPaginationResponse,
} from '../../types/dto';
import { groupService } from '@/services/message'; import { groupService } from '@/services/message';
import { groupCacheRepository } from '@/database'; import { groupCacheRepository } from '@/database';
@@ -17,26 +18,17 @@ export const GROUP_MEMBER_LIST_PAGE_SIZE = 50;
// ==================== 群组列表数据源 ==================== // ==================== 群组列表数据源 ====================
/** 单次拉取结果(一页或一批) */
export interface GroupListPage { export interface GroupListPage {
items: GroupResponse[]; items: GroupResponse[];
/** 在当前源上是否还能再 loadNext 一页 */
hasMore: boolean; hasMore: boolean;
} }
/**
* restart loadNext
*/
export interface IGroupListPagedSource { export interface IGroupListPagedSource {
restart(): void; restart(): void;
loadNext(): Promise<GroupListPage>; loadNext(): Promise<GroupListPage>;
/** 至少成功 loadNext 过一次且仍有下一页时为 true */
readonly hasMore: boolean; readonly hasMore: boolean;
} }
/**
*
*/
export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource { export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource {
private nextCursor: string | null = null; private nextCursor: string | null = null;
private hasMoreAfterLastLoad = false; private hasMoreAfterLastLoad = false;
@@ -73,9 +65,6 @@ export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource
} }
} }
/**
*
*/
export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource { export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource {
private nextPage = 1; private nextPage = 1;
private hasMoreAfterLastLoad = false; private hasMoreAfterLastLoad = false;
@@ -110,9 +99,6 @@ export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource
} }
} }
/**
* SQLite
*/
export class SqliteGroupListPagedSource implements IGroupListPagedSource { export class SqliteGroupListPagedSource implements IGroupListPagedSource {
private consumed = false; private consumed = false;
@@ -139,26 +125,19 @@ export class SqliteGroupListPagedSource implements IGroupListPagedSource {
} }
} }
// ==================== 群组成员列表数据源 ==================== // ==================== 群组成员列表数据源Manager 内部用) ====================
/** 成员列表单次拉取结果 */
export interface GroupMemberListPage { export interface GroupMemberListPage {
items: GroupMemberResponse[]; items: GroupMemberResponse[];
hasMore: boolean; hasMore: boolean;
} }
/**
*
*/
export interface IGroupMemberListPagedSource { export interface IGroupMemberListPagedSource {
restart(): void; restart(): void;
loadNext(): Promise<GroupMemberListPage>; loadNext(): Promise<GroupMemberListPage>;
readonly hasMore: boolean; readonly hasMore: boolean;
} }
/**
*
*/
export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource { export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource {
private nextCursor: string | null = null; private nextCursor: string | null = null;
private hasMoreAfterLastLoad = false; private hasMoreAfterLastLoad = false;
@@ -197,9 +176,6 @@ export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberList
} }
} }
/**
*
*/
export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource { export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource {
private nextPage = 1; private nextPage = 1;
private hasMoreAfterLastLoad = false; private hasMoreAfterLastLoad = false;
@@ -240,9 +216,6 @@ export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberList
} }
} }
/**
* SQLite
*/
export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSource { export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSource {
private consumed = false; private consumed = false;
private readonly groupId: string; 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<CursorPaginationResponse<GroupMemberResponse>>;
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<CursorPaginationResponse<GroupMemberResponse>> {
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<CursorPaginationResponse<GroupMemberResponse>> {
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 type RemoteGroupListSourceKind = 'cursor' | 'offset';
export function createRemoteGroupListSource( export function createRemoteGroupListSource(

View File

@@ -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';

View File

@@ -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<CursorPaginationResponse<GroupMemberResponse>>;
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<CursorPaginationResponse<GroupMemberResponse>> {
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<CursorPaginationResponse<GroupMemberResponse>> {
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);
}

View File

@@ -2,9 +2,14 @@
* 状态管理 Store 导出 * 状态管理 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 { export {
useChatSettingsStore, useChatSettingsStore,
useChatSettingsActions, useChatSettingsActions,
@@ -13,92 +18,8 @@ export {
useChatTheme, useChatTheme,
useChatNightMode, useChatNightMode,
CHAT_THEMES, CHAT_THEMES,
} from './chatSettingsStore'; } from './settings';
export type { ChatSettings, ChatThemeId } from './chatSettingsStore'; export type { ChatSettings, ChatThemeId } from './settings';
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';
export { export {
useAppColors, useAppColors,
useThemePreference, useThemePreference,
@@ -107,9 +28,40 @@ export {
usePaperThemeFromStore, usePaperThemeFromStore,
ThemeBootstrap, ThemeBootstrap,
useThemeStore, useThemeStore,
type ThemePreference, getSystemScheme,
type ResolvedScheme, } from './settings';
} from './themeStore'; 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 { export {
useConversations as useMessageManagerConversations, useConversations as useMessageManagerConversations,
useMessages, useMessages,
@@ -127,10 +79,61 @@ export {
useSystemUnreadCount as useMessageManagerSystemUnreadCount, useSystemUnreadCount as useMessageManagerSystemUnreadCount,
useGroupTyping, useGroupTyping,
useGroupMuted, 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 { export {
CacheEntry, CacheEntry,
createCacheEntry, createCacheEntry,
@@ -138,4 +141,5 @@ export {
isCacheValid, isCacheValid,
getCacheRemainingTTL, getCacheRemainingTTL,
DEFAULT_TTL, DEFAULT_TTL,
} from './utils/cache'; } from './utils/cache';
export { routePayloadCache } from './utils/routePayloadCache';

View File

@@ -12,7 +12,7 @@
*/ */
import type { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../../types/dto'; import type { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../../types/dto';
import { useAuthStore } from '../authStore'; import { useAuthStore } from '../auth/authStore';
import type { MessageManagerConversationListDeps } from './types'; import type { MessageManagerConversationListDeps } from './types';
// 导入服务 // 导入服务

View File

@@ -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<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
}
/**
* 获取会话列表
* 用于 MessageListScreen 等需要显示会话列表的组件
* 使用 Zustand selector 自动订阅状态变化
*/
export function useConversations(
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>,
loadMoreConversations: () => Promise<void>,
canLoadMore: () => boolean,
initialize: () => Promise<void>
): 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<void>;
refresh: () => Promise<void>;
}
/**
* 获取指定会话的消息
* 使用 Zustand selector 自动订阅消息变化
*/
export function useMessages(
conversationId: string,
fetchMessages: (conversationId: string) => Promise<void>,
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
): 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<MessageResponse | null>;
isSending: boolean;
}
/**
* 发送消息
*/
export function useSendMessage(
conversationId: string,
sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | 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]);
return {
sendMessage,
isSending,
};
}
// ==================== useMarkAsRead - 标记已读 ====================
interface UseMarkAsReadReturn {
markAsRead: (seq: number) => Promise<void>;
markAllAsRead: () => Promise<void>;
}
/**
* 标记已读
*/
export function useMarkAsRead(
conversationId: string,
markAsReadService: (conversationId: string, seq: number) => Promise<void>,
markAllAsReadService: () => Promise<void>
): 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<void>;
}
/**
* 获取未读数
* 使用 Zustand selector 直接订阅未读数变化
*/
export function useUnreadCount(fetchUnreadCount: () => Promise<void>): 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<void>;
}
/**
* 获取单个会话
* 使用 Zustand selector 直接订阅会话变化
*/
export function useConversation(
conversationId: string,
fetchConversationDetail: (conversationId: string) => Promise<ConversationResponse | null>
): 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<void>): 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<ConversationResponse | null>;
isCreating: boolean;
}
/**
* 创建会话
*/
export function useCreateConversation(
createConversationService: (userId: string) => Promise<ConversationResponse | null>
): 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<ConversationResponse>) => void;
}
/**
* 更新会话
*/
export function useUpdateConversation(
updateConversationService: (conversationId: string, updates: Partial<ConversationResponse>) => void
): UseUpdateConversationReturn {
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
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,
};
}

View File

@@ -1,12 +1,12 @@
/** /**
* 消息模块 React Hooks * MessageManager React Hooks
* *
* 提供React组件与消息状态管理的集成 * 提供React组件与MessageManager的集成
* 所有hooks都基于zustand store的selector机制 * 所有hooks都基于Zustand selector机制
* 确保组件能实时获取状态更新 * 确保组件能实时获取状态更新
* *
* 重构说明: * 重构说明:
* - 移除了 SubscriptionManager,改用 Zustand selector * - 移除了手动订阅机制,改用 Zustand selector
* - Zustand 会自动处理依赖追踪和组件重渲染 * - Zustand 会自动处理依赖追踪和组件重渲染
* - 不需要手动管理订阅/取消订阅 * - 不需要手动管理订阅/取消订阅
*/ */
@@ -14,7 +14,8 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
import { useMessageStore, normalizeConversationId } from './store'; import { messageManager } from './MessageManager';
import { useMessageStore } from './store';
// ==================== useConversations - 获取会话列表 ==================== // ==================== useConversations - 获取会话列表 ====================
@@ -28,29 +29,23 @@ interface UseConversationsReturn {
/** /**
* 获取会话列表 * 获取会话列表
* 用于 MessageListScreen 等需要显示会话列表的组件
* 使用 Zustand selector 自动订阅状态变化 * 使用 Zustand selector 自动订阅状态变化
*/ */
export function useConversations( export function useConversations(): UseConversationsReturn {
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>,
loadMoreConversations: () => Promise<void>,
canLoadMore: () => boolean,
initialize: () => Promise<void>
): UseConversationsReturn {
// 使用 Zustand selector 直接订阅状态 // 使用 Zustand selector 直接订阅状态
const conversations = useMessageStore(state => state.conversationList); const conversations = useMessageStore(state => state.conversationList);
const isLoading = useMessageStore(state => state.isLoadingConversations); const isLoading = useMessageStore(state => state.isLoadingConversations);
const [hasMore, setHasMore] = useState(() => canLoadMore()); const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations());
useEffect(() => { useEffect(() => {
// 初始化 // 初始化时确保 MessageManager 已初始化
initialize().catch(error => { messageManager.initialize().catch(error => {
console.error('[useConversations] 初始化失败:', error); console.error('[useConversations] 初始化失败:', error);
}); });
// 冷启动兜底:延迟做一次强制刷新 // 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
const coldStartSyncTimer = setTimeout(() => { const coldStartSyncTimer = setTimeout(() => {
fetchConversations(true, 'hooks-initial-refresh').catch(error => { messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
console.error('[useConversations] 冷启动强制刷新失败:', error); console.error('[useConversations] 冷启动强制刷新失败:', error);
}); });
}, 1200); }, 1200);
@@ -62,16 +57,16 @@ export function useConversations(
// 监听 hasMore 变化 // 监听 hasMore 变化
useEffect(() => { useEffect(() => {
setHasMore(canLoadMore()); setHasMore(messageManager.canLoadMoreConversations());
}, [conversations, canLoadMore]); }, [conversations]);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
await fetchConversations(true, 'hooks-manual-refresh'); await messageManager.refreshConversations(true, 'hooks-manual-refresh');
}, [fetchConversations]); }, []);
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
await loadMoreConversations(); await messageManager.loadMoreConversations();
}, [loadMoreConversations]); }, []);
return { return {
conversations, conversations,
@@ -96,50 +91,64 @@ interface UseMessagesReturn {
* 获取指定会话的消息 * 获取指定会话的消息
* 使用 Zustand selector 自动订阅消息变化 * 使用 Zustand selector 自动订阅消息变化
*/ */
export function useMessages( export function useMessages(conversationId: string | null): UseMessagesReturn {
conversationId: string, const normalizedConversationId = conversationId ? String(conversationId) : null;
fetchMessages: (conversationId: string) => Promise<void>,
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
): UseMessagesReturn {
const normalizedConversationId = normalizeConversationId(conversationId);
const store = useMessageStore();
// 使用 useShallow 避免每次返回新数组引用导致的无限循环 // 使用 useShallow 避免每次返回新数组引用导致的无限循环
const messages = useMessageStore( 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 [hasMore, setHasMore] = useState(true);
const loadMoreInFlightRef = useRef(false);
useEffect(() => { useEffect(() => {
// 激活会话 if (!normalizedConversationId) {
fetchMessages(normalizedConversationId).catch(error => { return;
}
// 架构入口:激活会话并完成初始化/同步
messageManager.activateConversation(normalizedConversationId).catch(error => {
console.error('[useMessages] 激活会话失败:', error); console.error('[useMessages] 激活会话失败:', error);
}); });
return () => { return () => {
// 清理活动会话 // 清理活动会话
if (store.getActiveConversation() === normalizedConversationId) { if (messageManager.getActiveConversation() === normalizedConversationId) {
store.setCurrentConversation(null); messageManager.setActiveConversation(null);
} }
}; };
}, [normalizedConversationId]); }, [normalizedConversationId]);
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
const currentMessages = messages; if (!conversationId || !hasMore || loadMoreInFlightRef.current) return;
const currentMessages = messageManager.getMessages(conversationId);
if (currentMessages.length === 0) return; if (currentMessages.length === 0) return;
// 消息数组在 store 内保持 seq 升序,首项即最早消息 // 消息数组在 MessageManager 内保持 seq 升序,首项即最早消息
const minSeq = currentMessages[0]?.seq; const minSeq = currentMessages[0]?.seq ?? Math.min(...currentMessages.map(m => m.seq));
if (minSeq === undefined) return;
const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20); loadMoreInFlightRef.current = true;
setHasMore(loadedMessages.length > 0); const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
}, [conversationId, messages, loadMoreMessages]); loadMoreInFlightRef.current = false;
// 如果没有加载到新消息,说明没有更多了
if (loadedMessages.length === 0) {
setHasMore(false);
}
}, [conversationId, hasMore]);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
await fetchMessages(conversationId); if (!conversationId) return;
}, [conversationId, fetchMessages]);
await messageManager.fetchMessages(conversationId);
setHasMore(true);
}, [conversationId]);
return { return {
messages, messages,
@@ -160,24 +169,23 @@ interface UseSendMessageReturn {
/** /**
* 发送消息 * 发送消息
*/ */
export function useSendMessage( export function useSendMessage(conversationId: string | null): UseSendMessageReturn {
conversationId: string,
sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>
): UseSendMessageReturn {
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => { const sendMessage = useCallback(
setIsSending(true); async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
try { if (!conversationId) return null;
const message = await sendMessageService(conversationId, segments, options);
return message; setIsSending(true);
} catch (error) { try {
console.error('[useSendMessage] 发送消息失败:', error); const message = await messageManager.sendMessage(conversationId, segments, options);
return null; return message;
} finally { } finally {
setIsSending(false); setIsSending(false);
} }
}, [conversationId, sendMessageService]); },
[conversationId]
);
return { return {
sendMessage, sendMessage,
@@ -190,27 +198,42 @@ export function useSendMessage(
interface UseMarkAsReadReturn { interface UseMarkAsReadReturn {
markAsRead: (seq: number) => Promise<void>; markAsRead: (seq: number) => Promise<void>;
markAllAsRead: () => Promise<void>; markAllAsRead: () => Promise<void>;
isMarking: boolean;
} }
/** /**
* 标记已读 * 标记已读
*/ */
export function useMarkAsRead( export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn {
conversationId: string, const [isMarking, setIsMarking] = useState(false);
markAsReadService: (conversationId: string, seq: number) => Promise<void>,
markAllAsReadService: () => Promise<void> const markAsRead = useCallback(
): UseMarkAsReadReturn { async (seq: number) => {
const markAsRead = useCallback(async (seq: number) => { if (!conversationId) return;
await markAsReadService(conversationId, seq);
}, [conversationId, markAsReadService]); setIsMarking(true);
try {
await messageManager.markAsRead(conversationId, seq);
} finally {
setIsMarking(false);
}
},
[conversationId]
);
const markAllAsRead = useCallback(async () => { const markAllAsRead = useCallback(async () => {
await markAllAsReadService(); setIsMarking(true);
}, [markAllAsReadService]); try {
await messageManager.markAllAsRead();
} finally {
setIsMarking(false);
}
}, []);
return { return {
markAsRead, markAsRead,
markAllAsRead, markAllAsRead,
isMarking,
}; };
} }
@@ -219,101 +242,78 @@ export function useMarkAsRead(
interface UseUnreadCountReturn { interface UseUnreadCountReturn {
totalUnreadCount: number; totalUnreadCount: number;
systemUnreadCount: number; systemUnreadCount: number;
refresh: () => Promise<void>;
} }
/** /**
* 获取未读数 * 获取未读消息
* 使用 Zustand selector 直接订阅未读数变化 * 使用 Zustand selector 直接订阅未读数变化
*/ */
export function useUnreadCount(fetchUnreadCount: () => Promise<void>): UseUnreadCountReturn { export function useUnreadCount(): UseUnreadCountReturn {
// 使用 Zustand selector 直接订阅未读数 // 使用 Zustand selector 直接订阅未读数
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
useEffect(() => { useEffect(() => {
// 初始获取 // 初始化时获取最新未读数
fetchUnreadCount(); messageManager.fetchUnreadCount();
}, []); }, []);
const refresh = useCallback(async () => {
await fetchUnreadCount();
}, [fetchUnreadCount]);
return { return {
totalUnreadCount, totalUnreadCount,
systemUnreadCount, systemUnreadCount,
refresh,
}; };
} }
// ==================== useSystemUnreadCount - 获取系统消息未读数 ==================== // ==================== useTotalUnreadCount - 获取总未读数(包含系统消息 ====================
interface UseSystemUnreadCountReturn {
systemUnreadCount: number;
setSystemUnreadCount: (count: number) => void;
incrementSystemUnreadCount: () => void;
decrementSystemUnreadCount: (count?: number) => void;
}
/** /**
* 获取系统消息未读 * 获取总未读消息数(会话未读 + 系统消息未读
* 使用 Zustand selector 直接订阅 * 使用 Zustand selector 直接订阅
*/ */
export function useSystemUnreadCount( export function useTotalUnreadCount(): number {
setSystemUnreadCountService: (count: number) => void, const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
incrementSystemUnreadCountService: () => void,
decrementSystemUnreadCountService: (count?: number) => void
): UseSystemUnreadCountReturn {
// 使用 Zustand selector 直接订阅
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
const setSystemUnreadCount = useCallback((count: number) => { useEffect(() => {
setSystemUnreadCountService(count); // 初始化时获取最新未读数
}, [setSystemUnreadCountService]); messageManager.fetchUnreadCount();
}, []);
const incrementSystemUnreadCount = useCallback(() => { return totalUnreadCount + systemUnreadCount;
incrementSystemUnreadCountService();
}, [incrementSystemUnreadCountService]);
const decrementSystemUnreadCount = useCallback((count = 1) => {
decrementSystemUnreadCountService(count);
}, [decrementSystemUnreadCountService]);
return {
systemUnreadCount,
setSystemUnreadCount,
incrementSystemUnreadCount,
decrementSystemUnreadCount,
};
} }
// ==================== useConversation - 获取单个会话 ==================== // ==================== useConversation - 获取单个会话信息 ====================
interface UseConversationReturn { interface UseConversationReturn {
conversation: ConversationResponse | null; conversation: ConversationResponse | null;
isLoading: boolean;
refresh: () => Promise<void>; refresh: () => Promise<void>;
} }
/** /**
* 获取单个会话 * 获取单个会话信息
* 使用 Zustand selector 直接订阅会话变化 * 使用 Zustand selector 直接订阅会话变化
*/ */
export function useConversation( export function useConversation(conversationId: string | null): UseConversationReturn {
conversationId: string,
fetchConversationDetail: (conversationId: string) => Promise<ConversationResponse | null>
): UseConversationReturn {
// 使用 useShallow 避免每次返回新对象引用导致的无限循环 // 使用 useShallow 避免每次返回新对象引用导致的无限循环
const conversation = useMessageStore( 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 () => { const refresh = useCallback(async () => {
await fetchConversationDetail(conversationId); if (!conversationId) return;
}, [conversationId, fetchConversationDetail]);
setIsLoading(true);
await messageManager.fetchConversationDetail(conversationId);
setIsLoading(false);
}, [conversationId]);
return { return {
conversation, conversation,
isLoading,
refresh, refresh,
}; };
} }
@@ -322,24 +322,28 @@ export function useConversation(
interface UseMessageManagerReturn { interface UseMessageManagerReturn {
isConnected: boolean; isConnected: boolean;
isInitialized: boolean;
} }
/** /**
* 通用消息管理状态 * 通用MessageManager状态
* 使用 Zustand selector 直接订阅连接状态 * 使用 Zustand selector 直接订阅连接状态
*/ */
export function useMessageManager(initialize: () => Promise<void>): UseMessageManagerReturn { export function useMessageManager(): UseMessageManagerReturn {
// 使用 Zustand selector 直接订阅连接状态 // 使用 Zustand selector 直接订阅状态
const isConnected = useMessageStore(state => state.isWSConnected); const isConnected = useMessageStore(state => state.isWSConnected);
const isInitialized = useMessageStore(state => state.isInitialized);
useEffect(() => { useEffect(() => {
initialize().catch(error => { // 初始化
messageManager.initialize().catch(error => {
console.error('[useMessageManager] 初始化失败:', error); console.error('[useMessageManager] 初始化失败:', error);
}); });
}, []); }, []);
return { return {
isConnected, isConnected,
isInitialized,
}; };
} }
@@ -351,25 +355,20 @@ interface UseCreateConversationReturn {
} }
/** /**
* 创建会话 * 创建私聊会话
*/ */
export function useCreateConversation( export function useCreateConversation(): UseCreateConversationReturn {
createConversationService: (userId: string) => Promise<ConversationResponse | null>
): UseCreateConversationReturn {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const createConversation = useCallback(async (userId: string) => { const createConversation = useCallback(async (userId: string): Promise<ConversationResponse | null> => {
setIsCreating(true); setIsCreating(true);
try { try {
const conversation = await createConversationService(userId); const conversation = await messageManager.createConversation(userId);
return conversation; return conversation;
} catch (error) {
console.error('[useCreateConversation] 创建会话失败:', error);
return null;
} finally { } finally {
setIsCreating(false); setIsCreating(false);
} }
}, [createConversationService]); }, []);
return { return {
createConversation, createConversation,
@@ -384,20 +383,130 @@ interface UseUpdateConversationReturn {
} }
/** /**
* 更新会话 * 本地更新会话信息
*/ */
export function useUpdateConversation( export function useUpdateConversation(): UseUpdateConversationReturn {
updateConversationService: (conversationId: string, updates: Partial<ConversationResponse>) => void
): UseUpdateConversationReturn {
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => { const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
updateConversationService(conversationId, updates); messageManager.updateConversation(conversationId, updates);
}, [updateConversationService]); }, []);
return { return {
updateConversation, 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<void>;
isRefreshing: boolean;
}
/**
* MessageListScreen焦点刷新Hook
*/
export function useMessageListRefresh(): UseMessageListRefreshReturn {
const [isRefreshing, setIsRefreshing] = useState(false);
const lastRefreshTimeRef = useRef<number>(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<void>;
refreshMessages: () => Promise<void>;
// 发送消息
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
isSending: boolean;
// 已读相关
markAsRead: (seq: number) => Promise<void>;
// 会话信息
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 - 群聊输入状态 ==================== // ==================== useGroupTyping - 群聊输入状态 ====================
interface UseGroupTypingReturn { interface UseGroupTypingReturn {
@@ -408,75 +517,70 @@ interface UseGroupTypingReturn {
* 获取群聊输入状态 * 获取群聊输入状态
* 使用 Zustand selector 直接订阅 * 使用 Zustand selector 直接订阅
*/ */
export function useGroupTyping(groupId: string): UseGroupTypingReturn { export function useGroupTyping(groupId: string | null): UseGroupTypingReturn {
// 使用 useShallow 避免每次返回新数组引用导致的无限循环 // 使用 useShallow 避免每次返回新数组引用导致的无限循环
const typingUsers = useMessageStore( const typingUsers = useMessageStore(
useShallow(state => state.typingUsersMap.get(groupId) || []) useShallow(state =>
groupId ? (state.typingUsersMap.get(groupId) || []) : []
)
); );
return { return { typingUsers };
typingUsers,
};
} }
// ==================== useGroupMuted - 群聊禁言状态 ==================== // ==================== useGroupMuted - 群聊禁言状态 ====================
interface UseGroupMutedReturn { interface UseGroupMutedReturn {
isMuted: boolean; isMuted: boolean;
setMutedStatus: (muted: boolean) => void; setMuted: (muted: boolean) => void;
} }
/** /**
* 获取群聊禁言状态 * 获取群聊禁言状态
* 使用 Zustand selector 直接订阅 * 使用 Zustand selector 直接订阅
*/ */
export function useGroupMuted(groupId: string): UseGroupMutedReturn { export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn {
// 使用 Zustand selector 直接订阅禁言状态 // 使用 Zustand selector 直接订阅禁言状态
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false); const isMuted = useMessageStore(state =>
const store = useMessageStore(); groupId ? (state.mutedStatusMap.get(groupId) || false) : false
);
const setMutedStatus = useCallback((muted: boolean) => { const setMuted = useCallback((muted: boolean) => {
store.setMutedStatus(groupId, muted); if (groupId) {
}, [groupId, store]); messageManager.setMutedStatus(groupId, muted);
}
}, [groupId]);
return { return { isMuted, setMuted };
isMuted,
setMutedStatus,
};
}
// ==================== useConnectionStatus - 连接状态 ====================
interface UseConnectionStatusReturn {
isConnected: boolean;
} }
/** /**
* 获取连接状态 * MessageListScreen专用Hook组合
* 使用 Zustand selector 直接订阅
*/ */
export function useConnectionStatus(): UseConnectionStatusReturn { interface UseMessageListReturn {
const isConnected = useMessageStore(state => state.isWSConnected); conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
}
export function useMessageList(): UseMessageListReturn {
const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations();
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
const { markAllAsRead } = useMarkAsRead(null);
return { return {
isConnected, conversations,
}; isLoading,
} refresh,
loadMore,
// ==================== useIsInitialized - 初始化状态 ==================== hasMore,
totalUnreadCount,
interface UseIsInitializedReturn { systemUnreadCount,
isInitialized: boolean; markAllAsRead,
}
/**
* 获取初始化状态
* 使用 Zustand selector 直接订阅
*/
export function useIsInitialized(): UseIsInitializedReturn {
const isInitialized = useMessageStore(state => state.isInitialized);
return {
isInitialized,
}; };
} }

View File

@@ -72,7 +72,7 @@ export {
WSMessageHandler, WSMessageHandler,
} from './services'; } from './services';
// ==================== Hooks 导出 ==================== // ==================== Hooks 导出(便捷版,使用 messageManager 单例) ====================
export { export {
// 会话相关 // 会话相关
useConversations, useConversations,
@@ -87,6 +87,7 @@ export {
// 未读数相关 // 未读数相关
useUnreadCount, useUnreadCount,
useTotalUnreadCount,
useSystemUnreadCount, useSystemUnreadCount,
// 群聊相关 // 群聊相关
@@ -95,9 +96,21 @@ export {
// 通用 // 通用
useMessageManager, useMessageManager,
useConnectionStatus, useMessageListRefresh,
useIsInitialized, useChat,
useMessageList,
} from './hooks'; } from './hooks';
// ==================== 数据源导出 ====================
export {
CONVERSATION_LIST_PAGE_SIZE,
type ConversationListPage,
type IConversationListPagedSource,
type RemoteConversationListSourceKind,
NetworkRemoteConversationListPagedSource,
SqliteConversationListPagedSource,
createRemoteConversationListSource,
} from './sources';
// ==================== 默认导出 ==================== // ==================== 默认导出 ====================
export { default } from './MessageManager'; export { default } from './MessageManager';

View File

@@ -16,7 +16,7 @@ import {
SqliteConversationListPagedSource, SqliteConversationListPagedSource,
createRemoteConversationListSource, createRemoteConversationListSource,
CONVERSATION_LIST_PAGE_SIZE, CONVERSATION_LIST_PAGE_SIZE,
} from '../../conversationListSources'; } from '../sources';
import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types'; import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types';
import { useMessageStore, normalizeConversationId, mergeMessagesById } from '../store'; import { useMessageStore, normalizeConversationId, mergeMessagesById } from '../store';
import { ReadReceiptManager } from './ReadReceiptManager'; import { ReadReceiptManager } from './ReadReceiptManager';

View File

@@ -2,7 +2,7 @@
* SQLite MessageManager * SQLite MessageManager
*/ */
import { ConversationResponse } from '../types/dto'; import { ConversationResponse } from '../../types/dto';
import { messageService } from '@/services/message'; import { messageService } from '@/services/message';
import { conversationRepository } from '@/database'; import { conversationRepository } from '@/database';

View File

@@ -1,5 +1,5 @@
import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto'; import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto';
import type { IConversationListPagedSource } from '../conversationListSources'; import type { IConversationListPagedSource } from './sources';
export interface MessageManagerState { export interface MessageManagerState {
conversations: Map<string, ConversationResponse>; conversations: Map<string, ConversationResponse>;

View File

@@ -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';

View File

@@ -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<void>;
loadMore: () => Promise<void>;
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<void>;
refresh: () => Promise<void>;
}
/**
* 获取指定会话的消息
* 使用 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<MessageResponse | null>;
isSending: boolean;
}
/**
* 发送消息
*/
export function useSendMessage(conversationId: string | null): UseSendMessageReturn {
const [isSending, setIsSending] = useState(false);
const sendMessage = useCallback(
async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
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<void>;
markAllAsRead: () => Promise<void>;
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<void>;
}
/**
* 获取单个会话信息
* 使用 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<ConversationResponse | null>;
isCreating: boolean;
}
/**
* 创建私聊会话
*/
export function useCreateConversation(): UseCreateConversationReturn {
const [isCreating, setIsCreating] = useState(false);
const createConversation = useCallback(async (userId: string): Promise<ConversationResponse | null> => {
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<ConversationResponse>) => void;
}
/**
* 本地更新会话信息
*/
export function useUpdateConversation(): UseUpdateConversationReturn {
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
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<void>;
isRefreshing: boolean;
}
/**
* MessageListScreen焦点刷新Hook
*/
export function useMessageListRefresh(): UseMessageListRefreshReturn {
const [isRefreshing, setIsRefreshing] = useState(false);
const lastRefreshTimeRef = useRef<number>(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<void>;
refreshMessages: () => Promise<void>;
// 发送消息
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
isSending: boolean;
// 已读相关
markAsRead: (seq: number) => Promise<void>;
// 会话信息
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<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
}
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,
};
}

View File

@@ -16,7 +16,7 @@ import {
createRemotePostListSource, createRemotePostListSource,
RemotePostListSourceKind, RemotePostListSourceKind,
POST_LIST_PAGE_SIZE, POST_LIST_PAGE_SIZE,
} from '../postListSources'; } from './sources';
function buildListKey(type: string, page: number, pageSize: number): string { function buildListKey(type: string, page: number, pageSize: number): string {
return `list:${type}:${page}:${pageSize}`; return `list:${type}:${page}:${pageSize}`;

View File

@@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef } from 'react';
import { usePostManagerStore } from './postStore'; import { usePostManagerStore } from './postStore';
import { postManager } from './PostManager'; import { postManager } from './PostManager';
import type { Post } from '../../types'; import type { Post } from '../../types';
import { POST_LIST_PAGE_SIZE } from '../postListSources'; import { POST_LIST_PAGE_SIZE } from './sources';
import type { PostListTab } from '../postListSources'; import type { PostListTab } from './sources';
export interface UsePostListResult { export interface UsePostListResult {
posts: Post[]; posts: Post[];

View File

@@ -29,5 +29,17 @@ export {
type UsePostDetailResult, type UsePostDetailResult,
} from './hooks'; } from './hooks';
// ==================== 数据源 ====================
export {
POST_LIST_PAGE_SIZE,
type PostListTab,
type PostListPage,
type IPostListPagedSource,
type RemotePostListSourceKind,
NetworkCursorPostListPagedSource,
NetworkOffsetPostListPagedSource,
createRemotePostListSource,
} from './sources';
// ==================== 默认导出 ==================== // ==================== 默认导出 ====================
export { default } from './PostManager'; export { default } from './PostManager';

View File

@@ -2,9 +2,9 @@
* PostManager * PostManager
*/ */
import { Post } from '../types'; import { Post } from '../../types';
import { postService } from '@/services/post'; import { postService } from '@/services/post';
import type { CursorPaginationRequest } from '../types/dto'; import type { CursorPaginationRequest } from '../../types/dto';
export const POST_LIST_PAGE_SIZE = 20; export const POST_LIST_PAGE_SIZE = 20;

View File

@@ -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';

View File

@@ -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';

View File

@@ -2,8 +2,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { Appearance, AppState, AppStateStatus } from 'react-native'; import { Appearance, AppState, AppStateStatus } from 'react-native';
import { create } from 'zustand'; import { create } from 'zustand';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { lightColors, darkColors, type AppColors } from '../theme/palettes'; import { lightColors, darkColors, type AppColors } from '../../theme/palettes';
import { buildPaperTheme } from '../theme/paperTheme'; import { buildPaperTheme } from '../../theme/paperTheme';
import type { MD3Theme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper';
const STORAGE_KEY = 'app_theme_preference'; const STORAGE_KEY = 'app_theme_preference';

6
src/stores/ui/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* UI 状态模块统一导出
*/
export { useHomeTabBarVisibilityStore } from './homeTabBarVisibilityStore';
export { useHomeTabPressStore } from './homeTabPressStore';

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
import { useUserManagerStore } from './userStore'; import { useUserManagerStore } from './userStore';
import { userManager } from './UserManager'; import { userManager } from './UserManager';
import type { UserDTO } from '../../types/dto'; import type { UserDTO } from '../../types/dto';
import { useAuthStore } from '../authStore'; import { useAuthStore } from '../auth/authStore';
export interface UseCurrentUserResult { export interface UseCurrentUserResult {
user: UserDTO | null; user: UserDTO | null;

View File

@@ -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';

View File

@@ -12,8 +12,8 @@ import {
postService, postService,
notificationService, notificationService,
} from '../services'; } from '../services';
import { userManager } from './userManager'; import { userManager } from './user';
import { messageManager } from './messageManager'; import { messageManager } from './message';
interface UserState { interface UserState {
// 状态 // 状态

View File

@@ -2,8 +2,8 @@
* URL * URL
* id stash id * id stash id
*/ */
import type { SystemMessageResponse } from '../types/dto'; import type { SystemMessageResponse } from '../../types/dto';
import type { Course } from '../types/schedule'; import type { Course } from '../../types/schedule';
const systemById = new Map<string, SystemMessageResponse>(); const systemById = new Map<string, SystemMessageResponse>();
const courseById = new Map<string, { course: Course; relatedCourses: Course[] }>(); const courseById = new Map<string, { course: Course; relatedCourses: Course[] }>();

View File

@@ -17,7 +17,7 @@ export {
useThemeStore, useThemeStore,
type ThemePreference, type ThemePreference,
type ResolvedScheme, type ResolvedScheme,
} from '../stores/themeStore'; } from '../stores/settings';
import { lightColors } from './palettes'; import { lightColors } from './palettes';
import { buildPaperTheme } from './paperTheme'; import { buildPaperTheme } from './paperTheme';