refactor(message): modularize MessageManager architecture
重构消息管理模块,将单体 MessageManager 拆分为职责单一的子模块: - MessageStateManager: 纯状态管理 - MessageDeduplication: 消息去重服务 - UserCacheService: 用户缓存服务 - ReadReceiptManager: 已读回执管理 - ConversationOperations: 会话操作 - MessageSendService: 消息发送 - MessageSyncService: 消息同步 - WSMessageHandler: WebSocket 消息处理 新增类型定义和常量文件,保持原有 API 向后兼容。修复 CallScreen 状态值错误,补充 PostDetailScreen 缺失样式,优化 GroupInfoPanel 导入。
This commit is contained in:
@@ -1,301 +1,260 @@
|
||||
/**
|
||||
* 消息状态管理器
|
||||
* 只负责管理状态,不包含业务逻辑
|
||||
* 纯状态管理类,不包含业务逻辑
|
||||
* 负责管理所有消息相关的内存状态
|
||||
*/
|
||||
|
||||
import type { Message, Conversation, UnreadCount } from '../../core/entities/Message';
|
||||
import type {
|
||||
ConversationResponse,
|
||||
MessageResponse,
|
||||
} from '../../types/dto';
|
||||
import type {
|
||||
MessageManagerState,
|
||||
MessageSubscriber,
|
||||
MessageEvent,
|
||||
IMessageStateManager,
|
||||
} from './types';
|
||||
|
||||
export type MessageEventType =
|
||||
| 'conversations_updated'
|
||||
| 'messages_updated'
|
||||
| 'unread_count_updated'
|
||||
| 'connection_changed'
|
||||
| 'message_sent'
|
||||
| 'message_read'
|
||||
| 'message_received'
|
||||
| 'message_recalled'
|
||||
| 'typing_status'
|
||||
| 'group_notice'
|
||||
| 'error';
|
||||
|
||||
export interface MessageEvent {
|
||||
type: MessageEventType;
|
||||
payload: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type MessageSubscriber = (event: MessageEvent) => void;
|
||||
|
||||
export interface MessageState {
|
||||
conversations: Map<string, Conversation>;
|
||||
conversationList: Conversation[];
|
||||
messagesMap: Map<string, Message[]>;
|
||||
unreadCount: UnreadCount;
|
||||
isSSEConnected: boolean;
|
||||
currentConversationId: string | null;
|
||||
isLoading: boolean;
|
||||
typingUsersMap: Map<string, string[]>;
|
||||
mutedStatusMap: Map<string, boolean>;
|
||||
}
|
||||
|
||||
export class MessageStateManager {
|
||||
private state: MessageState;
|
||||
private subscribers: Set<MessageSubscriber>;
|
||||
private readStateVersion: number = 0;
|
||||
private pendingReadMap: Map<string, { timestamp: number; version: number }> = new Map();
|
||||
export class MessageStateManager implements IMessageStateManager {
|
||||
private state: MessageManagerState;
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
unreadCount: { total: 0, system: 0 },
|
||||
isSSEConnected: false,
|
||||
totalUnreadCount: 0,
|
||||
systemUnreadCount: 0,
|
||||
isWSConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoading: false,
|
||||
isLoadingConversations: false,
|
||||
loadingMessagesSet: new Set(),
|
||||
isInitialized: false,
|
||||
subscribers: new Set(),
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
this.subscribers = new Set();
|
||||
}
|
||||
|
||||
// ==================== 状态获取 ====================
|
||||
// ==================== 获取状态 ====================
|
||||
|
||||
getState(): MessageState {
|
||||
getState(): MessageManagerState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
getConversations(): Conversation[] {
|
||||
getConversations(): ConversationResponse[] {
|
||||
return this.state.conversationList;
|
||||
}
|
||||
|
||||
getConversation(id: string): Conversation | undefined {
|
||||
return this.state.conversations.get(id);
|
||||
getConversation(conversationId: string): ConversationResponse | null {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
return this.state.conversations.get(normalizedId) || null;
|
||||
}
|
||||
|
||||
getMessages(conversationId: string): Message[] {
|
||||
return this.state.messagesMap.get(conversationId) || [];
|
||||
getMessages(conversationId: string): MessageResponse[] {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
return this.state.messagesMap.get(normalizedId) || [];
|
||||
}
|
||||
|
||||
getUnreadCount(): UnreadCount {
|
||||
return this.state.unreadCount;
|
||||
}
|
||||
|
||||
getCurrentConversationId(): string | null {
|
||||
return this.state.currentConversationId;
|
||||
getUnreadCount(): { total: number; system: number } {
|
||||
return {
|
||||
total: this.state.totalUnreadCount,
|
||||
system: this.state.systemUnreadCount,
|
||||
};
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.state.isSSEConnected;
|
||||
return this.state.isWSConnected;
|
||||
}
|
||||
|
||||
isLoading(): boolean {
|
||||
return this.state.isLoadingConversations;
|
||||
}
|
||||
|
||||
isLoadingMessages(conversationId: string): boolean {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
return this.state.loadingMessagesSet.has(normalizedId);
|
||||
}
|
||||
|
||||
getTypingUsers(groupId: string): string[] {
|
||||
return this.state.typingUsersMap.get(groupId) || [];
|
||||
}
|
||||
|
||||
getMutedStatus(groupId: string): boolean {
|
||||
isMuted(groupId: string): boolean {
|
||||
return this.state.mutedStatusMap.get(groupId) || false;
|
||||
}
|
||||
|
||||
// ==================== 状态更新 ====================
|
||||
getActiveConversation(): string | null {
|
||||
return this.state.currentConversationId;
|
||||
}
|
||||
|
||||
setConversations(conversations: Conversation[]): void {
|
||||
this.state.conversations.clear();
|
||||
this.state.conversationList = conversations;
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
setConversations(conversations: Map<string, ConversationResponse>): void {
|
||||
this.state.conversations = conversations;
|
||||
this.updateConversationList();
|
||||
}
|
||||
|
||||
updateConversation(conversation: ConversationResponse): void {
|
||||
const id = this.normalizeConversationId(conversation.id);
|
||||
this.state.conversations.set(id, { ...conversation, id });
|
||||
this.updateConversationList();
|
||||
}
|
||||
|
||||
removeConversation(conversationId: string): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
const target = this.state.conversations.get(normalizedId);
|
||||
const removedUnread = target?.unread_count || 0;
|
||||
|
||||
this.state.conversations.delete(normalizedId);
|
||||
this.state.messagesMap.delete(normalizedId);
|
||||
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - removedUnread);
|
||||
|
||||
for (const conv of conversations) {
|
||||
this.state.conversations.set(conv.id, conv);
|
||||
if (this.state.currentConversationId === normalizedId) {
|
||||
this.state.currentConversationId = null;
|
||||
}
|
||||
|
||||
this.updateUnreadCount();
|
||||
this.notify({ type: 'conversations_updated', payload: conversations });
|
||||
this.updateConversationList();
|
||||
}
|
||||
|
||||
addConversation(conversation: Conversation): void {
|
||||
this.state.conversations.set(conversation.id, conversation);
|
||||
this.state.conversationList = Array.from(this.state.conversations.values());
|
||||
this.updateUnreadCount();
|
||||
this.notify({ type: 'conversations_updated', payload: this.state.conversationList });
|
||||
setMessages(conversationId: string, messages: MessageResponse[]): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
this.state.messagesMap.set(normalizedId, messages);
|
||||
}
|
||||
|
||||
updateConversation(id: string, updates: Partial<Conversation>): void {
|
||||
const existing = this.state.conversations.get(id);
|
||||
if (existing) {
|
||||
const updated = { ...existing, ...updates };
|
||||
this.state.conversations.set(id, updated);
|
||||
this.state.conversationList = Array.from(this.state.conversations.values());
|
||||
this.updateUnreadCount();
|
||||
this.notify({ type: 'conversations_updated', payload: this.state.conversationList });
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
removeConversation(id: string): void {
|
||||
this.state.conversations.delete(id);
|
||||
this.state.messagesMap.delete(id);
|
||||
this.state.conversationList = Array.from(this.state.conversations.values());
|
||||
this.updateUnreadCount();
|
||||
this.notify({ type: 'conversations_updated', payload: this.state.conversationList });
|
||||
}
|
||||
|
||||
setMessages(conversationId: string, messages: Message[]): void {
|
||||
this.state.messagesMap.set(conversationId, messages);
|
||||
this.notify({ type: 'messages_updated', payload: { conversationId, messages } });
|
||||
}
|
||||
|
||||
addMessage(conversationId: string, message: Message): void {
|
||||
const messages = this.state.messagesMap.get(conversationId) || [];
|
||||
const existingIndex = messages.findIndex(m => m.id === message.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
messages[existingIndex] = message;
|
||||
} else {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
this.state.messagesMap.set(conversationId, messages);
|
||||
this.notify({ type: 'messages_updated', payload: { conversationId, messages } });
|
||||
}
|
||||
|
||||
prependMessages(conversationId: string, newMessages: Message[]): void {
|
||||
const existing = this.state.messagesMap.get(conversationId) || [];
|
||||
const merged = [...newMessages, ...existing];
|
||||
const unique = this.deduplicateMessages(merged);
|
||||
this.state.messagesMap.set(conversationId, unique);
|
||||
this.notify({ type: 'messages_updated', payload: { conversationId, messages: unique } });
|
||||
}
|
||||
|
||||
appendMessages(conversationId: string, newMessages: Message[]): void {
|
||||
const existing = this.state.messagesMap.get(conversationId) || [];
|
||||
const merged = [...existing, ...newMessages];
|
||||
const unique = this.deduplicateMessages(merged);
|
||||
this.state.messagesMap.set(conversationId, unique);
|
||||
this.notify({ type: 'messages_updated', payload: { conversationId, messages: unique } });
|
||||
}
|
||||
|
||||
updateMessage(conversationId: string, messageId: string, updates: Partial<Message>): void {
|
||||
const messages = this.state.messagesMap.get(conversationId);
|
||||
updateMessage(conversationId: string, messageId: string, updates: Partial<MessageResponse>): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
const messages = this.state.messagesMap.get(normalizedId);
|
||||
if (!messages) return;
|
||||
|
||||
const index = messages.findIndex(m => m.id === messageId);
|
||||
if (index >= 0) {
|
||||
messages[index] = { ...messages[index], ...updates };
|
||||
this.notify({ type: 'messages_updated', payload: { conversationId, messages } });
|
||||
}
|
||||
const updated = messages.map(m =>
|
||||
String(m.id) === String(messageId) ? { ...m, ...updates } : m
|
||||
);
|
||||
this.state.messagesMap.set(normalizedId, updated);
|
||||
}
|
||||
|
||||
setUnreadCount(count: UnreadCount): void {
|
||||
this.state.unreadCount = count;
|
||||
this.notify({ type: 'unread_count_updated', payload: count });
|
||||
setUnreadCount(total: number, system: number): void {
|
||||
this.state.totalUnreadCount = total;
|
||||
this.state.systemUnreadCount = system;
|
||||
}
|
||||
|
||||
setSSEConnected(connected: boolean): void {
|
||||
this.state.isSSEConnected = connected;
|
||||
this.notify({ type: 'connection_changed', payload: connected });
|
||||
this.state.isWSConnected = connected;
|
||||
}
|
||||
|
||||
setCurrentConversation(id: string | null): void {
|
||||
this.state.currentConversationId = id;
|
||||
setCurrentConversation(conversationId: string | null): void {
|
||||
this.state.currentConversationId = conversationId;
|
||||
}
|
||||
|
||||
setLoading(loading: boolean): void {
|
||||
this.state.isLoading = loading;
|
||||
this.state.isLoadingConversations = loading;
|
||||
}
|
||||
|
||||
setTypingUsers(groupId: string, userIds: string[]): void {
|
||||
this.state.typingUsersMap.set(groupId, userIds);
|
||||
this.notify({ type: 'typing_status', payload: { groupId, userIds } });
|
||||
setLoadingMessages(conversationId: string, loading: boolean): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
if (loading) {
|
||||
this.state.loadingMessagesSet.add(normalizedId);
|
||||
} else {
|
||||
this.state.loadingMessagesSet.delete(normalizedId);
|
||||
}
|
||||
}
|
||||
|
||||
setMutedStatus(groupId: string, muted: boolean): void {
|
||||
this.state.mutedStatusMap.set(groupId, muted);
|
||||
setTypingUsers(groupId: string, users: string[]): void {
|
||||
this.state.typingUsersMap.set(groupId, users);
|
||||
}
|
||||
|
||||
// ==================== 已读状态保护 ====================
|
||||
|
||||
beginReadOperation(conversationId: string): number {
|
||||
this.readStateVersion++;
|
||||
this.pendingReadMap.set(conversationId, {
|
||||
timestamp: Date.now(),
|
||||
version: this.readStateVersion,
|
||||
});
|
||||
return this.readStateVersion;
|
||||
setMutedStatus(groupId: string, isMuted: boolean): void {
|
||||
this.state.mutedStatusMap.set(groupId, isMuted);
|
||||
}
|
||||
|
||||
endReadOperation(conversationId: string): void {
|
||||
setTimeout(() => {
|
||||
this.pendingReadMap.delete(conversationId);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
isReadOperationPending(conversationId: string): boolean {
|
||||
const record = this.pendingReadMap.get(conversationId);
|
||||
if (!record) return false;
|
||||
return Date.now() - record.timestamp < 5000;
|
||||
}
|
||||
|
||||
getReadStateVersion(): number {
|
||||
return this.readStateVersion;
|
||||
setInitialized(initialized: boolean): void {
|
||||
this.state.isInitialized = initialized;
|
||||
}
|
||||
|
||||
// ==================== 订阅机制 ====================
|
||||
|
||||
subscribe(callback: MessageSubscriber): () => void {
|
||||
this.subscribers.add(callback);
|
||||
return () => this.subscribers.delete(callback);
|
||||
subscribe(subscriber: MessageSubscriber): () => void {
|
||||
this.state.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.state.subscribers.delete(subscriber);
|
||||
};
|
||||
}
|
||||
|
||||
private notify(event: Omit<MessageEvent, 'timestamp'>): void {
|
||||
const fullEvent: MessageEvent = {
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.subscribers.forEach(callback => {
|
||||
notifySubscribers(event: MessageEvent): void {
|
||||
this.state.subscribers.forEach(subscriber => {
|
||||
try {
|
||||
callback(fullEvent);
|
||||
subscriber(event);
|
||||
} catch (error) {
|
||||
console.error('[MessageStateManager] Subscriber error:', error);
|
||||
console.error('[MessageStateManager] 订阅者执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private updateUnreadCount(): void {
|
||||
let total = 0;
|
||||
let system = 0;
|
||||
|
||||
for (const conv of this.state.conversations.values()) {
|
||||
total += conv.unreadCount || 0;
|
||||
}
|
||||
|
||||
this.state.unreadCount = { total, system };
|
||||
this.notify({ type: 'unread_count_updated', payload: this.state.unreadCount });
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
private deduplicateMessages(messages: Message[]): Message[] {
|
||||
const seen = new Set<string>();
|
||||
return messages.filter(msg => {
|
||||
if (seen.has(msg.id)) return false;
|
||||
seen.add(msg.id);
|
||||
return true;
|
||||
/**
|
||||
* 更新会话列表排序
|
||||
* 排序规则:置顶优先,再按最后消息时间排序
|
||||
*/
|
||||
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;
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
// ==================== 重置 ====================
|
||||
|
||||
reset(): void {
|
||||
this.state = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
unreadCount: { total: 0, system: 0 },
|
||||
isSSEConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoading: false,
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
this.pendingReadMap.clear();
|
||||
this.readStateVersion = 0;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export const messageStateManager = new MessageStateManager();
|
||||
// 单例导出
|
||||
export const messageStateManager = new MessageStateManager();
|
||||
|
||||
Reference in New Issue
Block a user