refactor(message): replace MessageStateManager with zustand store
- Remove MessageStateManager wrapper layer in favor of direct zustand store - Move service modules to services/ subdirectory (ConversationOperations, MessageDeduplication, MessageSendService, MessageSyncService, ReadReceiptManager, UserCacheService, WSMessageHandler) - Add new zustand-based store with subscriptionManager for event handling - Introduce React hooks for message state (useConversations, useMessages, useUnreadCount, etc.) - Update exports in index.ts to reflect new module structure - Deprecate messageManager.ts entry point in favor of message/index.ts - Maintain backward compatibility with existing MessageManager API
This commit is contained in:
428
src/stores/message/store.ts
Normal file
428
src/stores/message/store.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 消息状态 Zustand Store
|
||||
* 使用 zustand 进行状态管理,提供响应式状态更新
|
||||
*
|
||||
* 重构说明:
|
||||
* - 移除了 MessageStateManager 包装层
|
||||
* - 直接使用 zustand store 进行状态管理
|
||||
* - 订阅管理器集成到 store 中
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
ConversationResponse,
|
||||
MessageResponse,
|
||||
} from '../../types/dto';
|
||||
import type {
|
||||
MessageSubscriber,
|
||||
MessageEvent,
|
||||
} from './types';
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
/**
|
||||
* 消息状态接口(不包含订阅者,订阅者单独管理)
|
||||
*/
|
||||
export interface MessageState {
|
||||
// 会话相关
|
||||
conversations: Map<string, ConversationResponse>;
|
||||
conversationList: ConversationResponse[];
|
||||
|
||||
// 消息相关 - 按会话ID存储
|
||||
messagesMap: Map<string, MessageResponse[]>;
|
||||
|
||||
// 未读数
|
||||
totalUnreadCount: number;
|
||||
systemUnreadCount: number;
|
||||
|
||||
// 连接状态
|
||||
isWSConnected: boolean;
|
||||
|
||||
// 当前活动会话ID(用户正在查看的会话)
|
||||
currentConversationId: string | null;
|
||||
|
||||
// 加载状态
|
||||
isLoadingConversations: boolean;
|
||||
loadingMessagesSet: Set<string>;
|
||||
|
||||
// 初始化状态
|
||||
isInitialized: boolean;
|
||||
|
||||
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
||||
typingUsersMap: Map<string, string[]>;
|
||||
|
||||
// 当前用户的禁言状态 - 按群组ID存储
|
||||
mutedStatusMap: 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;
|
||||
|
||||
// 设置状态
|
||||
setConversations: (conversations: Map<string, ConversationResponse>) => void;
|
||||
updateConversation: (conversation: ConversationResponse) => void;
|
||||
removeConversation: (conversationId: string) => void;
|
||||
setMessages: (conversationId: string, messages: MessageResponse[]) => void;
|
||||
addMessage: (conversationId: string, message: MessageResponse) => void;
|
||||
updateMessage: (conversationId: string, messageId: string, updates: Partial<MessageResponse>) => void;
|
||||
setUnreadCount: (total: number, system: number) => 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;
|
||||
setInitialized: (initialized: boolean) => void;
|
||||
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// ==================== 初始状态 ====================
|
||||
|
||||
const initialState: MessageState = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
totalUnreadCount: 0,
|
||||
systemUnreadCount: 0,
|
||||
isWSConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoadingConversations: false,
|
||||
loadingMessagesSet: new Set(),
|
||||
isInitialized: false,
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: 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();
|
||||
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,
|
||||
isWSConnected: state.isWSConnected,
|
||||
currentConversationId: state.currentConversationId,
|
||||
isLoadingConversations: state.isLoadingConversations,
|
||||
loadingMessagesSet: state.loadingMessagesSet,
|
||||
isInitialized: state.isInitialized,
|
||||
typingUsersMap: state.typingUsersMap,
|
||||
mutedStatusMap: state.mutedStatusMap,
|
||||
};
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
setConversations: (conversations: Map<string, ConversationResponse>) => {
|
||||
const conversationList = sortConversationList(conversations);
|
||||
set({ conversations, conversationList });
|
||||
},
|
||||
|
||||
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);
|
||||
return { conversations: newConversations, conversationList };
|
||||
});
|
||||
},
|
||||
|
||||
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 };
|
||||
});
|
||||
},
|
||||
|
||||
addMessage: (conversationId: string, message: MessageResponse) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const existing = state.messagesMap.get(normalizedId) || [];
|
||||
const exists = existing.some(m => String(m.id) === String(message.id));
|
||||
|
||||
if (exists) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updated = mergeMessagesById(existing, [message]);
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.set(normalizedId, updated);
|
||||
return { messagesMap: newMessagesMap };
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage: (conversationId: string, messageId: string, updates: Partial<MessageResponse>) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const messages = state.messagesMap.get(normalizedId);
|
||||
if (!messages) return state;
|
||||
|
||||
const updated = messages.map(m =>
|
||||
String(m.id) === String(messageId) ? { ...m, ...updates } : m
|
||||
);
|
||||
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.set(normalizedId, updated);
|
||||
return { messagesMap: newMessagesMap };
|
||||
});
|
||||
},
|
||||
|
||||
setUnreadCount: (total: number, system: number) => {
|
||||
set({ totalUnreadCount: total, systemUnreadCount: system });
|
||||
},
|
||||
|
||||
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 };
|
||||
});
|
||||
},
|
||||
|
||||
setInitialized: (initialized: boolean) => {
|
||||
set({ isInitialized: initialized });
|
||||
},
|
||||
|
||||
// ==================== 重置 ====================
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
...initialState,
|
||||
conversations: new Map(),
|
||||
messagesMap: new Map(),
|
||||
loadingMessagesSet: new Set(),
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
// ==================== 订阅管理器 ====================
|
||||
|
||||
/**
|
||||
* 订阅管理器类
|
||||
* 管理消息事件的订阅和通知
|
||||
*/
|
||||
export class SubscriptionManager {
|
||||
private subscribers: Set<MessageSubscriber> = new Set();
|
||||
|
||||
subscribe(subscriber: MessageSubscriber): () => void {
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
}
|
||||
|
||||
notifySubscribers(event: MessageEvent): void {
|
||||
this.subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber(event);
|
||||
} catch (error) {
|
||||
console.error('[SubscriptionManager] 订阅者执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.subscribers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局订阅管理器实例
|
||||
export const subscriptionManager = new SubscriptionManager();
|
||||
|
||||
// ==================== 便捷方法 ====================
|
||||
|
||||
/**
|
||||
* 通知所有订阅者
|
||||
*/
|
||||
export function notifySubscribers(event: MessageEvent): void {
|
||||
subscriptionManager.notifySubscribers(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息事件
|
||||
*/
|
||||
export function subscribeToMessages(subscriber: MessageSubscriber): () => void {
|
||||
return subscriptionManager.subscribe(subscriber);
|
||||
}
|
||||
Reference in New Issue
Block a user