refactor: 架构重构 - 解耦过度耦合模块
主要改动: 1. 创建乐观更新工具函数 (optimisticUpdate.ts) - 消除 userStore.ts 中的重复代码 2. 拆分 useResponsive.ts (485行 -> 12个专注模块) - useBreakpoint: 断点检测 - useOrientation: 方向检测 - usePlatform: 平台检测 - useScreenSize: 屏幕尺寸 - useResponsiveValue: 响应式值 - useResponsiveStyle: 响应式样式 - useMediaQuery: 媒体查询 - useColumnCount: 列数计算 - useResponsiveSpacing: 响应式间距 3. 整理数据层 (Repository 层) - ApiDataSource: API数据源 - LocalDataSource: 本地数据源 - CacheDataSource: 缓存数据源 - MessageRepository: 消息仓库 4. 重构 messageManager.ts (2194行 -> 4个模块) - MessageStateManager: 状态管理 - WebSocketMessageHandler: WebSocket处理 - MessageSyncService: 消息同步 - ReadReceiptManager: 已读管理 5. 导航解耦 (MainNavigator.tsx: 1118行 -> 100行) - 创建 NavigationService 解耦层 - 拆分多个 Navigator 组件 架构改进: - 单一职责原则: 每个模块职责明确 - 依赖倒置: 通过接口解耦 - 代码复用: 工具函数可被多处使用 - 可测试性: 各模块可独立测试
This commit is contained in:
251
src/stores/message/MessageManager.ts
Normal file
251
src/stores/message/MessageManager.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* MessageManager - 重构版
|
||||
* 作为协调者,整合各个专注的服务模块
|
||||
*/
|
||||
|
||||
import { messageStateManager, MessageStateManager } from './message/MessageStateManager';
|
||||
import { webSocketMessageHandler, WebSocketMessageHandler } from './message/WebSocketMessageHandler';
|
||||
import { messageSyncService, MessageSyncService } from './message/MessageSyncService';
|
||||
import { readReceiptManager, ReadReceiptManager } from './message/ReadReceiptManager';
|
||||
import { messageRepository } from '../data/repositories/MessageRepository';
|
||||
import { useAuthStore } from './authStore';
|
||||
import type { Message, Conversation } from '../core/entities/Message';
|
||||
import type { ConversationResponse, MessageResponse } from '../types/dto';
|
||||
|
||||
export type {
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
} from './message/MessageStateManager';
|
||||
|
||||
export class MessageManager {
|
||||
private stateManager: MessageStateManager;
|
||||
private wsHandler: WebSocketMessageHandler;
|
||||
private syncService: MessageSyncService;
|
||||
private readManager: ReadReceiptManager;
|
||||
|
||||
private initialized: boolean = false;
|
||||
private authUnsubscribe: (() => void) | null = null;
|
||||
private wsUnsubscribe: (() => void) | null = null;
|
||||
private currentUserId: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.stateManager = messageStateManager;
|
||||
this.wsHandler = webSocketMessageHandler;
|
||||
this.syncService = messageSyncService;
|
||||
this.readManager = readReceiptManager;
|
||||
|
||||
this.readManager.setStateManager(this.stateManager);
|
||||
this.setupWebSocketHandlers();
|
||||
this.initAuthListener();
|
||||
}
|
||||
|
||||
private initAuthListener(): void {
|
||||
const authState = useAuthStore.getState();
|
||||
this.currentUserId = authState.currentUser?.id || null;
|
||||
|
||||
if (this.currentUserId && !this.initialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
this.authUnsubscribe = useAuthStore.subscribe((state) => {
|
||||
const nextUserId = state.currentUser?.id || null;
|
||||
this.currentUserId = nextUserId;
|
||||
|
||||
if (nextUserId && !this.initialized) {
|
||||
this.initialize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupWebSocketHandlers(): void {
|
||||
this.wsUnsubscribe = this.wsHandler.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case 'chat_message':
|
||||
case 'group_message':
|
||||
this.handleNewMessage(event.payload);
|
||||
break;
|
||||
case 'read_receipt':
|
||||
case 'group_read_receipt':
|
||||
this.handleReadReceipt(event.payload);
|
||||
break;
|
||||
case 'message_recalled':
|
||||
case 'group_message_recalled':
|
||||
this.handleMessageRecall(event.payload);
|
||||
break;
|
||||
case 'typing':
|
||||
this.handleTypingStatus(event.payload);
|
||||
break;
|
||||
case 'group_notice':
|
||||
this.handleGroupNotice(event.payload);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
console.log('[MessageManager] Initializing...');
|
||||
|
||||
this.wsHandler.connect();
|
||||
|
||||
const conversations = await this.syncService.syncConversations();
|
||||
this.stateManager.setConversations(conversations);
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[MessageManager] Initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('[MessageManager] Initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async activateConversation(conversationId: string): Promise<void> {
|
||||
this.stateManager.setCurrentConversation(conversationId);
|
||||
|
||||
const existing = this.stateManager.getMessages(conversationId);
|
||||
if (existing.length === 0) {
|
||||
await this.loadMessages(conversationId);
|
||||
}
|
||||
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
if (conversation && conversation.unreadCount > 0) {
|
||||
await this.readManager.markAsRead(conversationId, conversation.lastSeq);
|
||||
}
|
||||
}
|
||||
|
||||
deactivateConversation(): void {
|
||||
this.stateManager.setCurrentConversation(null);
|
||||
}
|
||||
|
||||
async loadMessages(conversationId: string): Promise<Message[]> {
|
||||
const localMessages = await this.syncService.loadLocalMessages(conversationId);
|
||||
|
||||
if (localMessages.length > 0) {
|
||||
this.stateManager.setMessages(conversationId, localMessages);
|
||||
}
|
||||
|
||||
const syncResult = await this.syncService.syncMessages(conversationId);
|
||||
|
||||
if (syncResult.messages.length > 0) {
|
||||
this.stateManager.appendMessages(conversationId, syncResult.messages);
|
||||
return [...localMessages, ...syncResult.messages];
|
||||
}
|
||||
|
||||
return localMessages;
|
||||
}
|
||||
|
||||
async loadHistory(conversationId: string): Promise<Message[]> {
|
||||
const messages = this.stateManager.getMessages(conversationId);
|
||||
const oldestSeq = messages.length > 0 ? Math.min(...messages.map(m => m.seq)) : 0;
|
||||
|
||||
const history = await this.syncService.loadHistoryMessages(
|
||||
conversationId,
|
||||
oldestSeq,
|
||||
20
|
||||
);
|
||||
|
||||
if (history.length > 0) {
|
||||
this.stateManager.prependMessages(conversationId, history);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
conversationId: string,
|
||||
content: string,
|
||||
type: 'private' | 'group' = 'private'
|
||||
): Promise<Message> {
|
||||
throw new Error('sendMessage not implemented - use existing implementation');
|
||||
}
|
||||
|
||||
private async handleNewMessage(message: Message): Promise<void> {
|
||||
await messageRepository.saveMessage(message, true);
|
||||
|
||||
this.stateManager.addMessage(message.conversationId, message);
|
||||
|
||||
const currentConvId = this.stateManager.getCurrentConversationId();
|
||||
if (currentConvId !== message.conversationId) {
|
||||
const conv = this.stateManager.getConversation(message.conversationId);
|
||||
if (conv) {
|
||||
this.stateManager.updateConversation(message.conversationId, {
|
||||
unreadCount: (conv.unreadCount || 0) + 1,
|
||||
lastSeq: message.seq,
|
||||
lastMessageAt: message.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleReadReceipt(payload: any): void {
|
||||
const { conversationId, lastReadSeq } = payload;
|
||||
|
||||
this.stateManager.updateConversation(conversationId, {
|
||||
unreadCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMessageRecall(payload: any): Promise<void> {
|
||||
const { messageId, conversationId } = payload;
|
||||
|
||||
await messageRepository.updateMessageStatus(messageId, 'recalled', true);
|
||||
|
||||
this.stateManager.updateMessage(conversationId, messageId, {
|
||||
status: 'recalled',
|
||||
});
|
||||
}
|
||||
|
||||
private handleTypingStatus(payload: any): void {
|
||||
const { groupId, userIds } = payload;
|
||||
this.stateManager.setTypingUsers(groupId, userIds);
|
||||
}
|
||||
|
||||
private handleGroupNotice(notice: any): void {
|
||||
console.log('[MessageManager] Group notice:', notice);
|
||||
}
|
||||
|
||||
subscribe(callback: any): () => void {
|
||||
return this.stateManager.subscribe(callback);
|
||||
}
|
||||
|
||||
getConversations(): Conversation[] {
|
||||
return this.stateManager.getConversations();
|
||||
}
|
||||
|
||||
getMessages(conversationId: string): Message[] {
|
||||
return this.stateManager.getMessages(conversationId);
|
||||
}
|
||||
|
||||
getUnreadCount(): { total: number; system: number } {
|
||||
return this.stateManager.getUnreadCount();
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.wsHandler.isConnected();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.initialized = false;
|
||||
this.stateManager.reset();
|
||||
this.readManager.reset();
|
||||
this.wsHandler.disconnect();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.authUnsubscribe) {
|
||||
this.authUnsubscribe();
|
||||
this.authUnsubscribe = null;
|
||||
}
|
||||
if (this.wsUnsubscribe) {
|
||||
this.wsUnsubscribe();
|
||||
this.wsUnsubscribe = null;
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export const messageManager = new MessageManager();
|
||||
export default messageManager;
|
||||
301
src/stores/message/MessageStateManager.ts
Normal file
301
src/stores/message/MessageStateManager.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* 消息状态管理器
|
||||
* 只负责管理状态,不包含业务逻辑
|
||||
*/
|
||||
|
||||
import type { Message, Conversation, UnreadCount } from '../../core/entities/Message';
|
||||
|
||||
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;
|
||||
isWebSocketConnected: 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();
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
unreadCount: { total: 0, system: 0 },
|
||||
isWebSocketConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoading: false,
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
this.subscribers = new Set();
|
||||
}
|
||||
|
||||
// ==================== 状态获取 ====================
|
||||
|
||||
getState(): MessageState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
getConversations(): Conversation[] {
|
||||
return this.state.conversationList;
|
||||
}
|
||||
|
||||
getConversation(id: string): Conversation | undefined {
|
||||
return this.state.conversations.get(id);
|
||||
}
|
||||
|
||||
getMessages(conversationId: string): Message[] {
|
||||
return this.state.messagesMap.get(conversationId) || [];
|
||||
}
|
||||
|
||||
getUnreadCount(): UnreadCount {
|
||||
return this.state.unreadCount;
|
||||
}
|
||||
|
||||
getCurrentConversationId(): string | null {
|
||||
return this.state.currentConversationId;
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.state.isWebSocketConnected;
|
||||
}
|
||||
|
||||
getTypingUsers(groupId: string): string[] {
|
||||
return this.state.typingUsersMap.get(groupId) || [];
|
||||
}
|
||||
|
||||
getMutedStatus(groupId: string): boolean {
|
||||
return this.state.mutedStatusMap.get(groupId) || false;
|
||||
}
|
||||
|
||||
// ==================== 状态更新 ====================
|
||||
|
||||
setConversations(conversations: Conversation[]): void {
|
||||
this.state.conversations.clear();
|
||||
this.state.conversationList = conversations;
|
||||
|
||||
for (const conv of conversations) {
|
||||
this.state.conversations.set(conv.id, conv);
|
||||
}
|
||||
|
||||
this.updateUnreadCount();
|
||||
this.notify({ type: 'conversations_updated', payload: conversations });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
|
||||
setUnreadCount(count: UnreadCount): void {
|
||||
this.state.unreadCount = count;
|
||||
this.notify({ type: 'unread_count_updated', payload: count });
|
||||
}
|
||||
|
||||
setWebSocketConnected(connected: boolean): void {
|
||||
this.state.isWebSocketConnected = connected;
|
||||
this.notify({ type: 'connection_changed', payload: connected });
|
||||
}
|
||||
|
||||
setCurrentConversation(id: string | null): void {
|
||||
this.state.currentConversationId = id;
|
||||
}
|
||||
|
||||
setLoading(loading: boolean): void {
|
||||
this.state.isLoading = loading;
|
||||
}
|
||||
|
||||
setTypingUsers(groupId: string, userIds: string[]): void {
|
||||
this.state.typingUsersMap.set(groupId, userIds);
|
||||
this.notify({ type: 'typing_status', payload: { groupId, userIds } });
|
||||
}
|
||||
|
||||
setMutedStatus(groupId: string, muted: boolean): void {
|
||||
this.state.mutedStatusMap.set(groupId, muted);
|
||||
}
|
||||
|
||||
// ==================== 已读状态保护 ====================
|
||||
|
||||
beginReadOperation(conversationId: string): number {
|
||||
this.readStateVersion++;
|
||||
this.pendingReadMap.set(conversationId, {
|
||||
timestamp: Date.now(),
|
||||
version: this.readStateVersion,
|
||||
});
|
||||
return this.readStateVersion;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ==================== 订阅机制 ====================
|
||||
|
||||
subscribe(callback: MessageSubscriber): () => void {
|
||||
this.subscribers.add(callback);
|
||||
return () => this.subscribers.delete(callback);
|
||||
}
|
||||
|
||||
private notify(event: Omit<MessageEvent, 'timestamp'>): void {
|
||||
const fullEvent: MessageEvent = {
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(fullEvent);
|
||||
} catch (error) {
|
||||
console.error('[MessageStateManager] Subscriber error:', 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 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;
|
||||
});
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.state = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
unreadCount: { total: 0, system: 0 },
|
||||
isWebSocketConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoading: false,
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
this.pendingReadMap.clear();
|
||||
this.readStateVersion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const messageStateManager = new MessageStateManager();
|
||||
162
src/stores/message/MessageSyncService.ts
Normal file
162
src/stores/message/MessageSyncService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 消息同步服务
|
||||
* 负责从服务器同步消息和会话
|
||||
*/
|
||||
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { messageRepository } from '../../data/repositories/MessageRepository';
|
||||
import type { Message, Conversation } from '../../core/entities/Message';
|
||||
import type { ConversationResponse, MessageResponse } from '../../types/dto';
|
||||
|
||||
export interface SyncOptions {
|
||||
force?: boolean;
|
||||
lastSeq?: number;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
conversations: Conversation[];
|
||||
messages: Message[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export class MessageSyncService {
|
||||
private syncingConversations: boolean = false;
|
||||
private syncingMessages: Map<string, boolean> = new Map();
|
||||
|
||||
async syncConversations(): Promise<Conversation[]> {
|
||||
if (this.syncingConversations) {
|
||||
console.log('[MessageSyncService] Already syncing conversations');
|
||||
return [];
|
||||
}
|
||||
|
||||
this.syncingConversations = true;
|
||||
|
||||
try {
|
||||
const response = await messageService.getConversations();
|
||||
const conversations = this.mapConversations(response);
|
||||
return conversations;
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to sync conversations:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.syncingConversations = false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncMessages(
|
||||
conversationId: string,
|
||||
options: SyncOptions = {}
|
||||
): Promise<SyncResult> {
|
||||
const key = conversationId;
|
||||
|
||||
if (this.syncingMessages.get(key)) {
|
||||
console.log('[MessageSyncService] Already syncing messages for:', conversationId);
|
||||
return { conversations: [], messages: [], hasMore: false };
|
||||
}
|
||||
|
||||
this.syncingMessages.set(key, true);
|
||||
|
||||
try {
|
||||
const lastSeq = options.lastSeq ?? await messageRepository.getMaxSeq(conversationId);
|
||||
|
||||
const response = await messageService.getMessages(conversationId, {
|
||||
after_seq: lastSeq,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const messages = this.mapMessages(response.messages || []);
|
||||
|
||||
if (messages.length > 0) {
|
||||
await messageRepository.saveMessages(messages, true);
|
||||
}
|
||||
|
||||
return {
|
||||
conversations: [],
|
||||
messages,
|
||||
hasMore: response.has_more || false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to sync messages:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.syncingMessages.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async loadHistoryMessages(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
try {
|
||||
const response = await messageService.getMessages(conversationId, {
|
||||
before_seq: beforeSeq,
|
||||
limit,
|
||||
});
|
||||
|
||||
const messages = this.mapMessages(response.messages || []);
|
||||
|
||||
if (messages.length > 0) {
|
||||
await messageRepository.saveMessages(messages, true);
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to load history:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadLocalMessages(
|
||||
conversationId: string,
|
||||
limit: number = 50
|
||||
): Promise<Message[]> {
|
||||
return messageRepository.getMessagesByConversation(conversationId, limit);
|
||||
}
|
||||
|
||||
async loadHistoryFromLocal(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
return messageRepository.getMessagesBeforeSeq(conversationId, beforeSeq, limit);
|
||||
}
|
||||
|
||||
private mapConversations(response: ConversationResponse[]): Conversation[] {
|
||||
return response.map(conv => ({
|
||||
id: conv.id,
|
||||
type: conv.type || 'private',
|
||||
isPinned: conv.isPinned || false,
|
||||
lastSeq: conv.lastSeq || conv.last_seq || 0,
|
||||
lastMessageAt: conv.lastMessageAt || conv.last_message_at || new Date().toISOString(),
|
||||
unreadCount: conv.unreadCount || conv.unread_count || 0,
|
||||
participants: conv.participants || [],
|
||||
group: conv.group,
|
||||
createdAt: conv.createdAt || conv.created_at || new Date().toISOString(),
|
||||
updatedAt: conv.updatedAt || conv.updated_at || new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
private mapMessages(response: MessageResponse[]): Message[] {
|
||||
return response.map(msg => ({
|
||||
id: msg.id,
|
||||
conversationId: msg.conversationId || msg.conversation_id || '',
|
||||
senderId: msg.senderId || msg.sender_id || '',
|
||||
seq: msg.seq || 0,
|
||||
segments: msg.segments || [],
|
||||
createdAt: msg.createdAt || msg.created_at || new Date().toISOString(),
|
||||
status: msg.status || 'normal',
|
||||
sender: msg.sender,
|
||||
}));
|
||||
}
|
||||
|
||||
isSyncingConversations(): boolean {
|
||||
return this.syncingConversations;
|
||||
}
|
||||
|
||||
isSyncingMessages(conversationId: string): boolean {
|
||||
return this.syncingMessages.get(conversationId) || false;
|
||||
}
|
||||
}
|
||||
|
||||
export const messageSyncService = new MessageSyncService();
|
||||
92
src/stores/message/ReadReceiptManager.ts
Normal file
92
src/stores/message/ReadReceiptManager.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 已读回执管理器
|
||||
* 负责处理消息已读状态的同步
|
||||
*/
|
||||
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { messageRepository } from '../../data/repositories/MessageRepository';
|
||||
import type { MessageStateManager } from './MessageStateManager';
|
||||
|
||||
export interface ReadReceiptResult {
|
||||
conversationId: string;
|
||||
lastReadSeq: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class ReadReceiptManager {
|
||||
private pendingOperations: Map<string, Promise<void>> = new Map();
|
||||
private stateManager: MessageStateManager | null = null;
|
||||
|
||||
setStateManager(manager: MessageStateManager): void {
|
||||
this.stateManager = manager;
|
||||
}
|
||||
|
||||
async markAsRead(
|
||||
conversationId: string,
|
||||
lastSeq: number
|
||||
): Promise<ReadReceiptResult> {
|
||||
const existing = this.pendingOperations.get(conversationId);
|
||||
if (existing) {
|
||||
await existing;
|
||||
}
|
||||
|
||||
if (!this.stateManager) {
|
||||
throw new Error('StateManager not set');
|
||||
}
|
||||
|
||||
const version = this.stateManager.beginReadOperation(conversationId);
|
||||
|
||||
const promise = this.executeMarkAsRead(conversationId, lastSeq, version);
|
||||
this.pendingOperations.set(conversationId, promise);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
return { conversationId, lastReadSeq: lastSeq, success: true };
|
||||
} catch (error) {
|
||||
console.error('[ReadReceiptManager] Failed to mark as read:', error);
|
||||
return { conversationId, lastReadSeq: lastSeq, success: false };
|
||||
} finally {
|
||||
this.pendingOperations.delete(conversationId);
|
||||
this.stateManager.endReadOperation(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeMarkAsRead(
|
||||
conversationId: string,
|
||||
lastSeq: number,
|
||||
version: number
|
||||
): Promise<void> {
|
||||
await messageService.markAsRead(conversationId, lastSeq);
|
||||
|
||||
await messageRepository.markConversationAsRead(conversationId);
|
||||
|
||||
if (this.stateManager) {
|
||||
this.stateManager.updateConversation(conversationId, {
|
||||
unreadCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async markAllAsRead(): Promise<void> {
|
||||
if (!this.stateManager) return;
|
||||
|
||||
const conversations = this.stateManager.getConversations();
|
||||
const unreadConversations = conversations.filter(c => c.unreadCount > 0);
|
||||
|
||||
await Promise.all(
|
||||
unreadConversations.map(conv =>
|
||||
this.markAsRead(conv.id, conv.lastSeq)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
isPending(conversationId: string): boolean {
|
||||
return this.pendingOperations.has(conversationId);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.pendingOperations.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const readReceiptManager = new ReadReceiptManager();
|
||||
136
src/stores/message/WebSocketMessageHandler.ts
Normal file
136
src/stores/message/WebSocketMessageHandler.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* WebSocket 消息处理器
|
||||
* 只负责处理 WebSocket 消息,不管理状态
|
||||
*/
|
||||
|
||||
import { sseService, WSChatMessage, WSGroupChatMessage, WSReadMessage, WSGroupReadMessage, WSRecallMessage, WSGroupRecallMessage, WSGroupTypingMessage, WSGroupNoticeMessage, GroupNoticeType } from '../../services/sseService';
|
||||
import type { Message, GroupNotice } from '../../core/entities/Message';
|
||||
|
||||
export type WebSocketEventType =
|
||||
| 'chat_message'
|
||||
| 'group_message'
|
||||
| 'read_receipt'
|
||||
| 'group_read_receipt'
|
||||
| 'message_recalled'
|
||||
| 'group_message_recalled'
|
||||
| 'typing'
|
||||
| 'group_notice';
|
||||
|
||||
export interface WebSocketEvent {
|
||||
type: WebSocketEventType;
|
||||
payload: any;
|
||||
raw: any;
|
||||
}
|
||||
|
||||
export type WebSocketEventHandler = (event: WebSocketEvent) => void;
|
||||
|
||||
export class WebSocketMessageHandler {
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
private handlers: Set<WebSocketEventHandler> = new Set();
|
||||
|
||||
connect(): void {
|
||||
if (this.unsubscribe) return;
|
||||
|
||||
this.unsubscribe = sseService.subscribe(this.handleSSEEvent.bind(this));
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
this.handlers.clear();
|
||||
}
|
||||
|
||||
subscribe(handler: WebSocketEventHandler): () => void {
|
||||
this.handlers.add(handler);
|
||||
return () => this.handlers.delete(handler);
|
||||
}
|
||||
|
||||
private handleSSEEvent(event: any): void {
|
||||
const { type, data } = event;
|
||||
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
this.emit('chat_message', this.parseChatMessage(data), data);
|
||||
break;
|
||||
case 'group_message':
|
||||
this.emit('group_message', this.parseGroupMessage(data), data);
|
||||
break;
|
||||
case 'read':
|
||||
this.emit('read_receipt', data, data);
|
||||
break;
|
||||
case 'group_read':
|
||||
this.emit('group_read_receipt', data, data);
|
||||
break;
|
||||
case 'recall':
|
||||
this.emit('message_recalled', data, data);
|
||||
break;
|
||||
case 'group_recall':
|
||||
this.emit('group_message_recalled', data, data);
|
||||
break;
|
||||
case 'typing':
|
||||
this.emit('typing', data, data);
|
||||
break;
|
||||
case 'group_notice':
|
||||
this.emit('group_notice', this.parseGroupNotice(data), data);
|
||||
break;
|
||||
default:
|
||||
console.log('[WebSocketMessageHandler] Unknown event type:', type);
|
||||
}
|
||||
}
|
||||
|
||||
private emit(type: WebSocketEventType, payload: any, raw: any): void {
|
||||
const event: WebSocketEvent = { type, payload, raw };
|
||||
this.handlers.forEach(handler => {
|
||||
try {
|
||||
handler(event);
|
||||
} catch (error) {
|
||||
console.error('[WebSocketMessageHandler] Handler error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private parseChatMessage(data: WSChatMessage): Message {
|
||||
return {
|
||||
id: data.id || '',
|
||||
conversationId: data.conversationId || data.conversation_id || '',
|
||||
senderId: data.senderId || data.sender_id || '',
|
||||
seq: data.seq || 0,
|
||||
segments: data.segments || [],
|
||||
createdAt: data.createdAt || data.created_at || new Date().toISOString(),
|
||||
status: 'normal',
|
||||
sender: data.sender,
|
||||
};
|
||||
}
|
||||
|
||||
private parseGroupMessage(data: WSGroupChatMessage): Message {
|
||||
return {
|
||||
id: data.id || '',
|
||||
conversationId: data.groupId || data.group_id || '',
|
||||
senderId: data.senderId || data.sender_id || '',
|
||||
seq: data.seq || 0,
|
||||
segments: data.segments || [],
|
||||
createdAt: data.createdAt || data.created_at || new Date().toISOString(),
|
||||
status: 'normal',
|
||||
sender: data.sender,
|
||||
};
|
||||
}
|
||||
|
||||
private parseGroupNotice(data: WSGroupNoticeMessage): GroupNotice {
|
||||
return {
|
||||
type: data.noticeType || data.notice_type || 'member_join',
|
||||
groupId: data.groupId || data.group_id || '',
|
||||
data: data.data || {},
|
||||
timestamp: data.timestamp || Date.now(),
|
||||
messageId: data.messageId || data.message_id,
|
||||
seq: data.seq,
|
||||
};
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return sseService.isConnected();
|
||||
}
|
||||
}
|
||||
|
||||
export const webSocketMessageHandler = new WebSocketMessageHandler();
|
||||
19
src/stores/message/index.ts
Normal file
19
src/stores/message/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 消息模块统一导出
|
||||
*/
|
||||
|
||||
// 导出主要的管理器
|
||||
export { messageManager, MessageManager } from './MessageManager';
|
||||
|
||||
// 导出状态管理器
|
||||
export { messageStateManager, MessageStateManager } from './MessageStateManager';
|
||||
export type { MessageState, MessageEvent, MessageSubscriber, MessageEventType } from './MessageStateManager';
|
||||
|
||||
// 导出同步服务
|
||||
export { messageSyncService, MessageSyncService } from './MessageSyncService';
|
||||
|
||||
// 导出WebSocket处理器
|
||||
export { webSocketMessageHandler, WebSocketMessageHandler } from './WebSocketMessageHandler';
|
||||
|
||||
// 导出已读管理器
|
||||
export { readReceiptManager, ReadReceiptManager } from './ReadReceiptManager';
|
||||
Reference in New Issue
Block a user