refactor(stores): reorganize flat store files into modular directory structure
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:
@@ -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 => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
// ==================== 预取配置 ====================
|
// ==================== 预取配置 ====================
|
||||||
|
|
||||||
|
|||||||
@@ -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' : '';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 管理成员列表
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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: '全部' },
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }>();
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
10
src/stores/auth/index.ts
Normal 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';
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
SubmitVerificationRequest,
|
SubmitVerificationRequest,
|
||||||
UserIdentity,
|
UserIdentity,
|
||||||
VerificationStatus,
|
VerificationStatus,
|
||||||
} from '../types/dto';
|
} from '../../types/dto';
|
||||||
|
|
||||||
interface VerificationState {
|
interface VerificationState {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -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
6
src/stores/call/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 通话模块统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { callStore } from './callStore';
|
||||||
|
export type { CallType, CallSession, CallStatus, IncomingCallInfo } from './callStore';
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为群成员列表补全用户资料(昵称、头像、用户名)。
|
* 为群成员列表补全用户资料(昵称、头像、用户名)。
|
||||||
@@ -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(
|
||||||
@@ -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';
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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,
|
||||||
@@ -139,3 +142,4 @@ export {
|
|||||||
getCacheRemainingTTL,
|
getCacheRemainingTTL,
|
||||||
DEFAULT_TTL,
|
DEFAULT_TTL,
|
||||||
} from './utils/cache';
|
} from './utils/cache';
|
||||||
|
export { routePayloadCache } from './utils/routePayloadCache';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
// 导入服务
|
// 导入服务
|
||||||
|
|||||||
482
src/stores/message/baseHooks.ts
Normal file
482
src/stores/message/baseHooks.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
|
||||||
|
if (!conversationId) return null;
|
||||||
|
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
try {
|
try {
|
||||||
const message = await sendMessageService(conversationId, segments, options);
|
const message = await messageManager.sendMessage(conversationId, segments, options);
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
|
||||||
console.error('[useSendMessage] 发送消息失败:', error);
|
|
||||||
return null;
|
|
||||||
} 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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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';
|
|
||||||
26
src/stores/settings/index.ts
Normal file
26
src/stores/settings/index.ts
Normal 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';
|
||||||
@@ -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
6
src/stores/ui/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* UI 状态模块统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useHomeTabBarVisibilityStore } from './homeTabBarVisibilityStore';
|
||||||
|
export { useHomeTabPressStore } from './homeTabPressStore';
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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 {
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -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[] }>();
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user