refactor(message): modularize MessageManager architecture
All checks were successful
Frontend CI / ota-android (push) Successful in 11m20s
Frontend CI / build-and-push-web (push) Successful in 34m19s
Frontend CI / build-android-apk (push) Successful in 57m45s

重构消息管理模块,将单体 MessageManager 拆分为职责单一的子模块:

- MessageStateManager: 纯状态管理
- MessageDeduplication: 消息去重服务
- UserCacheService: 用户缓存服务
- ReadReceiptManager: 已读回执管理
- ConversationOperations: 会话操作
- MessageSendService: 消息发送
- MessageSyncService: 消息同步
- WSMessageHandler: WebSocket 消息处理

新增类型定义和常量文件,保持原有 API 向后兼容。修复 CallScreen 状态值错误,补充 PostDetailScreen 缺失样式,优化 GroupInfoPanel 导入。
This commit is contained in:
lafay
2026-03-31 16:13:24 +08:00
parent 2ef267a897
commit 1bee7ea551
16 changed files with 3128 additions and 3258 deletions

View File

@@ -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();