Files
frontend/src/stores/message/store.ts

464 lines
14 KiB
TypeScript
Raw Normal View History

/**
* 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 });
}
}