Refactor the message module to streamline state management by removing the `MessageStateManager` and `SubscriptionManager` layers, relying instead on Zustand's built-in selector mechanism for reactive updates. - Remove redundant `setConversations`, `addMessage`, and `updateMessage` actions from the Zustand store to favor more specialized update methods. - Clean up `MessageService` by removing deprecated conversation and message sending convenience methods. - Update service interfaces to support new synchronization capabilities including `syncBySeq` and `restartSources`. - Simplify documentation and comments across the message store and its associated services.
464 lines
14 KiB
TypeScript
464 lines
14 KiB
TypeScript
/**
|
||
* 消息状态 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<string, ConversationResponse>;
|
||
conversationList: ConversationResponse[];
|
||
|
||
// 消息相关 - 按会话ID存储
|
||
messagesMap: Map<string, MessageResponse[]>;
|
||
|
||
// 未读数
|
||
totalUnreadCount: number;
|
||
systemUnreadCount: number;
|
||
|
||
// 最后一条系统通知时间
|
||
lastSystemMessageAt: string | null;
|
||
|
||
// 连接状态
|
||
isWSConnected: boolean;
|
||
|
||
// 当前活动会话ID(用户正在查看的会话)
|
||
currentConversationId: string | null;
|
||
|
||
// 加载状态
|
||
isLoadingConversations: boolean;
|
||
loadingMessagesSet: Set<string>;
|
||
|
||
// 初始化状态
|
||
isInitialized: boolean;
|
||
|
||
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
||
typingUsersMap: Map<string, string[]>;
|
||
|
||
// 当前用户的禁言状态 - 按群组ID存储
|
||
mutedStatusMap: Map<string, boolean>;
|
||
|
||
// 会话免打扰状态 - 按会话ID存储
|
||
notificationMutedMap: Map<string, boolean>;
|
||
}
|
||
|
||
// ==================== 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<string, ConversationResponse>,
|
||
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<string, ConversationResponse>): 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<string, MessageResponse>();
|
||
[...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<MessageStore>((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<string, ConversationResponse>,
|
||
totalUnread: number,
|
||
systemUnread: number,
|
||
) => {
|
||
const conversationList = sortConversationList(conversations);
|
||
const notificationMutedMap = new Map<string, boolean>();
|
||
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<void> {
|
||
const time = await AsyncStorage.getItem(LAST_SYSTEM_MESSAGE_AT_KEY);
|
||
if (time) {
|
||
useMessageStore.setState({ lastSystemMessageAt: time });
|
||
}
|
||
}
|
||
|