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:
lafay
2026-03-18 12:11:49 +08:00
parent 1ffbb63753
commit a6cdb97e24
70 changed files with 8175 additions and 1728 deletions

View 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;

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

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

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

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

View 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';

View File

@@ -15,6 +15,7 @@ import {
} from '../services';
import { userManager } from './userManager';
import { messageManager } from './messageManager';
import { createOptimisticUpdaterFactory } from '../utils/optimisticUpdate';
interface UserState {
// 状态
@@ -59,504 +60,386 @@ interface UserState {
refreshAll: () => Promise<void>;
}
export const useUserStore = create<UserState>((set, get) => ({
// 初始状态
users: [],
userCache: {},
posts: [],
notifications: [],
notificationBadge: {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
},
messageUnreadCount: 0,
searchHistory: [],
isLoadingPosts: false,
isLoadingNotifications: false,
// 获取用户信息
fetchUser: async (userId: string) => {
// 先检查缓存
const cached = get().userCache[userId];
if (cached) return cached;
export const useUserStore = create<UserState>((set, get) => {
// 创建乐观更新器
const optimisticUpdater = createOptimisticUpdaterFactory<UserState>(set, get);
return {
// 初始状态
users: [],
userCache: {},
posts: [],
notifications: [],
notificationBadge: {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
},
messageUnreadCount: 0,
searchHistory: [],
isLoadingPosts: false,
isLoadingNotifications: false,
try {
const user = await userManager.getUserById(userId);
if (user) {
// 获取用户信息
fetchUser: async (userId: string) => {
// 先检查缓存
const cached = get().userCache[userId];
if (cached) return cached;
try {
const user = await userManager.getUserById(userId);
if (user) {
set(state => ({
userCache: { ...state.userCache, [userId]: user }
}));
return user;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
return undefined;
},
// 获取用户帖子
fetchUserPosts: async (userId: string, page = 1) => {
try {
const response = await postService.getUserPosts(userId, page);
const newPosts = response.list;
set(state => ({
userCache: { ...state.userCache, [userId]: user }
posts: page === 1 ? newPosts : [...state.posts, ...newPosts]
}));
return user;
return newPosts;
} catch (error) {
console.error('获取用户帖子失败:', error);
return [];
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
return undefined;
},
// 获取用户帖子
fetchUserPosts: async (userId: string, page = 1) => {
try {
const response = await postService.getUserPosts(userId, page);
const newPosts = response.list;
set(state => ({
posts: page === 1 ? newPosts : [...state.posts, ...newPosts]
}));
return newPosts;
} catch (error) {
console.error('获取用户帖子失败:', error);
return [];
}
},
// 获取帖子列表(首页)
fetchPosts: async (type: 'recommend' | 'follow' | 'hot' | 'latest', page = 1) => {
set({ isLoadingPosts: true });
},
try {
let response;
// 获取帖子列表(首页)
fetchPosts: async (type: 'recommend' | 'follow' | 'hot' | 'latest', page = 1) => {
set({ isLoadingPosts: true });
switch (type) {
case 'recommend':
response = await postService.getRecommendedPosts(page);
break;
case 'follow':
response = await postService.getFollowingPosts(page);
break;
case 'hot':
response = await postService.getHotPosts(page);
break;
case 'latest':
default:
response = await postService.getLatestPosts(page);
break;
try {
let response;
switch (type) {
case 'recommend':
response = await postService.getRecommendedPosts(page);
break;
case 'follow':
response = await postService.getFollowingPosts(page);
break;
case 'hot':
response = await postService.getHotPosts(page);
break;
case 'latest':
default:
response = await postService.getLatestPosts(page);
break;
}
const newPosts = response.list;
set(state => ({
posts: page === 1 ? newPosts : [...state.posts, ...newPosts],
isLoadingPosts: false
}));
return response;
} catch (error) {
console.error('获取帖子列表失败:', error);
set({ isLoadingPosts: false });
return {
list: [],
total: 0,
page,
page_size: 20,
total_pages: 0,
};
}
const newPosts = response.list;
set(state => ({
posts: page === 1 ? newPosts : [...state.posts, ...newPosts],
isLoadingPosts: false
}));
return response;
} catch (error) {
console.error('获取帖子列表失败:', error);
set({ isLoadingPosts: false });
return {
list: [],
total: 0,
page,
page_size: 20,
total_pages: 0,
};
}
},
// 获取通知列表
fetchNotifications: async (type?: string, page = 1) => {
set({ isLoadingNotifications: true });
},
try {
const response = await notificationService.getNotifications(
page,
20,
type as any
);
// 获取通知列表
fetchNotifications: async (type?: string, page = 1) => {
set({ isLoadingNotifications: true });
const newNotifications = response.list;
set(state => ({
notifications: page === 1 ? newNotifications : [...state.notifications, ...newNotifications],
isLoadingNotifications: false
}));
return newNotifications;
} catch (error) {
console.error('获取通知列表失败:', error);
set({ isLoadingNotifications: false });
return [];
}
},
// 标记通知为已读
markNotificationAsRead: async (notificationId: string) => {
try {
await notificationService.markAsRead(notificationId);
set(state => {
const notifications = state.notifications.map(n =>
n.id === notificationId ? { ...n, isRead: true } : n
try {
const response = await notificationService.getNotifications(
page,
20,
type as any
);
return {
notifications,
const newNotifications = response.list;
set(state => ({
notifications: page === 1 ? newNotifications : [...state.notifications, ...newNotifications],
isLoadingNotifications: false
}));
return newNotifications;
} catch (error) {
console.error('获取通知列表失败:', error);
set({ isLoadingNotifications: false });
return [];
}
},
// 标记通知为已读
markNotificationAsRead: async (notificationId: string) => {
try {
await notificationService.markAsRead(notificationId);
set(state => {
const notifications = state.notifications.map(n =>
n.id === notificationId ? { ...n, isRead: true } : n
);
return {
notifications,
notificationBadge: {
total: notifications.filter(n => !n.isRead).length,
likes: notifications.filter(n => n.type === 'like_post' && !n.isRead).length,
comments: notifications.filter(n => n.type === 'comment' && !n.isRead).length,
follows: notifications.filter(n => n.type === 'follow' && !n.isRead).length,
system: notifications.filter(n => n.type === 'system' && !n.isRead).length,
}
};
});
} catch (error) {
console.error('标记通知为已读失败:', error);
}
},
// 标记所有通知为已读
markAllNotificationsAsRead: async () => {
try {
await notificationService.markAllAsRead();
set(state => ({
notifications: state.notifications.map(n => ({ ...n, isRead: true })),
notificationBadge: {
total: notifications.filter(n => !n.isRead).length,
likes: notifications.filter(n => n.type === 'like_post' && !n.isRead).length,
comments: notifications.filter(n => n.type === 'comment' && !n.isRead).length,
follows: notifications.filter(n => n.type === 'follow' && !n.isRead).length,
system: notifications.filter(n => n.type === 'system' && !n.isRead).length,
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
}
};
}));
} catch (error) {
console.error('标记所有通知为已读失败:', error);
}
},
// 获取通知角标
fetchNotificationBadge: async () => {
try {
const badge = await notificationService.getNotificationBadge();
set({ notificationBadge: badge });
} catch (error) {
console.error('获取通知角标失败:', error);
}
},
// 设置消息未读数
// 注意:未读数现在由 MessageManager 统一管理
// 此方法保留用于 TabBar 角标显示,从 MessageManager 同步
setMessageUnreadCount: (count: number) => {
set({ messageUnreadCount: count });
},
// 从后端拉取最新消息未读数
// @deprecated 请使用 messageManager.fetchUnreadCount() 代替
// 此方法保留用于向后兼容
fetchMessageUnreadCount: async () => {
try {
await messageManager.fetchUnreadCount();
const unread = messageManager.getUnreadCount();
set({ messageUnreadCount: unread.total + unread.system });
} catch (error) {
console.error('获取消息未读数失败:', error);
}
},
// 添加搜索历史
addSearchHistory: (keyword: string) => {
set(state => {
const history = [keyword, ...state.searchHistory.filter(k => k !== keyword)].slice(0, 20);
return { searchHistory: history };
});
} catch (error) {
console.error('标记通知为已读失败:', error);
}
},
// 标记所有通知为已读
markAllNotificationsAsRead: async () => {
try {
await notificationService.markAllAsRead();
},
// 清除搜索历史
clearSearchHistory: () => {
set({ searchHistory: [] });
},
// 点赞帖子 - 使用乐观更新工具
likePost: async (postId: string) => {
const originalPosts = get().posts;
set(state => ({
notifications: state.notifications.map(n => ({ ...n, isRead: true })),
notificationBadge: {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
}
}));
} catch (error) {
console.error('标记所有通知为已读失败:', error);
}
},
// 获取通知角标
fetchNotificationBadge: async () => {
try {
const badge = await notificationService.getNotificationBadge();
set({ notificationBadge: badge });
} catch (error) {
console.error('获取通知角标失败:', error);
}
},
// 设置消息未读数
// 注意:未读数现在由 MessageManager 统一管理
// 此方法保留用于 TabBar 角标显示,从 MessageManager 同步
setMessageUnreadCount: (count: number) => {
set({ messageUnreadCount: count });
},
// 从后端拉取最新消息未读数
// @deprecated 请使用 messageManager.fetchUnreadCount() 代替
// 此方法保留用于向后兼容
fetchMessageUnreadCount: async () => {
try {
await messageManager.fetchUnreadCount();
const unread = messageManager.getUnreadCount();
set({ messageUnreadCount: unread.total + unread.system });
} catch (error) {
console.error('获取消息未读数失败:', error);
}
},
// 添加搜索历史
addSearchHistory: (keyword: string) => {
set(state => {
const history = [keyword, ...state.searchHistory.filter(k => k !== keyword)].slice(0, 20);
return { searchHistory: history };
});
},
// 清除搜索历史
clearSearchHistory: () => {
set({ searchHistory: [] });
},
// 点赞帖子 - 乐观更新
likePost: async (postId: string) => {
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
// 调用API
try {
const updatedPost = await postService.likePost(postId);
if (updatedPost) {
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
}
} catch (error) {
console.error('点赞帖子失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
}
},
// 取消点赞 - 乐观更新
unlikePost: async (postId: string) => {
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
// 调用API
try {
const updatedPost = await postService.unlikePost(postId);
if (updatedPost) {
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
}
} catch (error) {
console.error('取消点赞帖子失败:', error);
// 失败回滚状态
// 乐观更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
)
}));
}
},
// 收藏帖子 - 乐观更新
favoritePost: async (postId: string) => {
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
// 调用API
try {
const updatedPost = await postService.favoritePost(postId);
if (updatedPost) {
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
try {
const updatedPost = await postService.likePost(postId);
if (updatedPost) {
// 使用服务器返回的数据更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
}
} catch (error) {
console.error('点赞帖子失败:', error);
// 回滚
set({ posts: originalPosts });
}
} catch (error) {
console.error('收藏帖子失败:', error);
// 失败回滚状态
},
// 取消点赞 - 使用乐观更新工具
unlikePost: async (postId: string) => {
const originalPosts = get().posts;
// 乐观更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
)
}));
}
},
// 取消收藏 - 乐观更新
unfavoritePost: async (postId: string) => {
// 先乐观更新本地状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
// 调用API
try {
const updatedPost = await postService.unfavoritePost(postId);
if (updatedPost) {
// 使用后端返回的更新后数据更新状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
} else {
// API 返回失败,回滚状态
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
try {
const updatedPost = await postService.unlikePost(postId);
if (updatedPost) {
// 使用服务器返回的数据更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
}
} catch (error) {
console.error('取消点赞帖子失败:', error);
// 回滚
set({ posts: originalPosts });
}
} catch (error) {
console.error('取消收藏帖子失败:', error);
// 失败回滚状态
},
// 收藏帖子 - 使用乐观更新工具
favoritePost: async (postId: string) => {
const originalPosts = get().posts;
// 乐观更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
)
}));
}
},
// 点赞评论
likeComment: async (commentId: string) => {
// 先更新本地状态更新posts中的帖子的评论点赞状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
: p.top_comment
}))
}));
try {
const updatedPost = await postService.favoritePost(postId);
if (updatedPost) {
// 使用服务器返回的数据更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
}
} catch (error) {
console.error('收藏帖子失败:', error);
// 回滚
set({ posts: originalPosts });
}
},
// 调用API
try {
await commentService.likeComment(commentId);
} catch (error) {
console.error('点赞评论失败:', error);
// 失败回滚状态
// 取消收藏 - 使用乐观更新工具
unfavoritePost: async (postId: string) => {
const originalPosts = get().posts;
// 乐观更新
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
: p.top_comment
}))
}));
}
},
// 取消点赞评论
unlikeComment: async (commentId: string) => {
// 先更新本地状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
: p.top_comment
}))
}));
// 调用API
try {
await commentService.unlikeComment(commentId);
} catch (error) {
console.error('取消点赞评论失败:', error);
// 失败回滚状态
set(state => ({
posts: state.posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
: p.top_comment
}))
}));
}
},
// 关注用户
followUser: async (userId: string) => {
// 先更新本地状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: true, followersCount: u.followers_count + 1 }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: true, followersCount: user.followers_count + 1 }
: user
])
)
}));
// 调用API
try {
await authService.followUser(userId);
} catch (error) {
console.error('关注用户失败:', error);
// 失败回滚状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: false, followersCount: Math.max(0, u.followers_count - 1) }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: false, followersCount: Math.max(0, user.followers_count - 1) }
: user
])
posts: state.posts.map(p =>
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
)
}));
}
},
// 取消关注
unfollowUser: async (userId: string) => {
// 先更新本地状态
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: false, followersCount: Math.max(0, u.followers_count - 1) }
: u
),
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: false, followersCount: Math.max(0, user.followers_count - 1) }
: user
])
)
}));
try {
const updatedPost = await postService.unfavoritePost(postId);
if (updatedPost) {
// 使用服务器返回的数据更新
set(state => ({
posts: state.posts.map(p =>
p.id === postId ? updatedPost : p
)
}));
}
} catch (error) {
console.error('取消收藏帖子失败:', error);
// 回滚
set({ posts: originalPosts });
}
},
// 调用API
try {
await authService.unfollowUser(userId);
} catch (error) {
console.error('取消关注用户失败:', error);
// 失败回滚状态
// 点赞评论 - 使用乐观更新工具
likeComment: async (commentId: string) => {
await optimisticUpdater({
key: 'posts',
optimisticUpdate: (posts) =>
posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
: p.top_comment
})),
rollbackState: (original) => original,
apiCall: () => commentService.likeComment(commentId),
errorMessage: '点赞评论失败',
});
},
// 取消点赞评论 - 使用乐观更新工具
unlikeComment: async (commentId: string) => {
await optimisticUpdater({
key: 'posts',
optimisticUpdate: (posts) =>
posts.map(p => ({
...p,
top_comment: p.top_comment && p.top_comment.id === commentId
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
: p.top_comment
})),
rollbackState: (original) => original,
apiCall: () => commentService.unlikeComment(commentId),
errorMessage: '取消点赞评论失败',
});
},
// 关注用户 - 使用乐观更新工具
followUser: async (userId: string) => {
const originalUsers = get().users;
const originalUserCache = get().userCache;
// 乐观更新 users
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: true, followersCount: u.followers_count + 1 }
: u
),
}));
// 乐观更新 userCache
set(state => ({
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
@@ -566,17 +449,66 @@ export const useUserStore = create<UserState>((set, get) => ({
])
)
}));
}
},
// 刷新所有数据
refreshAll: async () => {
await Promise.all([
get().fetchPosts('recommend', 1),
get().fetchNotificationBadge(),
]);
},
}));
try {
await authService.followUser(userId);
} catch (error) {
console.error('关注用户失败:', error);
// 回滚
set({
users: originalUsers,
userCache: originalUserCache
});
}
},
// 取消关注 - 使用乐观更新工具
unfollowUser: async (userId: string) => {
const originalUsers = get().users;
const originalUserCache = get().userCache;
// 乐观更新 users
set(state => ({
users: state.users.map(u =>
u.id === userId
? { ...u, isFollowing: false, followersCount: Math.max(0, u.followers_count - 1) }
: u
),
}));
// 乐观更新 userCache
set(state => ({
userCache: Object.fromEntries(
Object.entries(state.userCache).map(([id, user]) => [
id,
id === userId
? { ...user, isFollowing: false, followersCount: Math.max(0, user.followers_count - 1) }
: user
])
)
}));
try {
await authService.unfollowUser(userId);
} catch (error) {
console.error('取消关注用户失败:', error);
// 回滚
set({
users: originalUsers,
userCache: originalUserCache
});
}
},
// 刷新所有数据
refreshAll: async () => {
await Promise.all([
get().fetchPosts('recommend', 1),
get().fetchNotificationBadge(),
]);
},
};
});
// 导出selector hooks以优化性能
export const usePosts = () => useUserStore((state) => state.posts);