/** * 消息状态 Zustand Store * 使用 zustand 进行状态管理,提供响应式状态更新 */ import { create } from 'zustand'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { ConversationResponse, MessageResponse, } from '../../types/dto'; const LAST_SYSTEM_MESSAGE_AT_KEY = 'last_system_message_at'; // ==================== 状态接口 ==================== /** * 消息状态接口(不包含订阅者,订阅者单独管理) */ export interface MessageState { // 会话相关 conversations: Map; conversationList: ConversationResponse[]; // 消息相关 - 按会话ID存储 messagesMap: Map; // 未读数 totalUnreadCount: number; systemUnreadCount: number; // 最后一条系统通知时间 lastSystemMessageAt: string | null; // 连接状态 isWSConnected: boolean; // 当前活动会话ID(用户正在查看的会话) currentConversationId: string | null; // 加载状态 isLoadingConversations: boolean; loadingMessagesSet: Set; // 初始化状态 isInitialized: boolean; // 输入状态 - 按群组ID存储正在输入的用户ID列表 typingUsersMap: Map; // 当前用户的禁言状态 - 按群组ID存储 mutedStatusMap: Map; // 会话免打扰状态 - 按会话ID存储 notificationMutedMap: Map; } // ==================== Actions 接口 ==================== export interface MessageActions { // 获取状态 getState: () => MessageState; getConversations: () => ConversationResponse[]; getConversation: (conversationId: string) => ConversationResponse | null; getMessages: (conversationId: string) => MessageResponse[]; getUnreadCount: () => { total: number; system: number }; isConnected: () => boolean; isLoading: () => boolean; isLoadingMessages: (conversationId: string) => boolean; getTypingUsers: (groupId: string) => string[]; isMuted: (groupId: string) => boolean; getActiveConversation: () => string | null; isNotificationMuted: (conversationId: string) => boolean; // 设置状态 setConversationsWithUnread: ( conversations: Map, totalUnread: number, systemUnread: number, ) => void; updateConversation: (conversation: ConversationResponse) => void; removeConversation: (conversationId: string) => void; setMessages: (conversationId: string, messages: MessageResponse[]) => void; setUnreadCount: (total: number, system: number) => void; setLastSystemMessageAt: (time: string | null) => void; setSSEConnected: (connected: boolean) => void; setCurrentConversation: (conversationId: string | null) => void; setLoading: (loading: boolean) => void; setLoadingMessages: (conversationId: string, loading: boolean) => void; setTypingUsers: (groupId: string, users: string[]) => void; setMutedStatus: (groupId: string, isMuted: boolean) => void; setNotificationMuted: (conversationId: string, notificationMuted: boolean) => void; setInitialized: (initialized: boolean) => void; // 原子性更新(避免中间渲染状态) updateConversationWithUnread: ( conversation: ConversationResponse, totalUnread: number, systemUnread: number, ) => void; incrementUnreadAtomic: (conversationId: string) => void; // 重置 reset: () => void; } // ==================== 初始状态 ==================== const initialState: MessageState = { conversations: new Map(), conversationList: [], messagesMap: new Map(), totalUnreadCount: 0, systemUnreadCount: 0, lastSystemMessageAt: null, isWSConnected: false, currentConversationId: null, isLoadingConversations: false, loadingMessagesSet: new Set(), isInitialized: false, typingUsersMap: new Map(), mutedStatusMap: new Map(), notificationMutedMap: new Map(), }; // ==================== 工具函数 ==================== /** * 规范化会话ID */ export function normalizeConversationId(conversationId: string | number | null | undefined): string { return conversationId == null ? '' : String(conversationId); } /** * 更新会话列表排序 * 排序规则:置顶优先,再按最后消息时间排序 */ function sortConversationList(conversations: Map): ConversationResponse[] { return Array.from(conversations.values()).sort((a, b) => { const aPinned = a.is_pinned ? 1 : 0; const bPinned = b.is_pinned ? 1 : 0; if (aPinned !== bPinned) { return bPinned - aPinned; } const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime(); const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime(); if (Number.isNaN(aTime) || Number.isNaN(bTime)) return 0; return bTime - aTime; }); } /** * 按 message_id 合并消息并按 seq 排序 */ export function mergeMessagesById(base: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] { if (incoming.length === 0) return base; const merged = new Map(); [...base, ...incoming].forEach(msg => { merged.set(String(msg.id), msg); }); return Array.from(merged.values()).sort((a, b) => a.seq - b.seq); } // ==================== Store 创建 ==================== export type MessageStore = MessageState & MessageActions; export const useMessageStore = create((set, get) => ({ // ==================== 初始状态 ==================== ...initialState, // ==================== 获取状态 ==================== getState: () => { const state = get(); return { conversations: state.conversations, conversationList: state.conversationList, messagesMap: state.messagesMap, totalUnreadCount: state.totalUnreadCount, systemUnreadCount: state.systemUnreadCount, lastSystemMessageAt: state.lastSystemMessageAt, isWSConnected: state.isWSConnected, currentConversationId: state.currentConversationId, isLoadingConversations: state.isLoadingConversations, loadingMessagesSet: state.loadingMessagesSet, isInitialized: state.isInitialized, typingUsersMap: state.typingUsersMap, mutedStatusMap: state.mutedStatusMap, notificationMutedMap: state.notificationMutedMap, }; }, getConversations: () => { return get().conversationList; }, getConversation: (conversationId: string) => { const normalizedId = normalizeConversationId(conversationId); return get().conversations.get(normalizedId) || null; }, getMessages: (conversationId: string) => { const normalizedId = normalizeConversationId(conversationId); return get().messagesMap.get(normalizedId) || []; }, getUnreadCount: () => { const state = get(); return { total: state.totalUnreadCount, system: state.systemUnreadCount, }; }, isConnected: () => { return get().isWSConnected; }, isLoading: () => { return get().isLoadingConversations; }, isLoadingMessages: (conversationId: string) => { const normalizedId = normalizeConversationId(conversationId); return get().loadingMessagesSet.has(normalizedId); }, getTypingUsers: (groupId: string) => { return get().typingUsersMap.get(groupId) || []; }, isMuted: (groupId: string) => { return get().mutedStatusMap.get(groupId) || false; }, getActiveConversation: () => { return get().currentConversationId; }, isNotificationMuted: (conversationId: string) => { return get().notificationMutedMap.get(conversationId) || false; }, // ==================== 设置状态 ==================== setConversationsWithUnread: ( conversations: Map, totalUnread: number, systemUnread: number, ) => { const conversationList = sortConversationList(conversations); const notificationMutedMap = new Map(); conversations.forEach((conv, id) => { if (conv.notification_muted) { notificationMutedMap.set(id, true); } }); set(state => ({ conversations, conversationList, notificationMutedMap: new Map([...state.notificationMutedMap, ...notificationMutedMap]), totalUnreadCount: totalUnread, systemUnreadCount: systemUnread, })); }, updateConversation: (conversation: ConversationResponse) => { const id = normalizeConversationId(conversation.id); set(state => { const newConversations = new Map(state.conversations); newConversations.set(id, { ...conversation, id }); const conversationList = sortConversationList(newConversations); const newNotificationMutedMap = new Map(state.notificationMutedMap); if (conversation.notification_muted) { newNotificationMutedMap.set(id, true); } else { newNotificationMutedMap.delete(id); } return { conversations: newConversations, conversationList, notificationMutedMap: newNotificationMutedMap }; }); }, removeConversation: (conversationId: string) => { const normalizedId = normalizeConversationId(conversationId); set(state => { const target = state.conversations.get(normalizedId); const removedUnread = target?.unread_count || 0; const newConversations = new Map(state.conversations); newConversations.delete(normalizedId); const newMessagesMap = new Map(state.messagesMap); newMessagesMap.delete(normalizedId); const newLoadingMessagesSet = new Set(state.loadingMessagesSet); newLoadingMessagesSet.delete(normalizedId); const newTotalUnreadCount = Math.max(0, state.totalUnreadCount - removedUnread); const newCurrentConversationId = state.currentConversationId === normalizedId ? null : state.currentConversationId; const conversationList = sortConversationList(newConversations); return { conversations: newConversations, conversationList, messagesMap: newMessagesMap, totalUnreadCount: newTotalUnreadCount, currentConversationId: newCurrentConversationId, loadingMessagesSet: newLoadingMessagesSet, }; }); }, setMessages: (conversationId: string, messages: MessageResponse[]) => { const normalizedId = normalizeConversationId(conversationId); set(state => { const newMessagesMap = new Map(state.messagesMap); newMessagesMap.set(normalizedId, messages); return { messagesMap: newMessagesMap }; }); }, setUnreadCount: (total: number, system: number) => { set({ totalUnreadCount: total, systemUnreadCount: system }); }, setLastSystemMessageAt: (time: string | null) => { set({ lastSystemMessageAt: time }); if (time) { AsyncStorage.setItem(LAST_SYSTEM_MESSAGE_AT_KEY, time).catch(() => {}); } else { AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {}); } }, setSSEConnected: (connected: boolean) => { set({ isWSConnected: connected }); }, setCurrentConversation: (conversationId: string | null) => { set({ currentConversationId: conversationId }); }, setLoading: (loading: boolean) => { set({ isLoadingConversations: loading }); }, setLoadingMessages: (conversationId: string, loading: boolean) => { const normalizedId = normalizeConversationId(conversationId); set(state => { const newLoadingMessagesSet = new Set(state.loadingMessagesSet); if (loading) { newLoadingMessagesSet.add(normalizedId); } else { newLoadingMessagesSet.delete(normalizedId); } return { loadingMessagesSet: newLoadingMessagesSet }; }); }, setTypingUsers: (groupId: string, users: string[]) => { set(state => { const newTypingUsersMap = new Map(state.typingUsersMap); newTypingUsersMap.set(groupId, users); return { typingUsersMap: newTypingUsersMap }; }); }, setMutedStatus: (groupId: string, isMuted: boolean) => { set(state => { const newMutedStatusMap = new Map(state.mutedStatusMap); newMutedStatusMap.set(groupId, isMuted); return { mutedStatusMap: newMutedStatusMap }; }); }, setNotificationMuted: (conversationId: string, notificationMuted: boolean) => { set(state => { const newNotificationMutedMap = new Map(state.notificationMutedMap); newNotificationMutedMap.set(conversationId, notificationMuted); return { notificationMutedMap: newNotificationMutedMap }; }); }, setInitialized: (initialized: boolean) => { set({ isInitialized: initialized }); }, updateConversationWithUnread: ( conversation: ConversationResponse, totalUnread: number, systemUnread: number, ) => { const id = normalizeConversationId(conversation.id); set(state => { const newConversations = new Map(state.conversations); newConversations.set(id, { ...conversation, id }); const conversationList = sortConversationList(newConversations); const newNotificationMutedMap = new Map(state.notificationMutedMap); if (conversation.notification_muted) { newNotificationMutedMap.set(id, true); } else { newNotificationMutedMap.delete(id); } return { conversations: newConversations, conversationList, notificationMutedMap: newNotificationMutedMap, totalUnreadCount: totalUnread, systemUnreadCount: systemUnread, }; }); }, incrementUnreadAtomic: (conversationId: string) => { const normalizedId = normalizeConversationId(conversationId); set(state => { const conversation = state.conversations.get(normalizedId); if (!conversation) return state; const newConversations = new Map(state.conversations); newConversations.set(normalizedId, { ...conversation, unread_count: (conversation.unread_count || 0) + 1, }); const conversationList = sortConversationList(newConversations); return { conversations: newConversations, conversationList, totalUnreadCount: state.totalUnreadCount + 1, }; }); }, // ==================== 重置 ==================== reset: () => { set({ ...initialState, conversations: new Map(), messagesMap: new Map(), loadingMessagesSet: new Set(), typingUsersMap: new Map(), mutedStatusMap: new Map(), notificationMutedMap: new Map(), }); AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {}); }, })); export async function loadPersistedLastSystemMessageAt(): Promise { const time = await AsyncStorage.getItem(LAST_SYSTEM_MESSAGE_AT_KEY); if (time) { useMessageStore.setState({ lastSystemMessageAt: time }); } }