/** * MessageManager React Hooks * * 提供React组件与MessageManager的集成 * 所有hooks都基于Zustand selector机制 * 确保组件能实时获取状态更新 * * 重构说明: * - 移除了手动订阅机制,改用 Zustand selector * - Zustand 会自动处理依赖追踪和组件重渲染 * - 不需要手动管理订阅/取消订阅 */ import { useState, useEffect, useCallback, useRef } from 'react'; import { useFocusEffect } from 'expo-router'; import { useShallow } from 'zustand/react/shallow'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto'; import { messageManager } from './MessageManager'; import { useMessageStore } from './store'; import { useUnreadCountQuery } from '../../hooks/useUnreadCount'; const EMPTY_MESSAGES: MessageResponse[] = []; // ==================== useConversations - 获取会话列表 ==================== interface UseConversationsReturn { conversations: ConversationResponse[]; isLoading: boolean; refresh: () => Promise; loadMore: () => Promise; hasMore: boolean; } /** * 获取会话列表 * 使用 Zustand selector 自动订阅状态变化 */ export function useConversations(): UseConversationsReturn { // 使用 Zustand selector 直接订阅状态 const conversations = useMessageStore(state => state.conversationList); const isLoading = useMessageStore(state => state.isLoadingConversations); const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations()); useEffect(() => { // 初始化时确保 MessageManager 已初始化 messageManager.initialize().catch(error => { console.error('[useConversations] 初始化失败:', error); }); // 仅在首次初始化时做一次冷启动兜底刷新,避免每次挂载都触发 const store = useMessageStore.getState(); if (!store.isInitialized) { const coldStartSyncTimer = setTimeout(() => { messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => { console.error('[useConversations] 冷启动强制刷新失败:', error); }); }, 1200); return () => { clearTimeout(coldStartSyncTimer); }; } }, []); // 监听 hasMore 变化 useEffect(() => { setHasMore(messageManager.canLoadMoreConversations()); }, [conversations]); const refresh = useCallback(async () => { await messageManager.refreshConversations(true, 'hooks-manual-refresh'); }, []); const loadMore = useCallback(async () => { await messageManager.loadMoreConversations(); }, []); return { conversations, isLoading, refresh, loadMore, hasMore, }; } // ==================== useMessages - 获取指定会话的消息 ==================== interface UseMessagesReturn { messages: MessageResponse[]; isLoading: boolean; hasMore: boolean; loadMore: () => Promise; refresh: () => Promise; } /** * 获取指定会话的消息 * 使用 Zustand selector 自动订阅消息变化 * * 核心修复: * 1. useFocusEffect:屏幕重新获得焦点时强制同步消息,解决"需要退出重进才能看到新消息"的问题 * 2. 引用计数清理 isLoadingMessages:仅在最后一个订阅者卸载时清除 loading 锁, * 避免 StrictMode 双调用或快速切换会话时,第一次 fetch 还没完成就被清锁 * 3. 重入时 forceSync:相同 conversationId 重复进入时强制拉取最新消息 */ // 记录每个 conversationId 上的活跃订阅数,仅在归零时清锁 const loadingLockRefCount: Map = new Map(); export function useMessages(conversationId: string | null): UseMessagesReturn { const normalizedConversationId = conversationId ? String(conversationId) : null; // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const messages = useMessageStore( useShallow(state => { if (!normalizedConversationId) return EMPTY_MESSAGES; return state.messagesMap.get(normalizedConversationId) ?? EMPTY_MESSAGES; }) ); const isLoadingMessages = useMessageStore(state => normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false ); const [hasMore, setHasMore] = useState(true); const loadMoreInFlightRef = useRef(false); // 追踪上次激活的 conversationId,避免同一会话重复激活时不必要的 forceSync const lastActivatedIdRef = useRef(null); // 追踪 useEffect 激活时间,与 useFocusEffect 去重 const lastActivatedAtRef = useRef(0); useEffect(() => { if (!normalizedConversationId) { return; } // 引用计数 +1 const prevCount = loadingLockRefCount.get(normalizedConversationId) ?? 0; loadingLockRefCount.set(normalizedConversationId, prevCount + 1); lastActivatedAtRef.current = Date.now(); const isReentry = lastActivatedIdRef.current === normalizedConversationId; // 架构入口:激活会话并完成初始化/同步 // 重入同一会话时 forceSync,确保拿到最新消息 messageManager.activateConversation(normalizedConversationId, { forceSync: isReentry }).catch(error => { console.error('[useMessages] 激活会话失败:', error); }); lastActivatedIdRef.current = normalizedConversationId; return () => { // 引用计数 -1,仅在最后一个订阅者卸载时清活动会话 + 清 loading 锁 const nextCount = (loadingLockRefCount.get(normalizedConversationId) ?? 1) - 1; if (nextCount <= 0) { loadingLockRefCount.delete(normalizedConversationId); if (messageManager.getActiveConversation() === normalizedConversationId) { messageManager.setActiveConversation(null); } useMessageStore.getState().setLoadingMessages(normalizedConversationId, false); } else { loadingLockRefCount.set(normalizedConversationId, nextCount); } }; }, [normalizedConversationId]); // 屏幕重新获得焦点时,同步活动会话的最新消息 useFocusEffect( useCallback(() => { if (!normalizedConversationId) return; // 如果刚被 useEffect 激活(500ms 内),跳过焦点同步,避免重复 if (Date.now() - lastActivatedAtRef.current < 500) return; // 先清除可能残留的 loading 锁,防止上次退出时未正常清理 useMessageStore.getState().setLoadingMessages(normalizedConversationId, false); // 增量同步最新消息(从当前内存最高 seq 开始拉取) const currentMessages = useMessageStore.getState().getMessages(normalizedConversationId); const maxSeq = currentMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0); if (maxSeq > 0) { messageManager.fetchMessages(normalizedConversationId, maxSeq).catch(error => { console.error('[useMessages] 焦点同步消息失败:', error); }); } else { messageManager.fetchMessages(normalizedConversationId).catch(error => { console.error('[useMessages] 焦点同步消息(冷启动)失败:', error); }); } }, [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; detailType?: 'private' | 'group' }) => Promise; retrySendMessage: (failedMessage: MessageResponse) => Promise; isSending: boolean; } /** * 发送消息 */ export function useSendMessage(conversationId: string | null): UseSendMessageReturn { const [isSending, setIsSending] = useState(false); const sendMessage = useCallback( async (segments: MessageSegment[], options?: { replyToId?: string; detailType?: 'private' | 'group' }): Promise => { if (!conversationId) return null; setIsSending(true); try { const message = await messageManager.sendMessage(conversationId, segments, options); return message; } finally { setIsSending(false); } }, [conversationId] ); const retrySendMessage = useCallback( async (failedMessage: MessageResponse): Promise => { if (!conversationId) return null; setIsSending(true); try { const message = await messageManager.retrySendMessage(conversationId, failedMessage); return message; } finally { setIsSending(false); } }, [conversationId] ); return { sendMessage, retrySendMessage, isSending, }; } // ==================== useMarkAsRead - 标记已读 ==================== interface UseMarkAsReadReturn { markAsRead: (seq: number) => Promise; markAllAsRead: () => Promise; isMarking: boolean; } /** * 标记已读 */ export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn { const [isMarking, setIsMarking] = useState(false); const markAsRead = useCallback( async (seq: number) => { if (!conversationId) return; setIsMarking(true); try { await messageManager.markAsRead(conversationId, seq); } finally { setIsMarking(false); } }, [conversationId] ); const markAllAsRead = useCallback(async () => { setIsMarking(true); try { await messageManager.markAllAsRead(); } finally { setIsMarking(false); } }, []); return { markAsRead, markAllAsRead, isMarking, }; } // ==================== useUnreadCount - 获取未读数 ==================== interface UseUnreadCountReturn { totalUnreadCount: number; systemUnreadCount: number; } /** * 获取未读消息数 * 使用 Zustand selector 直接订阅未读数变化 */ export function useUnreadCount(): UseUnreadCountReturn { // Use React Query for fetching (caching + auto-refetch), Zustand selectors for reading useUnreadCountQuery(); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); return { totalUnreadCount, systemUnreadCount, }; } // ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ==================== /** * 获取总未读消息数(会话未读 + 系统消息未读) * 使用 Zustand selector 直接订阅 */ export function useTotalUnreadCount(): number { // Use React Query for fetching, Zustand selectors for reading useUnreadCountQuery(); const totalUnreadCount = useMessageStore(state => state.totalUnreadCount); const systemUnreadCount = useMessageStore(state => state.systemUnreadCount); return totalUnreadCount + systemUnreadCount; } // ==================== useConversation - 获取单个会话信息 ==================== interface UseConversationReturn { conversation: ConversationResponse | null; isLoading: boolean; refresh: () => Promise; } /** * 获取单个会话信息 * 使用 Zustand selector 直接订阅会话变化 */ export function useConversation(conversationId: string | null): UseConversationReturn { // 使用 useShallow 避免每次返回新对象引用导致的无限循环 const conversation = useMessageStore( useShallow(state => conversationId ? (state.conversations.get(conversationId) || null) : null ) ); const [isLoading, setIsLoading] = useState(false); const refresh = useCallback(async () => { if (!conversationId) return; setIsLoading(true); await messageManager.fetchConversationDetail(conversationId); setIsLoading(false); }, [conversationId]); return { conversation, isLoading, refresh, }; } // ==================== useMessageManager - 通用状态 ==================== interface UseMessageManagerReturn { isConnected: boolean; isInitialized: boolean; } /** * 通用MessageManager状态 * 使用 Zustand selector 直接订阅连接状态 */ export function useMessageManager(): UseMessageManagerReturn { // 使用 Zustand selector 直接订阅状态 const isConnected = useMessageStore(state => state.isWSConnected); const isInitialized = useMessageStore(state => state.isInitialized); useEffect(() => { // 初始化 messageManager.initialize().catch(error => { console.error('[useMessageManager] 初始化失败:', error); }); }, []); return { isConnected, isInitialized, }; } // ==================== useCreateConversation - 创建会话 ==================== interface UseCreateConversationReturn { createConversation: (userId: string) => Promise; isCreating: boolean; } /** * 创建私聊会话 */ export function useCreateConversation(): UseCreateConversationReturn { const [isCreating, setIsCreating] = useState(false); const createConversation = useCallback(async (userId: string): Promise => { setIsCreating(true); try { const conversation = await messageManager.createConversation(userId); return conversation; } finally { setIsCreating(false); } }, []); return { createConversation, isCreating, }; } // ==================== useUpdateConversation - 更新会话 ==================== interface UseUpdateConversationReturn { updateConversation: (conversationId: string, updates: Partial) => void; } /** * 本地更新会话信息 */ export function useUpdateConversation(): UseUpdateConversationReturn { const updateConversation = useCallback((conversationId: string, updates: Partial) => { messageManager.updateConversation(conversationId, updates); }, []); return { updateConversation, }; } // ==================== useSystemUnreadCount - 系统消息未读数操作 ==================== interface UseSystemUnreadCountReturn { setSystemUnreadCount: (count: number) => void; incrementSystemUnreadCount: () => void; decrementSystemUnreadCount: (count?: number) => void; } /** * 系统消息未读数操作 */ export function useSystemUnreadCount(): UseSystemUnreadCountReturn { const setSystemUnreadCount = useCallback((count: number) => { messageManager.setSystemUnreadCount(count); }, []); const incrementSystemUnreadCount = useCallback(() => { messageManager.incrementSystemUnreadCount(); }, []); const decrementSystemUnreadCount = useCallback((count?: number) => { messageManager.decrementSystemUnreadCount(count); }, []); return { setSystemUnreadCount, incrementSystemUnreadCount, decrementSystemUnreadCount, }; } // ==================== useMessageListRefresh - MessageListScreen焦点刷新 ==================== interface UseMessageListRefreshReturn { refresh: () => Promise; isRefreshing: boolean; } /** * MessageListScreen焦点刷新Hook */ export function useMessageListRefresh(): UseMessageListRefreshReturn { const [isRefreshing, setIsRefreshing] = useState(false); const lastRefreshTimeRef = useRef(0); const refresh = useCallback(async () => { // 防止重复刷新(最小间隔500ms) const now = Date.now(); if (now - lastRefreshTimeRef.current < 500) { return; } lastRefreshTimeRef.current = now; setIsRefreshing(true); try { await messageManager.refreshConversations(true, 'hooks-load-more-fallback'); } catch (error) { console.error('[useMessageListRefresh] 刷新失败:', error); } finally { setIsRefreshing(false); } }, []); return { refresh, isRefreshing, }; } // ==================== 便捷hooks组合 ==================== /** * ChatScreen专用Hook组合 */ interface UseChatReturn { // 消息相关 messages: MessageResponse[]; isLoadingMessages: boolean; hasMoreMessages: boolean; loadMoreMessages: () => Promise; refreshMessages: () => Promise; // 发送消息 sendMessage: (segments: MessageSegment[], options?: { replyToId?: string; detailType?: 'private' | 'group' }) => Promise; retrySendMessage: (failedMessage: MessageResponse) => Promise; isSending: boolean; // 已读相关 markAsRead: (seq: number) => Promise; // 会话信息 conversation: ConversationResponse | null; } export function useChat(conversationId: string | null): UseChatReturn { const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId); const { sendMessage, retrySendMessage, isSending } = useSendMessage(conversationId); const { markAsRead } = useMarkAsRead(conversationId); const { conversation } = useConversation(conversationId); return { messages, isLoadingMessages, hasMoreMessages, loadMoreMessages: loadMore, refreshMessages: refresh, sendMessage, retrySendMessage, isSending, markAsRead, conversation, }; } // ==================== useGroupTyping - 群聊输入状态 ==================== interface UseGroupTypingReturn { typingUsers: string[]; } /** * 获取群聊输入状态 * 使用 Zustand selector 直接订阅 */ export function useGroupTyping(groupId: string | null): UseGroupTypingReturn { // 使用 useShallow 避免每次返回新数组引用导致的无限循环 const typingUsers = useMessageStore( useShallow(state => groupId ? (state.typingUsersMap.get(groupId) || []) : [] ) ); return { typingUsers }; } // ==================== useGroupMuted - 群聊禁言状态 ==================== interface UseGroupMutedReturn { isMuted: boolean; setMuted: (muted: boolean) => void; } /** * 获取群聊禁言状态 * 使用 Zustand selector 直接订阅 */ export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn { // 使用 Zustand selector 直接订阅禁言状态 const isMuted = useMessageStore(state => groupId ? (state.mutedStatusMap.get(groupId) || false) : false ); const setMuted = useCallback((muted: boolean) => { if (groupId) { messageManager.setMutedStatus(groupId, muted); } }, [groupId]); return { isMuted, setMuted }; } /** * MessageListScreen专用Hook组合 */ interface UseMessageListReturn { conversations: ConversationResponse[]; isLoading: boolean; refresh: () => Promise; loadMore: () => Promise; hasMore: boolean; totalUnreadCount: number; systemUnreadCount: number; markAllAsRead: () => Promise; } export function useMessageList(): UseMessageListReturn { const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations(); const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); const { markAllAsRead } = useMarkAsRead(null); return { conversations, isLoading, refresh, loadMore, hasMore, totalUnreadCount, systemUnreadCount, markAllAsRead, }; }