2026-03-18 12:11:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 消息状态管理器
|
2026-03-31 16:13:24 +08:00
|
|
|
|
* 纯状态管理类,不包含业务逻辑
|
|
|
|
|
|
* 负责管理所有消息相关的内存状态
|
2026-03-18 12:11:49 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
ConversationResponse,
|
|
|
|
|
|
MessageResponse,
|
|
|
|
|
|
} from '../../types/dto';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
MessageManagerState,
|
|
|
|
|
|
MessageSubscriber,
|
|
|
|
|
|
MessageEvent,
|
|
|
|
|
|
IMessageStateManager,
|
|
|
|
|
|
} from './types';
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
export class MessageStateManager implements IMessageStateManager {
|
|
|
|
|
|
private state: MessageManagerState;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.state = {
|
|
|
|
|
|
conversations: new Map(),
|
|
|
|
|
|
conversationList: [],
|
|
|
|
|
|
messagesMap: new Map(),
|
2026-03-31 16:13:24 +08:00
|
|
|
|
totalUnreadCount: 0,
|
|
|
|
|
|
systemUnreadCount: 0,
|
|
|
|
|
|
isWSConnected: false,
|
2026-03-18 12:11:49 +08:00
|
|
|
|
currentConversationId: null,
|
2026-03-31 16:13:24 +08:00
|
|
|
|
isLoadingConversations: false,
|
|
|
|
|
|
loadingMessagesSet: new Set(),
|
|
|
|
|
|
isInitialized: false,
|
|
|
|
|
|
subscribers: new Set(),
|
2026-03-18 12:11:49 +08:00
|
|
|
|
typingUsersMap: new Map(),
|
|
|
|
|
|
mutedStatusMap: new Map(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// ==================== 获取状态 ====================
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getState(): MessageManagerState {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
return this.state;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getConversations(): ConversationResponse[] {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
return this.state.conversationList;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getConversation(conversationId: string): ConversationResponse | null {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
return this.state.conversations.get(normalizedId) || null;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getMessages(conversationId: string): MessageResponse[] {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
return this.state.messagesMap.get(normalizedId) || [];
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getUnreadCount(): { total: number; system: number } {
|
|
|
|
|
|
return {
|
|
|
|
|
|
total: this.state.totalUnreadCount,
|
|
|
|
|
|
system: this.state.systemUnreadCount,
|
|
|
|
|
|
};
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
isConnected(): boolean {
|
|
|
|
|
|
return this.state.isWSConnected;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
isLoading(): boolean {
|
|
|
|
|
|
return this.state.isLoadingConversations;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isLoadingMessages(conversationId: string): boolean {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
return this.state.loadingMessagesSet.has(normalizedId);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getTypingUsers(groupId: string): string[] {
|
|
|
|
|
|
return this.state.typingUsersMap.get(groupId) || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
isMuted(groupId: string): boolean {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
return this.state.mutedStatusMap.get(groupId) || false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
getActiveConversation(): string | null {
|
|
|
|
|
|
return this.state.currentConversationId;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// ==================== 设置状态 ====================
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setConversations(conversations: Map<string, ConversationResponse>): void {
|
|
|
|
|
|
this.state.conversations = conversations;
|
|
|
|
|
|
this.updateConversationList();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
updateConversation(conversation: ConversationResponse): void {
|
|
|
|
|
|
const id = this.normalizeConversationId(conversation.id);
|
|
|
|
|
|
this.state.conversations.set(id, { ...conversation, id });
|
|
|
|
|
|
this.updateConversationList();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
removeConversation(conversationId: string): void {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
const target = this.state.conversations.get(normalizedId);
|
|
|
|
|
|
const removedUnread = target?.unread_count || 0;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.state.conversations.delete(normalizedId);
|
|
|
|
|
|
this.state.messagesMap.delete(normalizedId);
|
|
|
|
|
|
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - removedUnread);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
if (this.state.currentConversationId === normalizedId) {
|
|
|
|
|
|
this.state.currentConversationId = null;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.updateConversationList();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setMessages(conversationId: string, messages: MessageResponse[]): void {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
this.state.messagesMap.set(normalizedId, messages);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
addMessage(conversationId: string, message: MessageResponse): void {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
const existing = this.state.messagesMap.get(normalizedId) || [];
|
|
|
|
|
|
const exists = existing.some(m => String(m.id) === String(message.id));
|
|
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
|
const updated = this.mergeMessagesById(existing, [message]);
|
|
|
|
|
|
this.state.messagesMap.set(normalizedId, updated);
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
updateMessage(conversationId: string, messageId: string, updates: Partial<MessageResponse>): void {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
const messages = this.state.messagesMap.get(normalizedId);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
if (!messages) return;
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const updated = messages.map(m =>
|
|
|
|
|
|
String(m.id) === String(messageId) ? { ...m, ...updates } : m
|
|
|
|
|
|
);
|
|
|
|
|
|
this.state.messagesMap.set(normalizedId, updated);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setUnreadCount(total: number, system: number): void {
|
|
|
|
|
|
this.state.totalUnreadCount = total;
|
|
|
|
|
|
this.state.systemUnreadCount = system;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 00:56:41 +08:00
|
|
|
|
setSSEConnected(connected: boolean): void {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.state.isWSConnected = connected;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setCurrentConversation(conversationId: string | null): void {
|
|
|
|
|
|
this.state.currentConversationId = conversationId;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(loading: boolean): void {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.state.isLoadingConversations = loading;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setLoadingMessages(conversationId: string, loading: boolean): void {
|
|
|
|
|
|
const normalizedId = this.normalizeConversationId(conversationId);
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
this.state.loadingMessagesSet.add(normalizedId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.state.loadingMessagesSet.delete(normalizedId);
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setTypingUsers(groupId: string, users: string[]): void {
|
|
|
|
|
|
this.state.typingUsersMap.set(groupId, users);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setMutedStatus(groupId: string, isMuted: boolean): void {
|
|
|
|
|
|
this.state.mutedStatusMap.set(groupId, isMuted);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
setInitialized(initialized: boolean): void {
|
|
|
|
|
|
this.state.isInitialized = initialized;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 订阅机制 ====================
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
subscribe(subscriber: MessageSubscriber): () => void {
|
|
|
|
|
|
this.state.subscribers.add(subscriber);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
this.state.subscribers.delete(subscriber);
|
|
|
|
|
|
};
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
notifySubscribers(event: MessageEvent): void {
|
|
|
|
|
|
this.state.subscribers.forEach(subscriber => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
try {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
subscriber(event);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} catch (error) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageStateManager] 订阅者执行失败:', error);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 工具方法 ====================
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
|
|
|
|
|
return conversationId == null ? '' : String(conversationId);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 更新会话列表排序
|
|
|
|
|
|
* 排序规则:置顶优先,再按最后消息时间排序
|
|
|
|
|
|
*/
|
|
|
|
|
|
private updateConversationList(): void {
|
|
|
|
|
|
const list = Array.from(this.state.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;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
});
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.state.conversationList = list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按 message_id 合并消息并按 seq 排序
|
|
|
|
|
|
* 后者覆盖前者,可用来吸收更完整的同 ID 消息体
|
|
|
|
|
|
*/
|
|
|
|
|
|
private 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);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// ==================== 重置 ====================
|
|
|
|
|
|
|
2026-03-18 12:11:49 +08:00
|
|
|
|
reset(): void {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.state.conversations.clear();
|
|
|
|
|
|
this.state.conversationList = [];
|
|
|
|
|
|
this.state.messagesMap.clear();
|
|
|
|
|
|
this.state.totalUnreadCount = 0;
|
|
|
|
|
|
this.state.systemUnreadCount = 0;
|
|
|
|
|
|
this.state.isWSConnected = false;
|
|
|
|
|
|
this.state.currentConversationId = null;
|
|
|
|
|
|
this.state.isLoadingConversations = false;
|
|
|
|
|
|
this.state.loadingMessagesSet.clear();
|
|
|
|
|
|
this.state.isInitialized = false;
|
|
|
|
|
|
this.state.typingUsersMap.clear();
|
|
|
|
|
|
this.state.mutedStatusMap.clear();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// 单例导出
|
|
|
|
|
|
export const messageStateManager = new MessageStateManager();
|