Files
frontend/src/stores/message/store.ts
lan 7e0436a799
All checks were successful
Frontend CI / ota-ios (push) Successful in 1m36s
Frontend CI / ota-android (push) Successful in 1m36s
Frontend CI / build-and-push-web (push) Successful in 5m6s
Frontend CI / build-android-apk (push) Successful in 39m18s
refactor(message): simplify state management and remove redundant service methods
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.
2026-05-12 18:26:27 +08:00

464 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 消息状态 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 });
}
}