refactor(message): replace MessageStateManager with zustand store
- Remove MessageStateManager wrapper layer in favor of direct zustand store - Move service modules to services/ subdirectory (ConversationOperations, MessageDeduplication, MessageSendService, MessageSyncService, ReadReceiptManager, UserCacheService, WSMessageHandler) - Add new zustand-based store with subscriptionManager for event handling - Introduce React hooks for message state (useConversations, useMessages, useUnreadCount, etc.) - Update exports in index.ts to reflect new module structure - Deprecate messageManager.ts entry point in favor of message/index.ts - Maintain backward compatibility with existing MessageManager API
This commit is contained in:
@@ -13,13 +13,14 @@ export {
|
||||
} from './userStore';
|
||||
|
||||
// MessageManager 新架构导出(已替换 conversationStore)
|
||||
export { messageManager } from './messageManager';
|
||||
// 重构:从 ./message 目录导入,移除了 messageManager.ts 重导出文件
|
||||
export { messageManager, MessageManager } from './message';
|
||||
export type {
|
||||
MessageEvent,
|
||||
MessageEventType,
|
||||
MessageSubscriber,
|
||||
MessageManagerConversationListDeps,
|
||||
} from './messageManager';
|
||||
} from './message';
|
||||
export {
|
||||
CONVERSATION_LIST_PAGE_SIZE,
|
||||
type ConversationListPage,
|
||||
|
||||
@@ -1,59 +1,30 @@
|
||||
/**
|
||||
* MessageManager - 消息管理核心模块(重构版)
|
||||
*
|
||||
* 作为协调者,整合各个专注的服务模块
|
||||
* 保持与原始版本完全兼容的 API
|
||||
* 作为轻量级协调者,整合各个专注的服务模块
|
||||
* 直接使用 zustand store 进行状态管理
|
||||
*
|
||||
* 重构说明:
|
||||
* - 移除了 MessageStateManager 包装层
|
||||
* - 直接使用 zustand store
|
||||
* - 服务层移至 services/ 目录
|
||||
*/
|
||||
|
||||
import type { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import {
|
||||
wsService,
|
||||
WSChatMessage,
|
||||
WSGroupChatMessage,
|
||||
WSReadMessage,
|
||||
WSGroupReadMessage,
|
||||
WSRecallMessage,
|
||||
WSGroupRecallMessage,
|
||||
WSGroupTypingMessage,
|
||||
WSGroupNoticeMessage,
|
||||
GroupNoticeType,
|
||||
} from '../../services/wsService';
|
||||
import {
|
||||
saveMessage,
|
||||
saveMessagesBatch,
|
||||
getMessagesByConversation,
|
||||
getMaxSeq,
|
||||
getMinSeq,
|
||||
getMessagesBeforeSeq,
|
||||
markConversationAsRead,
|
||||
updateConversationCacheUnreadCount,
|
||||
CachedMessage,
|
||||
getUserCache,
|
||||
saveUserCache,
|
||||
updateMessageStatus,
|
||||
deleteConversation as deleteConversationFromDb,
|
||||
saveConversationsWithRelatedCache,
|
||||
} from '../../services/database';
|
||||
import { api } from '../../services/api';
|
||||
import { vibrateOnMessage } from '../../services/messageVibrationService';
|
||||
import { useAuthStore } from '../authStore';
|
||||
import {
|
||||
type IConversationListPagedSource,
|
||||
SqliteConversationListPagedSource,
|
||||
createRemoteConversationListSource,
|
||||
CONVERSATION_LIST_PAGE_SIZE,
|
||||
} from '../conversationListSources';
|
||||
import type { MessageManagerConversationListDeps } from './types';
|
||||
|
||||
// 导入新模块
|
||||
import { MessageStateManager } from './MessageStateManager';
|
||||
import { MessageDeduplication } from './MessageDeduplication';
|
||||
import { UserCacheService } from './UserCacheService';
|
||||
import { ReadReceiptManager } from './ReadReceiptManager';
|
||||
import { ConversationOperations } from './ConversationOperations';
|
||||
import { MessageSendService } from './MessageSendService';
|
||||
import { MessageSyncService } from './MessageSyncService';
|
||||
import { WSMessageHandler } from './WSMessageHandler';
|
||||
// 导入服务
|
||||
import { MessageDeduplication, messageDeduplication } from './services/MessageDeduplication';
|
||||
import { UserCacheService, userCacheService } from './services/UserCacheService';
|
||||
import { ReadReceiptManager } from './services/ReadReceiptManager';
|
||||
import { ConversationOperations } from './services/ConversationOperations';
|
||||
import { MessageSendService } from './services/MessageSendService';
|
||||
import { MessageSyncService } from './services/MessageSyncService';
|
||||
import { WSMessageHandler } from './services/WSMessageHandler';
|
||||
|
||||
// 导入 store
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId } from './store';
|
||||
|
||||
// 重新导出类型,保持兼容性
|
||||
export type {
|
||||
@@ -69,7 +40,6 @@ export type {
|
||||
|
||||
class MessageManager {
|
||||
// 子模块
|
||||
private stateManager: MessageStateManager;
|
||||
private deduplication: MessageDeduplication;
|
||||
private userCacheService: UserCacheService;
|
||||
private readReceiptManager: ReadReceiptManager;
|
||||
@@ -84,21 +54,20 @@ class MessageManager {
|
||||
private initializePromise: Promise<void> | null = null;
|
||||
private activatingConversationTasks: Map<string, Promise<void>> = new Map();
|
||||
|
||||
constructor(deps?: import('./types').MessageManagerConversationListDeps) {
|
||||
constructor(deps?: MessageManagerConversationListDeps) {
|
||||
// 初始化子模块
|
||||
this.stateManager = new MessageStateManager();
|
||||
this.deduplication = new MessageDeduplication();
|
||||
this.userCacheService = new UserCacheService();
|
||||
this.readReceiptManager = new ReadReceiptManager(this.stateManager);
|
||||
this.conversationOps = new ConversationOperations(this.stateManager);
|
||||
this.sendService = new MessageSendService(this.stateManager, () => this.getCurrentUserId());
|
||||
this.readReceiptManager = new ReadReceiptManager();
|
||||
this.conversationOps = new ConversationOperations();
|
||||
this.sendService = new MessageSendService(() => this.getCurrentUserId());
|
||||
this.syncService = new MessageSyncService(
|
||||
this.stateManager,
|
||||
() => this.getCurrentUserId(),
|
||||
this.readReceiptManager,
|
||||
this.userCacheService,
|
||||
deps
|
||||
);
|
||||
this.wsHandler = new WSMessageHandler(
|
||||
this.stateManager,
|
||||
this.deduplication,
|
||||
this.userCacheService,
|
||||
() => this.getCurrentUserId(),
|
||||
@@ -119,8 +88,9 @@ class MessageManager {
|
||||
private initAuthListener() {
|
||||
const authState = useAuthStore.getState();
|
||||
this.currentUserId = authState.currentUser?.id || null;
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
if (this.currentUserId && !this.stateManager.getState().isInitialized) {
|
||||
if (this.currentUserId && !store.isInitialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
@@ -130,17 +100,17 @@ class MessageManager {
|
||||
const prevUserId = this.currentUserId;
|
||||
this.currentUserId = nextUserId;
|
||||
|
||||
if (nextUserId && !this.stateManager.getState().isInitialized) {
|
||||
if (nextUserId && !useMessageStore.getState().isInitialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
if (nextUserId && this.stateManager.getActiveConversation()) {
|
||||
this.activateConversation(this.stateManager.getActiveConversation()!, { forceSync: true }).catch(error => {
|
||||
if (nextUserId && useMessageStore.getState().currentConversationId) {
|
||||
this.activateConversation(useMessageStore.getState().currentConversationId!, { forceSync: true }).catch(error => {
|
||||
console.error('[MessageManager] 登录态就绪后重激活会话失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (!nextUserId && prevUserId && this.stateManager.getState().isInitialized) {
|
||||
if (!nextUserId && prevUserId && useMessageStore.getState().isInitialized) {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
@@ -154,14 +124,12 @@ class MessageManager {
|
||||
return this.currentUserId;
|
||||
}
|
||||
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
// ==================== 初始化与销毁 ====================
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.stateManager.getState().isInitialized) {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
if (store.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,26 +157,26 @@ class MessageManager {
|
||||
// 加载未读数
|
||||
await this.fetchUnreadCount();
|
||||
|
||||
this.stateManager.setInitialized(true);
|
||||
useMessageStore.getState().setInitialized(true);
|
||||
|
||||
// 处理缓冲的 SSE 事件
|
||||
await this.wsHandler.flushBufferedSSEEvents();
|
||||
|
||||
this.wsHandler.setBootstrapping(false);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: this.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (this.stateManager.getActiveConversation()) {
|
||||
await this.fetchMessages(this.stateManager.getActiveConversation()!);
|
||||
if (useMessageStore.getState().currentConversationId) {
|
||||
await this.fetchMessages(useMessageStore.getState().currentConversationId!);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageManager] 初始化失败:', error);
|
||||
this.wsHandler.setBootstrapping(false);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'initialize' },
|
||||
timestamp: Date.now(),
|
||||
@@ -225,7 +193,8 @@ class MessageManager {
|
||||
|
||||
destroy(): void {
|
||||
this.wsHandler.disconnect();
|
||||
this.stateManager.reset();
|
||||
useMessageStore.getState().reset();
|
||||
subscriptionManager.clear();
|
||||
this.syncService.restartSources();
|
||||
this.readReceiptManager.reset();
|
||||
this.deduplication.reset();
|
||||
@@ -310,61 +279,61 @@ class MessageManager {
|
||||
return this.conversationOps.decrementSystemUnreadCount(count);
|
||||
}
|
||||
|
||||
// ==================== 状态查询方法(代理到 stateManager)====================
|
||||
// ==================== 状态查询方法(直接使用 store)====================
|
||||
|
||||
getConversations(): ConversationResponse[] {
|
||||
return this.stateManager.getConversations();
|
||||
return useMessageStore.getState().getConversations();
|
||||
}
|
||||
|
||||
getConversation(conversationId: string): ConversationResponse | null {
|
||||
return this.stateManager.getConversation(conversationId);
|
||||
return useMessageStore.getState().getConversation(conversationId);
|
||||
}
|
||||
|
||||
getMessages(conversationId: string): MessageResponse[] {
|
||||
return this.stateManager.getMessages(conversationId);
|
||||
return useMessageStore.getState().getMessages(conversationId);
|
||||
}
|
||||
|
||||
getUnreadCount(): { total: number; system: number } {
|
||||
return this.stateManager.getUnreadCount();
|
||||
return useMessageStore.getState().getUnreadCount();
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.stateManager.isConnected();
|
||||
return useMessageStore.getState().isConnected();
|
||||
}
|
||||
|
||||
isLoading(): boolean {
|
||||
return this.stateManager.isLoading();
|
||||
return useMessageStore.getState().isLoading();
|
||||
}
|
||||
|
||||
isLoadingMessages(conversationId: string): boolean {
|
||||
return this.stateManager.isLoadingMessages(conversationId);
|
||||
return useMessageStore.getState().isLoadingMessages(conversationId);
|
||||
}
|
||||
|
||||
getTypingUsers(groupId: string): string[] {
|
||||
return this.stateManager.getTypingUsers(groupId);
|
||||
return useMessageStore.getState().getTypingUsers(groupId);
|
||||
}
|
||||
|
||||
isMuted(groupId: string): boolean {
|
||||
return this.stateManager.isMuted(groupId);
|
||||
return useMessageStore.getState().isMuted(groupId);
|
||||
}
|
||||
|
||||
setMutedStatus(groupId: string, isMuted: boolean): void {
|
||||
return this.stateManager.setMutedStatus(groupId, isMuted);
|
||||
return useMessageStore.getState().setMutedStatus(groupId, isMuted);
|
||||
}
|
||||
|
||||
// ==================== 活动会话管理 ====================
|
||||
|
||||
setActiveConversation(conversationId: string | null): void {
|
||||
const normalizedId = conversationId ? this.normalizeConversationId(conversationId) : null;
|
||||
this.stateManager.setCurrentConversation(normalizedId);
|
||||
const normalizedId = conversationId ? normalizeConversationId(conversationId) : null;
|
||||
useMessageStore.getState().setCurrentConversation(normalizedId);
|
||||
}
|
||||
|
||||
getActiveConversation(): string | null {
|
||||
return this.stateManager.getActiveConversation();
|
||||
return useMessageStore.getState().currentConversationId;
|
||||
}
|
||||
|
||||
async activateConversation(conversationId: string, options?: { forceSync?: boolean }): Promise<void> {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
if (!normalizedId) return;
|
||||
|
||||
await this.initialize();
|
||||
@@ -372,7 +341,7 @@ class MessageManager {
|
||||
|
||||
// 先下发当前内存快照
|
||||
const snapshot = this.getMessages(normalizedId);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId: normalizedId,
|
||||
@@ -405,16 +374,16 @@ class MessageManager {
|
||||
// ==================== 订阅机制 ====================
|
||||
|
||||
subscribe(subscriber: import('./types').MessageSubscriber): () => void {
|
||||
const unsubscribe = this.stateManager.subscribe(subscriber);
|
||||
const unsubscribe = subscriptionManager.subscribe(subscriber);
|
||||
|
||||
// 立即发送当前状态给新订阅者(与原始版本保持一致)
|
||||
subscriber({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: this.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const unreadCount = this.stateManager.getUnreadCount();
|
||||
const unreadCount = this.getUnreadCount();
|
||||
subscriber({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
@@ -426,14 +395,14 @@ class MessageManager {
|
||||
|
||||
subscriber({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: this.stateManager.isConnected() },
|
||||
payload: { connected: this.isConnected() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 如果当前有活动会话,立即发送该会话的消息给新订阅者
|
||||
const activeConversationId = this.stateManager.getActiveConversation();
|
||||
const activeConversationId = this.getActiveConversation();
|
||||
if (activeConversationId) {
|
||||
const currentMessages = this.stateManager.getMessages(activeConversationId);
|
||||
const currentMessages = this.getMessages(activeConversationId);
|
||||
if (currentMessages.length > 0) {
|
||||
subscriber({
|
||||
type: 'messages_updated',
|
||||
@@ -451,7 +420,7 @@ class MessageManager {
|
||||
}
|
||||
|
||||
subscribeToMessages(conversationId: string, callback: (messages: MessageResponse[]) => void): () => void {
|
||||
return this.stateManager.subscribe((event) => {
|
||||
return subscriptionManager.subscribe((event) => {
|
||||
if (event.type === 'messages_updated' && event.payload.conversationId === conversationId) {
|
||||
callback(event.payload.messages);
|
||||
}
|
||||
@@ -484,7 +453,7 @@ class MessageManager {
|
||||
}
|
||||
|
||||
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
|
||||
if (!this.stateManager.getActiveConversation()) return false;
|
||||
if (!this.getActiveConversation()) return false;
|
||||
if (!forceRefresh) return false;
|
||||
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh';
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
/**
|
||||
* 消息状态管理器
|
||||
* 纯状态管理类,不包含业务逻辑
|
||||
* 负责管理所有消息相关的内存状态
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConversationResponse,
|
||||
MessageResponse,
|
||||
} from '../../types/dto';
|
||||
import type {
|
||||
MessageManagerState,
|
||||
MessageSubscriber,
|
||||
MessageEvent,
|
||||
IMessageStateManager,
|
||||
} from './types';
|
||||
|
||||
export class MessageStateManager implements IMessageStateManager {
|
||||
private state: MessageManagerState;
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
totalUnreadCount: 0,
|
||||
systemUnreadCount: 0,
|
||||
isWSConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoadingConversations: false,
|
||||
loadingMessagesSet: new Set(),
|
||||
isInitialized: false,
|
||||
subscribers: new Set(),
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 获取状态 ====================
|
||||
|
||||
getState(): MessageManagerState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
getConversations(): ConversationResponse[] {
|
||||
return this.state.conversationList;
|
||||
}
|
||||
|
||||
getConversation(conversationId: string): ConversationResponse | null {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
return this.state.conversations.get(normalizedId) || null;
|
||||
}
|
||||
|
||||
getMessages(conversationId: string): MessageResponse[] {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
return this.state.messagesMap.get(normalizedId) || [];
|
||||
}
|
||||
|
||||
getUnreadCount(): { total: number; system: number } {
|
||||
return {
|
||||
total: this.state.totalUnreadCount,
|
||||
system: this.state.systemUnreadCount,
|
||||
};
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
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) || [];
|
||||
}
|
||||
|
||||
isMuted(groupId: string): boolean {
|
||||
return this.state.mutedStatusMap.get(groupId) || false;
|
||||
}
|
||||
|
||||
getActiveConversation(): string | null {
|
||||
return this.state.currentConversationId;
|
||||
}
|
||||
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
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);
|
||||
|
||||
if (this.state.currentConversationId === normalizedId) {
|
||||
this.state.currentConversationId = null;
|
||||
}
|
||||
|
||||
this.updateConversationList();
|
||||
}
|
||||
|
||||
setMessages(conversationId: string, messages: MessageResponse[]): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
this.state.messagesMap.set(normalizedId, messages);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 updated = messages.map(m =>
|
||||
String(m.id) === String(messageId) ? { ...m, ...updates } : m
|
||||
);
|
||||
this.state.messagesMap.set(normalizedId, updated);
|
||||
}
|
||||
|
||||
setUnreadCount(total: number, system: number): void {
|
||||
this.state.totalUnreadCount = total;
|
||||
this.state.systemUnreadCount = system;
|
||||
}
|
||||
|
||||
setSSEConnected(connected: boolean): void {
|
||||
this.state.isWSConnected = connected;
|
||||
}
|
||||
|
||||
setCurrentConversation(conversationId: string | null): void {
|
||||
this.state.currentConversationId = conversationId;
|
||||
}
|
||||
|
||||
setLoading(loading: boolean): void {
|
||||
this.state.isLoadingConversations = loading;
|
||||
}
|
||||
|
||||
setLoadingMessages(conversationId: string, loading: boolean): void {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
if (loading) {
|
||||
this.state.loadingMessagesSet.add(normalizedId);
|
||||
} else {
|
||||
this.state.loadingMessagesSet.delete(normalizedId);
|
||||
}
|
||||
}
|
||||
|
||||
setTypingUsers(groupId: string, users: string[]): void {
|
||||
this.state.typingUsersMap.set(groupId, users);
|
||||
}
|
||||
|
||||
setMutedStatus(groupId: string, isMuted: boolean): void {
|
||||
this.state.mutedStatusMap.set(groupId, isMuted);
|
||||
}
|
||||
|
||||
setInitialized(initialized: boolean): void {
|
||||
this.state.isInitialized = initialized;
|
||||
}
|
||||
|
||||
// ==================== 订阅机制 ====================
|
||||
|
||||
subscribe(subscriber: MessageSubscriber): () => void {
|
||||
this.state.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.state.subscribers.delete(subscriber);
|
||||
};
|
||||
}
|
||||
|
||||
notifySubscribers(event: MessageEvent): void {
|
||||
this.state.subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber(event);
|
||||
} catch (error) {
|
||||
console.error('[MessageStateManager] 订阅者执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话列表排序
|
||||
* 排序规则:置顶优先,再按最后消息时间排序
|
||||
*/
|
||||
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.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();
|
||||
557
src/stores/message/hooks.ts
Normal file
557
src/stores/message/hooks.ts
Normal file
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* 消息模块 React Hooks
|
||||
*
|
||||
* 提供React组件与消息状态管理的集成
|
||||
* 所有hooks都基于zustand store和订阅机制
|
||||
* 确保组件能实时获取状态更新
|
||||
*
|
||||
* 重构说明:直接使用 zustand store,移除了对 messageManager 的直接依赖
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId } from './store';
|
||||
import type { MessageEvent, MessageSubscriber } from './types';
|
||||
|
||||
// ==================== useConversations - 获取会话列表 ====================
|
||||
|
||||
interface UseConversationsReturn {
|
||||
conversations: ConversationResponse[];
|
||||
isLoading: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
loadMore: () => Promise<void>;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
* 用于 MessageListScreen 等需要显示会话列表的组件
|
||||
*/
|
||||
export function useConversations(
|
||||
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>,
|
||||
loadMoreConversations: () => Promise<void>,
|
||||
canLoadMore: () => boolean,
|
||||
initialize: () => Promise<void>
|
||||
): UseConversationsReturn {
|
||||
const store = useMessageStore();
|
||||
const [conversations, setConversations] = useState<ConversationResponse[]>(() => store.getConversations());
|
||||
const [isLoading, setIsLoading] = useState(() => store.isLoading());
|
||||
const [hasMore, setHasMore] = useState(() => canLoadMore());
|
||||
|
||||
useEffect(() => {
|
||||
// 订阅事件
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
switch (event.type) {
|
||||
case 'conversations_updated':
|
||||
setConversations(store.getConversations());
|
||||
setHasMore(canLoadMore());
|
||||
break;
|
||||
case 'conversations_loading':
|
||||
setIsLoading(!!event.payload?.loading);
|
||||
break;
|
||||
case 'connection_changed':
|
||||
// 连接状态变化时可能需要刷新
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
initialize().catch(error => {
|
||||
console.error('[useConversations] 初始化失败:', error);
|
||||
});
|
||||
|
||||
// 冷启动兜底:延迟做一次强制刷新
|
||||
const coldStartSyncTimer = setTimeout(() => {
|
||||
fetchConversations(true, 'hooks-initial-refresh').catch(error => {
|
||||
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
||||
});
|
||||
}, 1200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(coldStartSyncTimer);
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchConversations(true, 'hooks-manual-refresh');
|
||||
}, [fetchConversations]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
await loadMoreConversations();
|
||||
}, [loadMoreConversations]);
|
||||
|
||||
return {
|
||||
conversations,
|
||||
isLoading,
|
||||
refresh,
|
||||
loadMore,
|
||||
hasMore,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useMessages - 获取指定会话的消息 ====================
|
||||
|
||||
interface UseMessagesReturn {
|
||||
messages: MessageResponse[];
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话的消息
|
||||
*/
|
||||
export function useMessages(
|
||||
conversationId: string,
|
||||
fetchMessages: (conversationId: string) => Promise<void>,
|
||||
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
||||
): UseMessagesReturn {
|
||||
const store = useMessageStore();
|
||||
const normalizedConversationId = normalizeConversationId(conversationId);
|
||||
|
||||
const [messages, setMessages] = useState<MessageResponse[]>(() =>
|
||||
store.getMessages(normalizedConversationId)
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(() =>
|
||||
store.isLoadingMessages(normalizedConversationId)
|
||||
);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'messages_updated' && event.payload.conversationId === normalizedConversationId) {
|
||||
setMessages(event.payload.messages);
|
||||
}
|
||||
});
|
||||
|
||||
// 激活会话
|
||||
fetchMessages(normalizedConversationId).catch(error => {
|
||||
console.error('[useMessages] 激活会话失败:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
// 清理活动会话
|
||||
if (store.getActiveConversation() === normalizedConversationId) {
|
||||
store.setCurrentConversation(null);
|
||||
}
|
||||
unsubscribe();
|
||||
};
|
||||
}, [normalizedConversationId]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const currentMessages = store.getMessages(conversationId);
|
||||
if (currentMessages.length === 0) return;
|
||||
|
||||
// 消息数组在 store 内保持 seq 升序,首项即最早消息
|
||||
const minSeq = currentMessages[0]?.seq;
|
||||
if (minSeq === undefined) return;
|
||||
|
||||
const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20);
|
||||
setHasMore(loadedMessages.length > 0);
|
||||
}, [conversationId, loadMoreMessages]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchMessages(conversationId);
|
||||
setMessages(store.getMessages(conversationId));
|
||||
}, [conversationId, fetchMessages]);
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
hasMore,
|
||||
loadMore,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useSendMessage - 发送消息 ====================
|
||||
|
||||
interface UseSendMessageReturn {
|
||||
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
|
||||
isSending: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
export function useSendMessage(
|
||||
conversationId: string,
|
||||
sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>
|
||||
): UseSendMessageReturn {
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
const message = await sendMessageService(conversationId, segments, options);
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('[useSendMessage] 发送消息失败:', error);
|
||||
return null;
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
}, [conversationId, sendMessageService]);
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
isSending,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useMarkAsRead - 标记已读 ====================
|
||||
|
||||
interface UseMarkAsReadReturn {
|
||||
markAsRead: (seq: number) => Promise<void>;
|
||||
markAllAsRead: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已读
|
||||
*/
|
||||
export function useMarkAsRead(
|
||||
conversationId: string,
|
||||
markAsReadService: (conversationId: string, seq: number) => Promise<void>,
|
||||
markAllAsReadService: () => Promise<void>
|
||||
): UseMarkAsReadReturn {
|
||||
const markAsRead = useCallback(async (seq: number) => {
|
||||
await markAsReadService(conversationId, seq);
|
||||
}, [conversationId, markAsReadService]);
|
||||
|
||||
const markAllAsRead = useCallback(async () => {
|
||||
await markAllAsReadService();
|
||||
}, [markAllAsReadService]);
|
||||
|
||||
return {
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useUnreadCount - 获取未读数 ====================
|
||||
|
||||
interface UseUnreadCountReturn {
|
||||
totalUnreadCount: number;
|
||||
systemUnreadCount: number;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读数
|
||||
*/
|
||||
export function useUnreadCount(fetchUnreadCount: () => Promise<void>): UseUnreadCountReturn {
|
||||
const store = useMessageStore();
|
||||
const [counts, setCounts] = useState(() => store.getUnreadCount());
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'unread_count_updated') {
|
||||
setCounts(store.getUnreadCount());
|
||||
}
|
||||
});
|
||||
|
||||
// 初始获取
|
||||
fetchUnreadCount();
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchUnreadCount();
|
||||
}, [fetchUnreadCount]);
|
||||
|
||||
return {
|
||||
totalUnreadCount: counts.total,
|
||||
systemUnreadCount: counts.system,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useSystemUnreadCount - 获取系统消息未读数 ====================
|
||||
|
||||
interface UseSystemUnreadCountReturn {
|
||||
systemUnreadCount: number;
|
||||
setSystemUnreadCount: (count: number) => void;
|
||||
incrementSystemUnreadCount: () => void;
|
||||
decrementSystemUnreadCount: (count?: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统消息未读数
|
||||
*/
|
||||
export function useSystemUnreadCount(
|
||||
setSystemUnreadCountService: (count: number) => void,
|
||||
incrementSystemUnreadCountService: () => void,
|
||||
decrementSystemUnreadCountService: (count?: number) => void
|
||||
): UseSystemUnreadCountReturn {
|
||||
const store = useMessageStore();
|
||||
const [systemUnreadCount, setSystemUnreadCountState] = useState(() =>
|
||||
store.getUnreadCount().system
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'unread_count_updated') {
|
||||
setSystemUnreadCountState(store.getUnreadCount().system);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const setSystemUnreadCount = useCallback((count: number) => {
|
||||
setSystemUnreadCountService(count);
|
||||
}, [setSystemUnreadCountService]);
|
||||
|
||||
const incrementSystemUnreadCount = useCallback(() => {
|
||||
incrementSystemUnreadCountService();
|
||||
}, [incrementSystemUnreadCountService]);
|
||||
|
||||
const decrementSystemUnreadCount = useCallback((count = 1) => {
|
||||
decrementSystemUnreadCountService(count);
|
||||
}, [decrementSystemUnreadCountService]);
|
||||
|
||||
return {
|
||||
systemUnreadCount,
|
||||
setSystemUnreadCount,
|
||||
incrementSystemUnreadCount,
|
||||
decrementSystemUnreadCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useConversation - 获取单个会话 ====================
|
||||
|
||||
interface UseConversationReturn {
|
||||
conversation: ConversationResponse | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个会话
|
||||
*/
|
||||
export function useConversation(
|
||||
conversationId: string,
|
||||
fetchConversationDetail: (conversationId: string) => Promise<ConversationResponse | null>
|
||||
): UseConversationReturn {
|
||||
const store = useMessageStore();
|
||||
const [conversation, setConversation] = useState<ConversationResponse | null>(() =>
|
||||
store.getConversation(conversationId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setConversation(store.getConversation(conversationId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'conversations_updated') {
|
||||
const updated = store.getConversation(conversationId);
|
||||
setConversation(updated);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [conversationId]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchConversationDetail(conversationId);
|
||||
setConversation(store.getConversation(conversationId));
|
||||
}, [conversationId, fetchConversationDetail]);
|
||||
|
||||
return {
|
||||
conversation,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useMessageManager - 通用订阅 ====================
|
||||
|
||||
interface UseMessageManagerReturn {
|
||||
isConnected: boolean;
|
||||
subscribe: (subscriber: MessageSubscriber) => () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用消息订阅
|
||||
*/
|
||||
export function useMessageManager(initialize: () => Promise<void>): UseMessageManagerReturn {
|
||||
const store = useMessageStore();
|
||||
const [isConnected, setIsConnected] = useState(() => store.isConnected());
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'connection_changed') {
|
||||
setIsConnected(event.payload.connected);
|
||||
}
|
||||
});
|
||||
|
||||
initialize().then(() => {
|
||||
setIsConnected(store.isConnected());
|
||||
}).catch(error => {
|
||||
console.error('[useMessageManager] 初始化失败:', error);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((subscriber: MessageSubscriber) => {
|
||||
return subscriptionManager.subscribe(subscriber);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useCreateConversation - 创建会话 ====================
|
||||
|
||||
interface UseCreateConversationReturn {
|
||||
createConversation: (userId: string) => Promise<ConversationResponse | null>;
|
||||
isCreating: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
export function useCreateConversation(
|
||||
createConversationService: (userId: string) => Promise<ConversationResponse | null>
|
||||
): UseCreateConversationReturn {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const createConversation = useCallback(async (userId: string) => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const conversation = await createConversationService(userId);
|
||||
return conversation;
|
||||
} catch (error) {
|
||||
console.error('[useCreateConversation] 创建会话失败:', error);
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [createConversationService]);
|
||||
|
||||
return {
|
||||
createConversation,
|
||||
isCreating,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useUpdateConversation - 更新会话 ====================
|
||||
|
||||
interface UseUpdateConversationReturn {
|
||||
updateConversation: (conversationId: string, updates: Partial<ConversationResponse>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
*/
|
||||
export function useUpdateConversation(
|
||||
updateConversationService: (conversationId: string, updates: Partial<ConversationResponse>) => void
|
||||
): UseUpdateConversationReturn {
|
||||
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
|
||||
updateConversationService(conversationId, updates);
|
||||
}, [updateConversationService]);
|
||||
|
||||
return {
|
||||
updateConversation,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useGroupTyping - 群聊输入状态 ====================
|
||||
|
||||
interface UseGroupTypingReturn {
|
||||
typingUsers: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群聊输入状态
|
||||
*/
|
||||
export function useGroupTyping(groupId: string): UseGroupTypingReturn {
|
||||
const store = useMessageStore();
|
||||
const [typingUsers, setTypingUsers] = useState<string[]>(() =>
|
||||
store.getTypingUsers(groupId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTypingUsers(store.getTypingUsers(groupId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'typing_status' && event.payload.groupId === groupId) {
|
||||
setTypingUsers(event.payload.typingUsers);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [groupId]);
|
||||
|
||||
return {
|
||||
typingUsers,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useGroupMuted - 群聊禁言状态 ====================
|
||||
|
||||
interface UseGroupMutedReturn {
|
||||
isMuted: boolean;
|
||||
setMutedStatus: (muted: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群聊禁言状态
|
||||
*/
|
||||
export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
||||
const store = useMessageStore();
|
||||
const [isMuted, setIsMuted] = useState(() => store.isMuted(groupId));
|
||||
|
||||
useEffect(() => {
|
||||
setIsMuted(store.isMuted(groupId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'group_notice' && event.payload.groupId === groupId) {
|
||||
if (event.payload.noticeType === 'muted' || event.payload.noticeType === 'unmuted') {
|
||||
setIsMuted(event.payload.noticeType === 'muted');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [groupId]);
|
||||
|
||||
const setMutedStatus = useCallback((muted: boolean) => {
|
||||
store.setMutedStatus(groupId, muted);
|
||||
}, [groupId]);
|
||||
|
||||
return {
|
||||
isMuted,
|
||||
setMutedStatus,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useActiveConversation - 活动会话 ====================
|
||||
|
||||
interface UseActiveConversationReturn {
|
||||
activeConversationId: string | null;
|
||||
setActiveConversation: (conversationId: string | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取/设置活动会话
|
||||
*/
|
||||
export function useActiveConversation(): UseActiveConversationReturn {
|
||||
const store = useMessageStore();
|
||||
const [activeConversationId, setActiveConversationId] = useState<string | null>(() =>
|
||||
store.getActiveConversation()
|
||||
);
|
||||
|
||||
const setActiveConversation = useCallback((conversationId: string | null) => {
|
||||
store.setCurrentConversation(conversationId);
|
||||
setActiveConversationId(conversationId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
activeConversationId,
|
||||
setActiveConversation,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,27 @@
|
||||
/**
|
||||
* 消息模块统一导出
|
||||
*
|
||||
* 提供向后兼容的 API 导出
|
||||
* 重构说明:
|
||||
* - 移除了 MessageStateManager 包装层
|
||||
* - 直接使用 zustand store 进行状态管理
|
||||
* - 服务层移至 services/ 目录
|
||||
* - 新增 hooks.ts 封装常用逻辑
|
||||
*/
|
||||
|
||||
// ==================== 主管理器 ====================
|
||||
export { messageManager, MessageManager } from './MessageManager';
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
export {
|
||||
useMessageStore,
|
||||
subscriptionManager,
|
||||
normalizeConversationId,
|
||||
mergeMessagesById,
|
||||
notifySubscribers,
|
||||
subscribeToMessages,
|
||||
} from './store';
|
||||
export type { MessageState, MessageActions, MessageStore } from './store';
|
||||
|
||||
// ==================== 类型导出 ====================
|
||||
// 从 types.ts 导出所有类型
|
||||
export type {
|
||||
@@ -17,6 +32,7 @@ export type {
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
HandleNewMessageOptions,
|
||||
// 服务接口类型(保留向后兼容,但服务层已不再使用)
|
||||
IMessageStateManager,
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
@@ -37,30 +53,57 @@ export {
|
||||
MIN_RECONNECT_SYNC_INTERVAL,
|
||||
} from './constants';
|
||||
|
||||
// ==================== 子模块导出(可选使用)====================
|
||||
// 状态管理器
|
||||
export { MessageStateManager, messageStateManager } from './MessageStateManager';
|
||||
// ==================== 服务导出 ====================
|
||||
export {
|
||||
// 消息去重服务
|
||||
MessageDeduplication,
|
||||
messageDeduplication,
|
||||
|
||||
// 消息去重服务
|
||||
export { MessageDeduplication, messageDeduplication } from './MessageDeduplication';
|
||||
// 用户缓存服务
|
||||
UserCacheService,
|
||||
userCacheService,
|
||||
|
||||
// 用户缓存服务
|
||||
export { UserCacheService, userCacheService } from './UserCacheService';
|
||||
// 已读回执管理器
|
||||
ReadReceiptManager,
|
||||
|
||||
// 已读回执管理器
|
||||
export { ReadReceiptManager } from './ReadReceiptManager';
|
||||
// 会话操作服务
|
||||
ConversationOperations,
|
||||
|
||||
// 会话操作服务
|
||||
export { ConversationOperations } from './ConversationOperations';
|
||||
// 消息发送服务
|
||||
MessageSendService,
|
||||
|
||||
// 消息发送服务
|
||||
export { MessageSendService } from './MessageSendService';
|
||||
// 消息同步服务
|
||||
MessageSyncService,
|
||||
|
||||
// 消息同步服务
|
||||
export { MessageSyncService } from './MessageSyncService';
|
||||
// WebSocket 消息处理器
|
||||
WSMessageHandler,
|
||||
} from './services';
|
||||
|
||||
// WebSocket 消息处理器
|
||||
export { WSMessageHandler } from './WSMessageHandler';
|
||||
// ==================== Hooks 导出 ====================
|
||||
export {
|
||||
// 会话相关
|
||||
useConversations,
|
||||
useConversation,
|
||||
useCreateConversation,
|
||||
useUpdateConversation,
|
||||
useActiveConversation,
|
||||
|
||||
// 消息相关
|
||||
useMessages,
|
||||
useSendMessage,
|
||||
useMarkAsRead,
|
||||
|
||||
// 未读数相关
|
||||
useUnreadCount,
|
||||
useSystemUnreadCount,
|
||||
|
||||
// 群聊相关
|
||||
useGroupTyping,
|
||||
useGroupMuted,
|
||||
|
||||
// 通用
|
||||
useMessageManager,
|
||||
} from './hooks';
|
||||
|
||||
// ==================== 默认导出 ====================
|
||||
export { default } from './MessageManager';
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
/**
|
||||
* 会话操作服务
|
||||
* 处理会话的 CRUD 操作
|
||||
*
|
||||
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
|
||||
*/
|
||||
|
||||
import type { ConversationResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { deleteConversation } from '../../services/database';
|
||||
import type { IConversationOperations, IMessageStateManager } from './types';
|
||||
import type { ConversationResponse } from '../../../types/dto';
|
||||
import { messageService } from '../../../services/messageService';
|
||||
import { deleteConversation } from '../../../services/database';
|
||||
import type { IConversationOperations } from '../types';
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
|
||||
|
||||
export class ConversationOperations implements IConversationOperations {
|
||||
private stateManager: IMessageStateManager;
|
||||
|
||||
constructor(stateManager: IMessageStateManager) {
|
||||
this.stateManager = stateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建私聊会话
|
||||
*/
|
||||
async createConversation(userId: string): Promise<ConversationResponse | null> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
const conversation = await messageService.createConversation(userId);
|
||||
|
||||
// 添加到会话列表
|
||||
this.stateManager.updateConversation(conversation);
|
||||
store.updateConversation(conversation);
|
||||
|
||||
// 通知更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return conversation;
|
||||
} catch (error) {
|
||||
console.error('[ConversationOperations] 创建会话失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'createConversation' },
|
||||
timestamp: Date.now(),
|
||||
@@ -48,7 +47,8 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 本地更新会话信息
|
||||
*/
|
||||
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
if (!conversation) {
|
||||
console.warn('[ConversationOperations] 更新会话失败,会话不存在:', conversationId);
|
||||
return;
|
||||
@@ -59,12 +59,12 @@ export class ConversationOperations implements IConversationOperations {
|
||||
...updates,
|
||||
};
|
||||
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
|
||||
// 通知更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
@@ -73,17 +73,18 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 仅自己删除会话后的本地状态回收
|
||||
*/
|
||||
removeConversation(conversationId: string): void {
|
||||
this.stateManager.removeConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
store.removeConversation(conversationId);
|
||||
|
||||
// 通知更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const unreadCount = this.stateManager.getUnreadCount();
|
||||
this.stateManager.notifySubscribers({
|
||||
const unreadCount = store.getUnreadCount();
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: unreadCount.total,
|
||||
@@ -102,15 +103,16 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 清空会话列表
|
||||
*/
|
||||
clearConversations(): void {
|
||||
this.stateManager.reset();
|
||||
const store = useMessageStore.getState();
|
||||
store.reset();
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: [] },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: 0,
|
||||
@@ -124,7 +126,8 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 更新单个会话未读数
|
||||
*/
|
||||
updateUnreadCount(conversationId: string, count: number): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
if (!conversation) {
|
||||
console.warn('[ConversationOperations] 更新未读数失败,会话不存在:', conversationId);
|
||||
return;
|
||||
@@ -138,28 +141,28 @@ export class ConversationOperations implements IConversationOperations {
|
||||
...conversation,
|
||||
unread_count: count,
|
||||
};
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
|
||||
// 更新总未读数
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
this.stateManager.setUnreadCount(
|
||||
const currentUnread = store.getUnreadCount();
|
||||
store.setUnreadCount(
|
||||
Math.max(0, currentUnread.total + diff),
|
||||
currentUnread.system
|
||||
);
|
||||
|
||||
// 通知更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: this.stateManager.getUnreadCount().total,
|
||||
systemUnreadCount: this.stateManager.getUnreadCount().system,
|
||||
totalUnreadCount: store.getUnreadCount().total,
|
||||
systemUnreadCount: store.getUnreadCount().system,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
@@ -168,7 +171,8 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 增加会话未读数
|
||||
*/
|
||||
incrementUnreadCount(conversationId: string): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
if (!conversation) return;
|
||||
|
||||
const prevUnreadCount = conversation.unread_count || 0;
|
||||
@@ -180,27 +184,27 @@ export class ConversationOperations implements IConversationOperations {
|
||||
unread_count: newUnreadCount,
|
||||
};
|
||||
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
this.stateManager.setUnreadCount(
|
||||
const currentUnread = store.getUnreadCount();
|
||||
store.setUnreadCount(
|
||||
currentUnread.total + 1,
|
||||
currentUnread.system
|
||||
);
|
||||
|
||||
// 通知更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: this.stateManager.getUnreadCount().total,
|
||||
systemUnreadCount: this.stateManager.getUnreadCount().system,
|
||||
totalUnreadCount: store.getUnreadCount().total,
|
||||
systemUnreadCount: store.getUnreadCount().system,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
@@ -209,10 +213,11 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 设置系统消息未读数
|
||||
*/
|
||||
setSystemUnreadCount(count: number): void {
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
this.stateManager.setUnreadCount(currentUnread.total, count);
|
||||
const store = useMessageStore.getState();
|
||||
const currentUnread = store.getUnreadCount();
|
||||
store.setUnreadCount(currentUnread.total, count);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: currentUnread.total,
|
||||
@@ -226,10 +231,11 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 增加系统消息未读数
|
||||
*/
|
||||
incrementSystemUnreadCount(): void {
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
this.stateManager.setUnreadCount(currentUnread.total, currentUnread.system + 1);
|
||||
const store = useMessageStore.getState();
|
||||
const currentUnread = store.getUnreadCount();
|
||||
store.setUnreadCount(currentUnread.total, currentUnread.system + 1);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: currentUnread.total,
|
||||
@@ -243,11 +249,12 @@ export class ConversationOperations implements IConversationOperations {
|
||||
* 减少系统消息未读数
|
||||
*/
|
||||
decrementSystemUnreadCount(count = 1): void {
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
const store = useMessageStore.getState();
|
||||
const currentUnread = store.getUnreadCount();
|
||||
const newSystemCount = Math.max(0, currentUnread.system - count);
|
||||
this.stateManager.setUnreadCount(currentUnread.total, newSystemCount);
|
||||
store.setUnreadCount(currentUnread.total, newSystemCount);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: currentUnread.total,
|
||||
@@ -3,11 +3,11 @@
|
||||
* 处理消息去重逻辑,防止重复处理相同的消息
|
||||
*/
|
||||
|
||||
import type { IMessageDeduplication } from './types';
|
||||
import type { IMessageDeduplication } from '../types';
|
||||
import {
|
||||
PROCESSED_MESSAGE_ID_EXPIRY,
|
||||
PROCESSED_MESSAGE_ID_MAX_SIZE,
|
||||
} from './constants';
|
||||
} from '../constants';
|
||||
|
||||
export class MessageDeduplication implements IMessageDeduplication {
|
||||
// 已处理消息ID集合(用于去重,防止ACK消息和正常消息重复处理)
|
||||
@@ -1,19 +1,20 @@
|
||||
/**
|
||||
* 消息发送服务
|
||||
* 处理消息发送相关逻辑
|
||||
*
|
||||
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
|
||||
*/
|
||||
|
||||
import type { MessageResponse, MessageSegment, ConversationResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { saveMessage } from '../../services/database';
|
||||
import type { IMessageSendService, IMessageStateManager } from './types';
|
||||
import type { MessageResponse, MessageSegment, ConversationResponse } from '../../../types/dto';
|
||||
import { messageService } from '../../../services/messageService';
|
||||
import { saveMessage } from '../../../services/database';
|
||||
import type { IMessageSendService } from '../types';
|
||||
import { useMessageStore, subscriptionManager, mergeMessagesById } from '../store';
|
||||
|
||||
export class MessageSendService implements IMessageSendService {
|
||||
private stateManager: IMessageStateManager;
|
||||
private getCurrentUserId: () => string | null;
|
||||
|
||||
constructor(stateManager: IMessageStateManager, getCurrentUserId: () => string | null) {
|
||||
this.stateManager = stateManager;
|
||||
constructor(getCurrentUserId: () => string | null) {
|
||||
this.getCurrentUserId = getCurrentUserId;
|
||||
}
|
||||
|
||||
@@ -25,6 +26,8 @@ export class MessageSendService implements IMessageSendService {
|
||||
segments: MessageSegment[],
|
||||
options?: { replyToId?: string }
|
||||
): Promise<MessageResponse | null> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
// 调用API发送
|
||||
const response = await messageService.sendMessage(conversationId, {
|
||||
@@ -36,7 +39,7 @@ export class MessageSendService implements IMessageSendService {
|
||||
const currentUserId = this.getCurrentUserId();
|
||||
|
||||
// 添加到本地消息列表
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const newMessage: MessageResponse = {
|
||||
id: response.id,
|
||||
conversation_id: conversationId,
|
||||
@@ -47,11 +50,11 @@ export class MessageSendService implements IMessageSendService {
|
||||
status: 'normal',
|
||||
};
|
||||
|
||||
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]);
|
||||
this.stateManager.setMessages(conversationId, updatedMessages);
|
||||
const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
|
||||
store.setMessages(conversationId, updatedMessages);
|
||||
|
||||
// 关键:立即广播消息列表更新
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -66,7 +69,7 @@ export class MessageSendService implements IMessageSendService {
|
||||
this.updateConversationWithNewMessage(conversationId, newMessage);
|
||||
|
||||
// 通知消息已发送
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_sent',
|
||||
payload: { conversationId, message: newMessage },
|
||||
timestamp: Date.now(),
|
||||
@@ -97,7 +100,7 @@ export class MessageSendService implements IMessageSendService {
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[MessageSendService] 发送消息失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'sendMessage' },
|
||||
timestamp: Date.now(),
|
||||
@@ -113,7 +116,8 @@ export class MessageSendService implements IMessageSendService {
|
||||
conversationId: string,
|
||||
message: MessageResponse
|
||||
): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
const createdAt = message.created_at || new Date().toISOString();
|
||||
|
||||
if (conversation) {
|
||||
@@ -126,19 +130,7 @@ export class MessageSendService implements IMessageSendService {
|
||||
updated_at: createdAt,
|
||||
};
|
||||
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 message_id 合并消息并按 seq 排序
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,33 @@
|
||||
/**
|
||||
* 消息同步服务
|
||||
* 处理消息和会话的服务器同步逻辑
|
||||
*
|
||||
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
|
||||
*/
|
||||
|
||||
import type { ConversationResponse, MessageResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import type { ConversationResponse, MessageResponse } from '../../../types/dto';
|
||||
import { messageService } from '../../../services/messageService';
|
||||
import {
|
||||
getMessagesByConversation,
|
||||
getMaxSeq,
|
||||
saveMessagesBatch,
|
||||
saveConversationsWithRelatedCache,
|
||||
getMessagesBeforeSeq,
|
||||
} from '../../services/database';
|
||||
} from '../../../services/database';
|
||||
import {
|
||||
type IConversationListPagedSource,
|
||||
SqliteConversationListPagedSource,
|
||||
createRemoteConversationListSource,
|
||||
CONVERSATION_LIST_PAGE_SIZE,
|
||||
} from '../conversationListSources';
|
||||
import type { IMessageSyncService, IMessageStateManager, MessageManagerConversationListDeps } from './types';
|
||||
} from '../../conversationListSources';
|
||||
import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types';
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId, mergeMessagesById } from '../store';
|
||||
import { ReadReceiptManager } from './ReadReceiptManager';
|
||||
|
||||
export class MessageSyncService implements IMessageSyncService {
|
||||
private stateManager: IMessageStateManager;
|
||||
private getCurrentUserId: () => string | null;
|
||||
private readReceiptManager: ReadReceiptManager;
|
||||
private userCacheService: IUserCacheService;
|
||||
|
||||
/** 远端会话列表(游标或页码) */
|
||||
private readonly remoteConversationListSource: IConversationListPagedSource;
|
||||
@@ -35,12 +40,14 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
private deferredConversationRefresh = false;
|
||||
|
||||
constructor(
|
||||
stateManager: IMessageStateManager,
|
||||
getCurrentUserId: () => string | null,
|
||||
readReceiptManager: ReadReceiptManager,
|
||||
userCacheService: IUserCacheService,
|
||||
deps?: MessageManagerConversationListDeps
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.getCurrentUserId = getCurrentUserId;
|
||||
this.readReceiptManager = readReceiptManager;
|
||||
this.userCacheService = userCacheService;
|
||||
|
||||
const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE;
|
||||
this.remoteConversationListSource =
|
||||
@@ -57,7 +64,9 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 获取会话列表
|
||||
*/
|
||||
async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> {
|
||||
if (this.stateManager.isLoading() && !forceRefresh) {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
if (store.isLoading() && !forceRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,7 +75,7 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
if (__DEV__) {
|
||||
console.log('[MessageSyncService] defer fetchConversations', {
|
||||
source,
|
||||
activeConversationId: this.stateManager.getActiveConversation(),
|
||||
activeConversationId: store.getActiveConversation(),
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -76,12 +85,12 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
console.log('[MessageSyncService] fetchConversations', {
|
||||
source,
|
||||
forceRefresh,
|
||||
activeConversationId: this.stateManager.getActiveConversation(),
|
||||
activeConversationId: store.getActiveConversation(),
|
||||
});
|
||||
}
|
||||
|
||||
// 非强制刷新且内存为空:先用本地源暖机
|
||||
const currentState = this.stateManager.getState();
|
||||
const currentState = store.getState();
|
||||
if (!forceRefresh && currentState.conversations.size === 0) {
|
||||
const warmed = await this.hydrateConversationsFromLocalSource();
|
||||
if (warmed) {
|
||||
@@ -89,10 +98,10 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
this.stateManager.setLoading(true);
|
||||
const emitLoadingToUi = this.stateManager.getConversations().length === 0;
|
||||
store.setLoading(true);
|
||||
const emitLoadingToUi = store.getConversations().length === 0;
|
||||
if (emitLoadingToUi) {
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_loading',
|
||||
payload: { loading: true },
|
||||
timestamp: Date.now(),
|
||||
@@ -103,32 +112,41 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
this.remoteConversationListSource.restart();
|
||||
const page = await this.remoteConversationListSource.loadNext();
|
||||
|
||||
currentState.conversations.clear();
|
||||
// 使用 zustand set 函数正确更新状态
|
||||
const newConversations = new Map<string, ConversationResponse>();
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
currentState.conversations.set(normalizedConv.id, normalizedConv);
|
||||
newConversations.set(normalizedConv.id, normalizedConv);
|
||||
});
|
||||
|
||||
this.updateConversationList();
|
||||
this.recomputeConversationTotalUnread();
|
||||
// 使用 store.setConversations 会自动排序并更新 conversationList
|
||||
store.setConversations(newConversations);
|
||||
|
||||
// 计算并更新未读数
|
||||
const totalUnread = Array.from(newConversations.values()).reduce(
|
||||
(sum, conv) => sum + (conv.unread_count || 0),
|
||||
0
|
||||
);
|
||||
store.setUnreadCount(totalUnread, store.getUnreadCount().system);
|
||||
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取会话列表失败:', error);
|
||||
if (this.stateManager.getConversations().length === 0) {
|
||||
if (store.getConversations().length === 0) {
|
||||
const recovered = await this.hydrateConversationsFromLocalSource();
|
||||
if (recovered) {
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
}
|
||||
}
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'fetchConversations' },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
this.stateManager.setLoading(false);
|
||||
store.setLoading(false);
|
||||
if (emitLoadingToUi) {
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_loading',
|
||||
payload: { loading: false },
|
||||
timestamp: Date.now(),
|
||||
@@ -141,10 +159,12 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 会话列表加载下一页
|
||||
*/
|
||||
async loadMoreConversations(): Promise<void> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
if (!this.remoteConversationListSource.hasMore) {
|
||||
return;
|
||||
}
|
||||
if (this.loadingMoreConversations || this.stateManager.isLoading()) {
|
||||
if (this.loadingMoreConversations || store.isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,14 +174,14 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
this.stateManager.updateConversation(normalizedConv);
|
||||
store.updateConversation(normalizedConv);
|
||||
});
|
||||
|
||||
this.recomputeConversationTotalUnread();
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 加载更多会话失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'loadMoreConversations' },
|
||||
timestamp: Date.now(),
|
||||
@@ -175,12 +195,14 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 获取单个会话详情
|
||||
*/
|
||||
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
|
||||
try {
|
||||
const detail = await messageService.getConversationById(normalizedId);
|
||||
if (detail) {
|
||||
const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse);
|
||||
this.stateManager.updateConversation(normalizedDetail);
|
||||
store.updateConversation(normalizedDetail);
|
||||
return normalizedDetail;
|
||||
}
|
||||
return null;
|
||||
@@ -194,15 +216,17 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 获取会话消息(增量同步)
|
||||
*/
|
||||
async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
// 防止重复加载
|
||||
if (this.stateManager.isLoadingMessages(conversationId)) {
|
||||
if (store.isLoadingMessages(conversationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateManager.setLoadingMessages(conversationId, true);
|
||||
store.setLoadingMessages(conversationId, true);
|
||||
|
||||
try {
|
||||
const existingMessagesAtStart = this.stateManager.getMessages(conversationId);
|
||||
const existingMessagesAtStart = store.getMessages(conversationId);
|
||||
const hasInMemoryMessages = existingMessagesAtStart.length > 0;
|
||||
let baselineMaxSeq = hasInMemoryMessages
|
||||
? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0)
|
||||
@@ -227,8 +251,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
created_at: m.createdAt,
|
||||
}));
|
||||
|
||||
this.stateManager.setMessages(conversationId, formattedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setMessages(conversationId, formattedMessages);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -239,8 +263,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
});
|
||||
} else {
|
||||
// 冷启动兜底:先下发空列表事件
|
||||
this.stateManager.setMessages(conversationId, []);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setMessages(conversationId, []);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -261,11 +285,11 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
const snapshotMessages = snapshotResp?.messages || [];
|
||||
|
||||
if (snapshotMessages.length > 0) {
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedSnapshot = this.mergeMessagesById(existingMessages, snapshotMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedSnapshot);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const mergedSnapshot = mergeMessagesById(existingMessages, snapshotMessages);
|
||||
store.setMessages(conversationId, mergedSnapshot);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -300,11 +324,11 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
const newMessages = incrementalResp?.messages || [];
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const mergedMessages = mergeMessagesById(existingMessages, newMessages);
|
||||
store.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -340,11 +364,11 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
|
||||
if (response?.messages && response.messages.length > 0) {
|
||||
const newMessages = response.messages;
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const mergedMessages = mergeMessagesById(existingMessages, newMessages);
|
||||
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setMessages(conversationId, mergedMessages);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -373,13 +397,33 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取消息失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'fetchMessages', conversationId },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
this.stateManager.setLoadingMessages(conversationId, false);
|
||||
// 异步填充用户信息(不阻塞消息显示)
|
||||
const currentMessages = store.getMessages(conversationId);
|
||||
if (currentMessages.length > 0) {
|
||||
this.userCacheService.enrichMessagesWithSenderInfo(
|
||||
conversationId,
|
||||
currentMessages,
|
||||
(convId, enrichedMessages) => {
|
||||
store.setMessages(convId, enrichedMessages);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId: convId,
|
||||
messages: enrichedMessages,
|
||||
source: 'sender_enriched',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
store.setLoadingMessages(conversationId, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +431,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 加载更多历史消息
|
||||
*/
|
||||
async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
// 先从本地获取
|
||||
const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit);
|
||||
@@ -402,11 +448,11 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
created_at: m.createdAt,
|
||||
}));
|
||||
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
store.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: { conversationId, messages: mergedMessages, source: 'local_history' },
|
||||
timestamp: Date.now(),
|
||||
@@ -434,11 +480,11 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
segments: m.segments,
|
||||
})));
|
||||
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
store.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: { conversationId, messages: mergedMessages, source: 'server_history' },
|
||||
timestamp: Date.now(),
|
||||
@@ -458,6 +504,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
* 获取未读数
|
||||
*/
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
const [unreadData, systemUnreadData] = await Promise.all([
|
||||
messageService.getUnreadCount(),
|
||||
@@ -467,30 +515,32 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
const totalUnread = unreadData?.total_unread_count ?? 0;
|
||||
const systemUnread = systemUnreadData?.unread_count ?? 0;
|
||||
|
||||
this.stateManager.setUnreadCount(totalUnread, systemUnread);
|
||||
store.setUnreadCount(totalUnread, systemUnread);
|
||||
|
||||
// 服务端汇总未读为 0 时,清掉内存中残留的红点
|
||||
if (totalUnread === 0) {
|
||||
const currentState = this.stateManager.getState();
|
||||
const currentConversations = store.getState().conversations;
|
||||
let anyCleared = false;
|
||||
for (const [cid, conv] of currentState.conversations) {
|
||||
const newConversations = new Map(currentConversations);
|
||||
for (const [cid, conv] of newConversations) {
|
||||
if ((conv.unread_count || 0) > 0) {
|
||||
currentState.conversations.set(cid, { ...conv, unread_count: 0 });
|
||||
newConversations.set(cid, { ...conv, unread_count: 0 });
|
||||
anyCleared = true;
|
||||
}
|
||||
}
|
||||
if (anyCleared) {
|
||||
this.updateConversationList();
|
||||
// 使用 zustand set 函数正确更新状态
|
||||
store.setConversations(newConversations);
|
||||
this.persistConversationListCache();
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: totalUnread,
|
||||
@@ -522,56 +572,43 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
|
||||
if (!this.stateManager.getActiveConversation()) return false;
|
||||
const store = useMessageStore.getState();
|
||||
if (!store.getActiveConversation()) return false;
|
||||
if (!forceRefresh) return false;
|
||||
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh';
|
||||
}
|
||||
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
// hooks-initial-refresh 是冷启动兜底刷新,不应该被延迟
|
||||
return source === 'sse-reconnect' || source === 'prefetch';
|
||||
}
|
||||
|
||||
private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse {
|
||||
const id = this.normalizeConversationId(conv.id);
|
||||
return {
|
||||
const id = normalizeConversationId(conv.id);
|
||||
// 应用已读保护逻辑
|
||||
return this.readReceiptManager.applyConversationFromFetch({
|
||||
...conv,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
private updateConversationList(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
const list = Array.from(currentState.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;
|
||||
});
|
||||
currentState.conversationList = list;
|
||||
}
|
||||
|
||||
private recomputeConversationTotalUnread(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
currentState.totalUnreadCount = Array.from(currentState.conversations.values()).reduce(
|
||||
const store = useMessageStore.getState();
|
||||
const totalUnread = Array.from(store.getState().conversations.values()).reduce(
|
||||
(sum, conv) => sum + (conv.unread_count || 0),
|
||||
0
|
||||
);
|
||||
// 使用 zustand set 函数正确更新状态
|
||||
store.setUnreadCount(totalUnread, store.getUnreadCount().system);
|
||||
}
|
||||
|
||||
private emitConversationListAndUnreadUpdates(): void {
|
||||
const store = useMessageStore.getState();
|
||||
this.persistConversationListCache();
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const unreadCount = this.stateManager.getUnreadCount();
|
||||
this.stateManager.notifySubscribers({
|
||||
const unreadCount = store.getUnreadCount();
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: unreadCount.total,
|
||||
@@ -582,7 +619,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
}
|
||||
|
||||
private persistConversationListCache(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
const store = useMessageStore.getState();
|
||||
const currentState = store.getState();
|
||||
const list = Array.from(currentState.conversations.values());
|
||||
const users = list.flatMap(conv => [
|
||||
...(conv.participants || []),
|
||||
@@ -598,33 +636,33 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
}
|
||||
|
||||
private async hydrateConversationsFromLocalSource(): Promise<boolean> {
|
||||
const store = useMessageStore.getState();
|
||||
this.localConversationListSource.restart();
|
||||
const page = await this.localConversationListSource.loadNext();
|
||||
if (!page.items.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentState = this.stateManager.getState();
|
||||
currentState.conversations.clear();
|
||||
// 使用 zustand set 函数正确更新状态
|
||||
const newConversations = new Map<string, ConversationResponse>();
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
currentState.conversations.set(normalizedConv.id, normalizedConv);
|
||||
newConversations.set(normalizedConv.id, normalizedConv);
|
||||
});
|
||||
|
||||
this.updateConversationList();
|
||||
this.recomputeConversationTotalUnread();
|
||||
// 使用 store.setConversations 会自动排序并更新 conversationList
|
||||
store.setConversations(newConversations);
|
||||
|
||||
// 计算并更新未读数
|
||||
const totalUnread = Array.from(newConversations.values()).reduce(
|
||||
(sum, conv) => sum + (conv.unread_count || 0),
|
||||
0
|
||||
);
|
||||
store.setUnreadCount(totalUnread, store.getUnreadCount().system);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
|
||||
if (incoming.length === 0) return existing;
|
||||
|
||||
@@ -638,6 +676,6 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
return [...incomingAsc, ...existing];
|
||||
}
|
||||
|
||||
return this.mergeMessagesById(existing, incomingAsc);
|
||||
return mergeMessagesById(existing, incomingAsc);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
/**
|
||||
* 已读回执管理器
|
||||
* 管理消息已读状态的同步和保护
|
||||
*
|
||||
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
|
||||
*/
|
||||
|
||||
import type { ConversationResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { markConversationAsRead, updateConversationCacheUnreadCount } from '../../services/database';
|
||||
import type { ReadStateRecord, IReadReceiptManager, IMessageStateManager } from './types';
|
||||
import { READ_STATE_PROTECTION_DELAY } from './constants';
|
||||
import type { ConversationResponse } from '../../../types/dto';
|
||||
import { messageService } from '../../../services/messageService';
|
||||
import { markConversationAsRead, updateConversationCacheUnreadCount } from '../../../services/database';
|
||||
import type { ReadStateRecord, IReadReceiptManager } from '../types';
|
||||
import { READ_STATE_PROTECTION_DELAY } from '../constants';
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
|
||||
|
||||
export class ReadReceiptManager implements IReadReceiptManager {
|
||||
private stateManager: IMessageStateManager;
|
||||
|
||||
/**
|
||||
* 正在进行中的已读 API 请求集合
|
||||
* 用于防止 fetchConversations 的服务器数据覆盖乐观已读状态
|
||||
@@ -23,17 +24,14 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
*/
|
||||
private readStateVersion: number = 0;
|
||||
|
||||
constructor(stateManager: IMessageStateManager) {
|
||||
this.stateManager = stateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记会话已读
|
||||
* 关键方法:确保已读状态在所有组件间同步
|
||||
*/
|
||||
async markAsRead(conversationId: string, seq: number): Promise<void> {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
const conversation = this.stateManager.getConversation(normalizedId);
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(normalizedId);
|
||||
|
||||
if (!conversation) {
|
||||
console.warn('[ReadReceiptManager] 会话不存在:', normalizedId);
|
||||
@@ -71,17 +69,17 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
my_last_read_seq: Math.max(Number(conversation.my_last_read_seq || 0), seq),
|
||||
};
|
||||
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
const currentUnread = store.getUnreadCount();
|
||||
const newTotalUnread = Math.max(0, currentUnread.total - prevUnreadCount);
|
||||
this.stateManager.setUnreadCount(newTotalUnread, currentUnread.system);
|
||||
store.setUnreadCount(newTotalUnread, currentUnread.system);
|
||||
|
||||
// 3. 更新本地数据库
|
||||
markConversationAsRead(normalizedId).catch(console.error);
|
||||
|
||||
// 4. 立即通知所有订阅者
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_read',
|
||||
payload: {
|
||||
conversationId: normalizedId,
|
||||
@@ -91,7 +89,7 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: newTotalUnread,
|
||||
@@ -107,10 +105,10 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
console.error('[ReadReceiptManager] 标记已读API失败:', error);
|
||||
|
||||
// 失败时回滚状态
|
||||
this.stateManager.updateConversation(conversation);
|
||||
this.stateManager.setUnreadCount(currentUnread.total, currentUnread.system);
|
||||
store.updateConversation(conversation);
|
||||
store.setUnreadCount(currentUnread.total, currentUnread.system);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_read',
|
||||
payload: {
|
||||
conversationId: normalizedId,
|
||||
@@ -146,18 +144,19 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
* 标记所有消息已读
|
||||
*/
|
||||
async markAllAsRead(): Promise<void> {
|
||||
const conversations = this.stateManager.getState().conversations;
|
||||
const prevTotalUnread = this.stateManager.getUnreadCount().total;
|
||||
const currentSystem = this.stateManager.getUnreadCount().system;
|
||||
const store = useMessageStore.getState();
|
||||
const conversations = store.conversations;
|
||||
const prevTotalUnread = store.getUnreadCount().total;
|
||||
const currentSystem = store.getUnreadCount().system;
|
||||
|
||||
// 乐观更新
|
||||
conversations.forEach(conv => {
|
||||
conv.unread_count = 0;
|
||||
});
|
||||
|
||||
this.stateManager.setUnreadCount(0, currentSystem);
|
||||
store.setUnreadCount(0, currentSystem);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: 0,
|
||||
@@ -182,9 +181,9 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
} catch (error) {
|
||||
console.error('[ReadReceiptManager] 标记所有已读失败:', error);
|
||||
// 回滚
|
||||
this.stateManager.setUnreadCount(prevTotalUnread, currentSystem);
|
||||
store.setUnreadCount(prevTotalUnread, currentSystem);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: prevTotalUnread,
|
||||
@@ -258,18 +257,11 @@ export class ReadReceiptManager implements IReadReceiptManager {
|
||||
return this.pendingReadMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化会话ID
|
||||
*/
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已读保护状态智能合并会话
|
||||
*/
|
||||
applyConversationFromFetch(conv: ConversationResponse): ConversationResponse {
|
||||
const id = this.normalizeConversationId(conv.id);
|
||||
const id = normalizeConversationId(conv.id);
|
||||
const readRecord = this.pendingReadMap.get(id);
|
||||
|
||||
let next: ConversationResponse;
|
||||
@@ -3,10 +3,10 @@
|
||||
* 管理用户信息的缓存和获取
|
||||
*/
|
||||
|
||||
import type { MessageResponse, UserDTO } from '../../types/dto';
|
||||
import { api } from '../../services/api';
|
||||
import { getUserCache, saveUserCache } from '../../services/database';
|
||||
import type { IUserCacheService } from './types';
|
||||
import type { MessageResponse, UserDTO } from '../../../types/dto';
|
||||
import { api } from '../../../services/api';
|
||||
import { getUserCache, saveUserCache } from '../../../services/database';
|
||||
import type { IUserCacheService } from '../types';
|
||||
|
||||
export class UserCacheService implements IUserCacheService {
|
||||
// 正在获取用户信息的请求映射(用于去重)
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* WebSocket 消息处理器
|
||||
* 处理所有来自 WebSocket 的消息事件
|
||||
*
|
||||
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
|
||||
*/
|
||||
|
||||
import type {
|
||||
@@ -12,22 +14,21 @@ import type {
|
||||
WSGroupRecallMessage,
|
||||
WSGroupTypingMessage,
|
||||
WSGroupNoticeMessage,
|
||||
} from '../../services/wsService';
|
||||
import { wsService, GroupNoticeType } from '../../services/wsService';
|
||||
import { vibrateOnMessage } from '../../services/messageVibrationService';
|
||||
import { saveMessage, updateMessageStatus } from '../../services/database';
|
||||
import type { MessageResponse, ConversationResponse } from '../../types/dto';
|
||||
} from '../../../services/wsService';
|
||||
import { wsService, GroupNoticeType } from '../../../services/wsService';
|
||||
import { vibrateOnMessage } from '../../../services/messageVibrationService';
|
||||
import { saveMessage, updateMessageStatus } from '../../../services/database';
|
||||
import type { MessageResponse, ConversationResponse } from '../../../types/dto';
|
||||
import type {
|
||||
IWSMessageHandler,
|
||||
IMessageStateManager,
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
HandleNewMessageOptions,
|
||||
} from './types';
|
||||
import { MAX_FLUSH_ITERATIONS } from './constants';
|
||||
} from '../types';
|
||||
import { MAX_FLUSH_ITERATIONS } from '../constants';
|
||||
import { useMessageStore, subscriptionManager, normalizeConversationId, mergeMessagesById } from '../store';
|
||||
|
||||
export class WSMessageHandler implements IWSMessageHandler {
|
||||
private stateManager: IMessageStateManager;
|
||||
private deduplication: IMessageDeduplication;
|
||||
private userCacheService: IUserCacheService;
|
||||
private getCurrentUserId: () => string | null;
|
||||
@@ -45,7 +46,6 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
private bufferedReadReceipts: Array<WSReadMessage | WSGroupReadMessage> = [];
|
||||
|
||||
constructor(
|
||||
stateManager: IMessageStateManager,
|
||||
deduplication: IMessageDeduplication,
|
||||
userCacheService: IUserCacheService,
|
||||
getCurrentUserId: () => string | null,
|
||||
@@ -56,7 +56,6 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
fetchMessages: (conversationId: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.deduplication = deduplication;
|
||||
this.userCacheService = userCacheService;
|
||||
this.getCurrentUserId = getCurrentUserId;
|
||||
@@ -74,9 +73,11 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
this.sseUnsubscribe();
|
||||
}
|
||||
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
// 监听私聊消息
|
||||
wsService.on('chat', (message: WSChatMessage) => {
|
||||
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) {
|
||||
if (!store.getState().isInitialized || this.isBootstrapping) {
|
||||
this.bufferedChatMessages.push(message);
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +86,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// 监听群聊消息
|
||||
wsService.on('group_message', (message: WSGroupChatMessage) => {
|
||||
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) {
|
||||
if (!store.getState().isInitialized || this.isBootstrapping) {
|
||||
this.bufferedChatMessages.push(message);
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +95,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// 监听私聊已读回执
|
||||
wsService.on('read', (message: WSReadMessage) => {
|
||||
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) {
|
||||
if (!store.getState().isInitialized || this.isBootstrapping) {
|
||||
this.bufferedReadReceipts.push(message);
|
||||
return;
|
||||
}
|
||||
@@ -103,7 +104,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// 监听群聊已读回执
|
||||
wsService.on('group_read', (message: WSGroupReadMessage) => {
|
||||
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) {
|
||||
if (!store.getState().isInitialized || this.isBootstrapping) {
|
||||
this.bufferedReadReceipts.push(message);
|
||||
return;
|
||||
}
|
||||
@@ -132,8 +133,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// 监听连接状态
|
||||
wsService.onConnect(() => {
|
||||
this.stateManager.setSSEConnected(true);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setSSEConnected(true);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: true },
|
||||
timestamp: Date.now(),
|
||||
@@ -143,7 +144,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
const now = Date.now();
|
||||
if (now - this.lastReconnectSyncAt > 1500) {
|
||||
this.lastReconnectSyncAt = now;
|
||||
const activeConversation = this.stateManager.getActiveConversation();
|
||||
const activeConversation = store.getActiveConversation();
|
||||
|
||||
if (!activeConversation) {
|
||||
this.fetchConversationsCallback(true, 'sse-reconnect').catch(error => {
|
||||
@@ -164,8 +165,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
});
|
||||
|
||||
wsService.onDisconnect(() => {
|
||||
this.stateManager.setSSEConnected(false);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setSSEConnected(false);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: false },
|
||||
timestamp: Date.now(),
|
||||
@@ -187,7 +188,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
* 检查是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.stateManager.isConnected();
|
||||
return useMessageStore.getState().isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,8 +243,9 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean },
|
||||
options?: HandleNewMessageOptions
|
||||
): Promise<void> {
|
||||
const store = useMessageStore.getState();
|
||||
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
|
||||
const normalizedConversationId = this.normalizeConversationId(conversation_id);
|
||||
const normalizedConversationId = normalizeConversationId(conversation_id);
|
||||
const currentUserId = this.getCurrentUserId();
|
||||
const suppressUnreadIncrement = options?.suppressUnreadIncrement ?? false;
|
||||
const suppressVibration = options?.suppressVibration ?? false;
|
||||
@@ -260,13 +262,13 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
if (message.type === 'group_message' && sender_id && sender_id !== currentUserId && sender_id !== '10000') {
|
||||
this.userCacheService.getSenderInfo(sender_id).then(user => {
|
||||
if (user) {
|
||||
const currentMessages = this.stateManager.getMessages(normalizedConversationId);
|
||||
const currentMessages = store.getMessages(normalizedConversationId);
|
||||
if (currentMessages) {
|
||||
const updatedMessages = currentMessages.map(m =>
|
||||
m.id === id ? { ...m, sender: user } : m
|
||||
);
|
||||
this.stateManager.setMessages(normalizedConversationId, updatedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setMessages(normalizedConversationId, updatedMessages);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId: normalizedConversationId,
|
||||
@@ -293,14 +295,14 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
};
|
||||
|
||||
// 立即更新内存中的消息列表
|
||||
const existingMessages = this.stateManager.getMessages(normalizedConversationId);
|
||||
const existingMessages = store.getMessages(normalizedConversationId);
|
||||
const messageExists = existingMessages.some(m => String(m.id) === String(id));
|
||||
|
||||
if (!messageExists) {
|
||||
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]);
|
||||
this.stateManager.setMessages(normalizedConversationId, updatedMessages);
|
||||
const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
|
||||
store.setMessages(normalizedConversationId, updatedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId: normalizedConversationId,
|
||||
@@ -318,7 +320,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// 更新未读数
|
||||
const isCurrentUserMessage = sender_id === currentUserId;
|
||||
const isActiveConversation = normalizedConversationId === this.stateManager.getActiveConversation();
|
||||
const isActiveConversation = normalizedConversationId === store.getActiveConversation();
|
||||
|
||||
const shouldIncrementUnread =
|
||||
!suppressUnreadIncrement &&
|
||||
@@ -358,7 +360,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
});
|
||||
|
||||
// 通知收到新消息
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_received',
|
||||
payload: {
|
||||
conversationId: conversation_id,
|
||||
@@ -388,12 +390,12 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
*/
|
||||
handleRecallMessage(message: WSRecallMessage): void {
|
||||
const { conversation_id, message_id } = message;
|
||||
const normalizedConversationId = this.normalizeConversationId(conversation_id);
|
||||
const normalizedConversationId = normalizeConversationId(conversation_id);
|
||||
|
||||
this.markMessageAsRecalled(normalizedConversationId, message_id);
|
||||
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_recalled',
|
||||
payload: { conversationId: normalizedConversationId, messageId: message_id },
|
||||
timestamp: Date.now(),
|
||||
@@ -409,12 +411,12 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
*/
|
||||
handleGroupRecallMessage(message: WSGroupRecallMessage): void {
|
||||
const { conversation_id, message_id } = message;
|
||||
const normalizedConversationId = this.normalizeConversationId(conversation_id);
|
||||
const normalizedConversationId = normalizeConversationId(conversation_id);
|
||||
|
||||
this.markMessageAsRecalled(normalizedConversationId, message_id);
|
||||
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'message_recalled',
|
||||
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
|
||||
timestamp: Date.now(),
|
||||
@@ -429,10 +431,11 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
* 处理群聊输入状态
|
||||
*/
|
||||
handleGroupTyping(message: WSGroupTypingMessage): void {
|
||||
const store = useMessageStore.getState();
|
||||
const { group_id, user_id, is_typing } = message;
|
||||
const groupIdStr = String(group_id);
|
||||
|
||||
const currentTypingUsers = this.stateManager.getTypingUsers(groupIdStr);
|
||||
const currentTypingUsers = store.getTypingUsers(groupIdStr);
|
||||
let updatedTypingUsers: string[];
|
||||
|
||||
if (is_typing) {
|
||||
@@ -446,8 +449,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
}
|
||||
|
||||
if (updatedTypingUsers.length !== currentTypingUsers.length) {
|
||||
this.stateManager.setTypingUsers(groupIdStr, updatedTypingUsers);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setTypingUsers(groupIdStr, updatedTypingUsers);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'typing_status',
|
||||
payload: { groupId: groupIdStr, typingUsers: updatedTypingUsers },
|
||||
timestamp: Date.now(),
|
||||
@@ -459,6 +462,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
* 处理群通知
|
||||
*/
|
||||
handleGroupNotice(message: WSGroupNoticeMessage): void {
|
||||
const store = useMessageStore.getState();
|
||||
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
|
||||
const groupIdStr = String(group_id);
|
||||
|
||||
@@ -469,7 +473,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
if (mutedUserId && mutedUserId === currentUserId) {
|
||||
const isMuted = notice_type === 'muted';
|
||||
this.stateManager.setMutedStatus(groupIdStr, isMuted);
|
||||
store.setMutedStatus(groupIdStr, isMuted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +481,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
if (message_id && typeof seq === 'number' && seq > 0) {
|
||||
const conversationId = this.findConversationIdByGroupId(groupIdStr);
|
||||
if (conversationId) {
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const existingMessages = store.getMessages(conversationId);
|
||||
const exists = existingMessages.some(m => String(m.id) === String(message_id));
|
||||
if (!exists) {
|
||||
const noticeText = this.buildGroupNoticeText(notice_type, data);
|
||||
@@ -495,9 +499,9 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
};
|
||||
|
||||
const updatedMessages = [...existingMessages, systemNoticeMessage].sort((a, b) => a.seq - b.seq);
|
||||
this.stateManager.setMessages(conversationId, updatedMessages);
|
||||
store.setMessages(conversationId, updatedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
@@ -512,7 +516,7 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
}
|
||||
|
||||
// 通知订阅者群通知
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'group_notice',
|
||||
payload: {
|
||||
groupId: groupIdStr,
|
||||
@@ -528,25 +532,13 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private updateConversationWithNewMessage(
|
||||
conversationId: string,
|
||||
message: MessageResponse,
|
||||
createdAt: string
|
||||
): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
|
||||
if (conversation) {
|
||||
const updatedConv: ConversationResponse = {
|
||||
@@ -556,48 +548,50 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
last_message_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
};
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
}
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
private incrementUnreadCount(conversationId: string): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
if (conversation) {
|
||||
const prevUnreadCount = conversation.unread_count || 0;
|
||||
const updatedConv: ConversationResponse = {
|
||||
...conversation,
|
||||
unread_count: prevUnreadCount + 1,
|
||||
};
|
||||
this.stateManager.updateConversation(updatedConv);
|
||||
store.updateConversation(updatedConv);
|
||||
|
||||
const currentUnread = this.stateManager.getUnreadCount();
|
||||
this.stateManager.setUnreadCount(currentUnread.total + 1, currentUnread.system);
|
||||
const currentUnread = store.getUnreadCount();
|
||||
store.setUnreadCount(currentUnread.total + 1, currentUnread.system);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: this.stateManager.getUnreadCount().total,
|
||||
systemUnreadCount: this.stateManager.getUnreadCount().system,
|
||||
totalUnreadCount: store.getUnreadCount().total,
|
||||
systemUnreadCount: store.getUnreadCount().system,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
payload: { conversations: store.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private markMessageAsRecalled(conversationId: string, messageId: string): void {
|
||||
const messages = this.stateManager.getMessages(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const messages = store.getMessages(conversationId);
|
||||
if (!messages) return;
|
||||
|
||||
let changed = false;
|
||||
@@ -615,8 +609,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
this.stateManager.setMessages(conversationId, updatedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
store.setMessages(conversationId, updatedMessages);
|
||||
subscriptionManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: { conversationId, messages: updatedMessages },
|
||||
timestamp: Date.now(),
|
||||
@@ -624,7 +618,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
}
|
||||
|
||||
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void {
|
||||
const conversation = this.stateManager.getConversation(conversationId);
|
||||
const store = useMessageStore.getState();
|
||||
const conversation = store.getConversation(conversationId);
|
||||
if (!conversation?.last_message) return;
|
||||
if (String(conversation.last_message.id) !== String(messageId)) return;
|
||||
if (conversation.last_message.status === 'recalled') return;
|
||||
@@ -636,11 +631,12 @@ export class WSMessageHandler implements IWSMessageHandler {
|
||||
status: 'recalled',
|
||||
},
|
||||
};
|
||||
this.stateManager.updateConversation(updatedConversation);
|
||||
store.updateConversation(updatedConversation);
|
||||
}
|
||||
|
||||
private findConversationIdByGroupId(groupId: string): string | null {
|
||||
const conversations = this.stateManager.getConversations();
|
||||
const store = useMessageStore.getState();
|
||||
const conversations = store.getConversations();
|
||||
for (const conv of conversations) {
|
||||
if (String(conv.group?.id || '') === groupId) {
|
||||
return conv.id;
|
||||
26
src/stores/message/services/index.ts
Normal file
26
src/stores/message/services/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 消息服务层统一导出
|
||||
*
|
||||
* 重构说明:所有服务直接使用 zustand store,移除了 IMessageStateManager 接口依赖
|
||||
*/
|
||||
|
||||
// 消息去重服务
|
||||
export { MessageDeduplication, messageDeduplication } from './MessageDeduplication';
|
||||
|
||||
// 用户缓存服务
|
||||
export { UserCacheService, userCacheService } from './UserCacheService';
|
||||
|
||||
// 已读回执管理器
|
||||
export { ReadReceiptManager } from './ReadReceiptManager';
|
||||
|
||||
// 会话操作服务
|
||||
export { ConversationOperations } from './ConversationOperations';
|
||||
|
||||
// 消息发送服务
|
||||
export { MessageSendService } from './MessageSendService';
|
||||
|
||||
// 消息同步服务
|
||||
export { MessageSyncService } from './MessageSyncService';
|
||||
|
||||
// WebSocket 消息处理器
|
||||
export { WSMessageHandler } from './WSMessageHandler';
|
||||
428
src/stores/message/store.ts
Normal file
428
src/stores/message/store.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 消息状态 Zustand Store
|
||||
* 使用 zustand 进行状态管理,提供响应式状态更新
|
||||
*
|
||||
* 重构说明:
|
||||
* - 移除了 MessageStateManager 包装层
|
||||
* - 直接使用 zustand store 进行状态管理
|
||||
* - 订阅管理器集成到 store 中
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
ConversationResponse,
|
||||
MessageResponse,
|
||||
} from '../../types/dto';
|
||||
import type {
|
||||
MessageSubscriber,
|
||||
MessageEvent,
|
||||
} from './types';
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
/**
|
||||
* 消息状态接口(不包含订阅者,订阅者单独管理)
|
||||
*/
|
||||
export interface MessageState {
|
||||
// 会话相关
|
||||
conversations: Map<string, ConversationResponse>;
|
||||
conversationList: ConversationResponse[];
|
||||
|
||||
// 消息相关 - 按会话ID存储
|
||||
messagesMap: Map<string, MessageResponse[]>;
|
||||
|
||||
// 未读数
|
||||
totalUnreadCount: number;
|
||||
systemUnreadCount: number;
|
||||
|
||||
// 连接状态
|
||||
isWSConnected: boolean;
|
||||
|
||||
// 当前活动会话ID(用户正在查看的会话)
|
||||
currentConversationId: string | null;
|
||||
|
||||
// 加载状态
|
||||
isLoadingConversations: boolean;
|
||||
loadingMessagesSet: Set<string>;
|
||||
|
||||
// 初始化状态
|
||||
isInitialized: boolean;
|
||||
|
||||
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
||||
typingUsersMap: Map<string, string[]>;
|
||||
|
||||
// 当前用户的禁言状态 - 按群组ID存储
|
||||
mutedStatusMap: Map<string, boolean>;
|
||||
}
|
||||
|
||||
// ==================== Actions 接口 ====================
|
||||
|
||||
export interface MessageActions {
|
||||
// 获取状态
|
||||
getState: () => MessageState;
|
||||
getConversations: () => ConversationResponse[];
|
||||
getConversation: (conversationId: string) => ConversationResponse | null;
|
||||
getMessages: (conversationId: string) => MessageResponse[];
|
||||
getUnreadCount: () => { total: number; system: number };
|
||||
isConnected: () => boolean;
|
||||
isLoading: () => boolean;
|
||||
isLoadingMessages: (conversationId: string) => boolean;
|
||||
getTypingUsers: (groupId: string) => string[];
|
||||
isMuted: (groupId: string) => boolean;
|
||||
getActiveConversation: () => string | null;
|
||||
|
||||
// 设置状态
|
||||
setConversations: (conversations: Map<string, ConversationResponse>) => void;
|
||||
updateConversation: (conversation: ConversationResponse) => void;
|
||||
removeConversation: (conversationId: string) => void;
|
||||
setMessages: (conversationId: string, messages: MessageResponse[]) => void;
|
||||
addMessage: (conversationId: string, message: MessageResponse) => void;
|
||||
updateMessage: (conversationId: string, messageId: string, updates: Partial<MessageResponse>) => void;
|
||||
setUnreadCount: (total: number, system: number) => void;
|
||||
setSSEConnected: (connected: boolean) => void;
|
||||
setCurrentConversation: (conversationId: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setLoadingMessages: (conversationId: string, loading: boolean) => void;
|
||||
setTypingUsers: (groupId: string, users: string[]) => void;
|
||||
setMutedStatus: (groupId: string, isMuted: boolean) => void;
|
||||
setInitialized: (initialized: boolean) => void;
|
||||
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// ==================== 初始状态 ====================
|
||||
|
||||
const initialState: MessageState = {
|
||||
conversations: new Map(),
|
||||
conversationList: [],
|
||||
messagesMap: new Map(),
|
||||
totalUnreadCount: 0,
|
||||
systemUnreadCount: 0,
|
||||
isWSConnected: false,
|
||||
currentConversationId: null,
|
||||
isLoadingConversations: false,
|
||||
loadingMessagesSet: new Set(),
|
||||
isInitialized: false,
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
};
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 规范化会话ID
|
||||
*/
|
||||
export function normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话列表排序
|
||||
* 排序规则:置顶优先,再按最后消息时间排序
|
||||
*/
|
||||
function sortConversationList(conversations: Map<string, ConversationResponse>): ConversationResponse[] {
|
||||
return Array.from(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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 message_id 合并消息并按 seq 排序
|
||||
*/
|
||||
export function 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);
|
||||
}
|
||||
|
||||
// ==================== Store 创建 ====================
|
||||
|
||||
export type MessageStore = MessageState & MessageActions;
|
||||
|
||||
export const useMessageStore = create<MessageStore>((set, get) => ({
|
||||
// ==================== 初始状态 ====================
|
||||
...initialState,
|
||||
|
||||
// ==================== 获取状态 ====================
|
||||
|
||||
getState: () => {
|
||||
const state = get();
|
||||
return {
|
||||
conversations: state.conversations,
|
||||
conversationList: state.conversationList,
|
||||
messagesMap: state.messagesMap,
|
||||
totalUnreadCount: state.totalUnreadCount,
|
||||
systemUnreadCount: state.systemUnreadCount,
|
||||
isWSConnected: state.isWSConnected,
|
||||
currentConversationId: state.currentConversationId,
|
||||
isLoadingConversations: state.isLoadingConversations,
|
||||
loadingMessagesSet: state.loadingMessagesSet,
|
||||
isInitialized: state.isInitialized,
|
||||
typingUsersMap: state.typingUsersMap,
|
||||
mutedStatusMap: state.mutedStatusMap,
|
||||
};
|
||||
},
|
||||
|
||||
getConversations: () => {
|
||||
return get().conversationList;
|
||||
},
|
||||
|
||||
getConversation: (conversationId: string) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
return get().conversations.get(normalizedId) || null;
|
||||
},
|
||||
|
||||
getMessages: (conversationId: string) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
return get().messagesMap.get(normalizedId) || [];
|
||||
},
|
||||
|
||||
getUnreadCount: () => {
|
||||
const state = get();
|
||||
return {
|
||||
total: state.totalUnreadCount,
|
||||
system: state.systemUnreadCount,
|
||||
};
|
||||
},
|
||||
|
||||
isConnected: () => {
|
||||
return get().isWSConnected;
|
||||
},
|
||||
|
||||
isLoading: () => {
|
||||
return get().isLoadingConversations;
|
||||
},
|
||||
|
||||
isLoadingMessages: (conversationId: string) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
return get().loadingMessagesSet.has(normalizedId);
|
||||
},
|
||||
|
||||
getTypingUsers: (groupId: string) => {
|
||||
return get().typingUsersMap.get(groupId) || [];
|
||||
},
|
||||
|
||||
isMuted: (groupId: string) => {
|
||||
return get().mutedStatusMap.get(groupId) || false;
|
||||
},
|
||||
|
||||
getActiveConversation: () => {
|
||||
return get().currentConversationId;
|
||||
},
|
||||
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
setConversations: (conversations: Map<string, ConversationResponse>) => {
|
||||
const conversationList = sortConversationList(conversations);
|
||||
set({ conversations, conversationList });
|
||||
},
|
||||
|
||||
updateConversation: (conversation: ConversationResponse) => {
|
||||
const id = normalizeConversationId(conversation.id);
|
||||
set(state => {
|
||||
const newConversations = new Map(state.conversations);
|
||||
newConversations.set(id, { ...conversation, id });
|
||||
const conversationList = sortConversationList(newConversations);
|
||||
return { conversations: newConversations, conversationList };
|
||||
});
|
||||
},
|
||||
|
||||
removeConversation: (conversationId: string) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const target = state.conversations.get(normalizedId);
|
||||
const removedUnread = target?.unread_count || 0;
|
||||
|
||||
const newConversations = new Map(state.conversations);
|
||||
newConversations.delete(normalizedId);
|
||||
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.delete(normalizedId);
|
||||
|
||||
const newLoadingMessagesSet = new Set(state.loadingMessagesSet);
|
||||
newLoadingMessagesSet.delete(normalizedId);
|
||||
|
||||
const newTotalUnreadCount = Math.max(0, state.totalUnreadCount - removedUnread);
|
||||
|
||||
const newCurrentConversationId = state.currentConversationId === normalizedId
|
||||
? null
|
||||
: state.currentConversationId;
|
||||
|
||||
const conversationList = sortConversationList(newConversations);
|
||||
|
||||
return {
|
||||
conversations: newConversations,
|
||||
conversationList,
|
||||
messagesMap: newMessagesMap,
|
||||
totalUnreadCount: newTotalUnreadCount,
|
||||
currentConversationId: newCurrentConversationId,
|
||||
loadingMessagesSet: newLoadingMessagesSet,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setMessages: (conversationId: string, messages: MessageResponse[]) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.set(normalizedId, messages);
|
||||
return { messagesMap: newMessagesMap };
|
||||
});
|
||||
},
|
||||
|
||||
addMessage: (conversationId: string, message: MessageResponse) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const existing = state.messagesMap.get(normalizedId) || [];
|
||||
const exists = existing.some(m => String(m.id) === String(message.id));
|
||||
|
||||
if (exists) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updated = mergeMessagesById(existing, [message]);
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.set(normalizedId, updated);
|
||||
return { messagesMap: newMessagesMap };
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage: (conversationId: string, messageId: string, updates: Partial<MessageResponse>) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const messages = state.messagesMap.get(normalizedId);
|
||||
if (!messages) return state;
|
||||
|
||||
const updated = messages.map(m =>
|
||||
String(m.id) === String(messageId) ? { ...m, ...updates } : m
|
||||
);
|
||||
|
||||
const newMessagesMap = new Map(state.messagesMap);
|
||||
newMessagesMap.set(normalizedId, updated);
|
||||
return { messagesMap: newMessagesMap };
|
||||
});
|
||||
},
|
||||
|
||||
setUnreadCount: (total: number, system: number) => {
|
||||
set({ totalUnreadCount: total, systemUnreadCount: system });
|
||||
},
|
||||
|
||||
setSSEConnected: (connected: boolean) => {
|
||||
set({ isWSConnected: connected });
|
||||
},
|
||||
|
||||
setCurrentConversation: (conversationId: string | null) => {
|
||||
set({ currentConversationId: conversationId });
|
||||
},
|
||||
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ isLoadingConversations: loading });
|
||||
},
|
||||
|
||||
setLoadingMessages: (conversationId: string, loading: boolean) => {
|
||||
const normalizedId = normalizeConversationId(conversationId);
|
||||
set(state => {
|
||||
const newLoadingMessagesSet = new Set(state.loadingMessagesSet);
|
||||
if (loading) {
|
||||
newLoadingMessagesSet.add(normalizedId);
|
||||
} else {
|
||||
newLoadingMessagesSet.delete(normalizedId);
|
||||
}
|
||||
return { loadingMessagesSet: newLoadingMessagesSet };
|
||||
});
|
||||
},
|
||||
|
||||
setTypingUsers: (groupId: string, users: string[]) => {
|
||||
set(state => {
|
||||
const newTypingUsersMap = new Map(state.typingUsersMap);
|
||||
newTypingUsersMap.set(groupId, users);
|
||||
return { typingUsersMap: newTypingUsersMap };
|
||||
});
|
||||
},
|
||||
|
||||
setMutedStatus: (groupId: string, isMuted: boolean) => {
|
||||
set(state => {
|
||||
const newMutedStatusMap = new Map(state.mutedStatusMap);
|
||||
newMutedStatusMap.set(groupId, isMuted);
|
||||
return { mutedStatusMap: newMutedStatusMap };
|
||||
});
|
||||
},
|
||||
|
||||
setInitialized: (initialized: boolean) => {
|
||||
set({ isInitialized: initialized });
|
||||
},
|
||||
|
||||
// ==================== 重置 ====================
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
...initialState,
|
||||
conversations: new Map(),
|
||||
messagesMap: new Map(),
|
||||
loadingMessagesSet: new Set(),
|
||||
typingUsersMap: new Map(),
|
||||
mutedStatusMap: new Map(),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
// ==================== 订阅管理器 ====================
|
||||
|
||||
/**
|
||||
* 订阅管理器类
|
||||
* 管理消息事件的订阅和通知
|
||||
*/
|
||||
export class SubscriptionManager {
|
||||
private subscribers: Set<MessageSubscriber> = new Set();
|
||||
|
||||
subscribe(subscriber: MessageSubscriber): () => void {
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
}
|
||||
|
||||
notifySubscribers(event: MessageEvent): void {
|
||||
this.subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber(event);
|
||||
} catch (error) {
|
||||
console.error('[SubscriptionManager] 订阅者执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.subscribers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局订阅管理器实例
|
||||
export const subscriptionManager = new SubscriptionManager();
|
||||
|
||||
// ==================== 便捷方法 ====================
|
||||
|
||||
/**
|
||||
* 通知所有订阅者
|
||||
*/
|
||||
export function notifySubscribers(event: MessageEvent): void {
|
||||
subscriptionManager.notifySubscribers(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息事件
|
||||
*/
|
||||
export function subscribeToMessages(subscriber: MessageSubscriber): () => void {
|
||||
return subscriptionManager.subscribe(subscriber);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
/**
|
||||
* MessageManager - 消息管理核心模块
|
||||
*
|
||||
* 向后兼容的重导出文件
|
||||
* 所有实现已迁移到 ./message 目录下的模块化组件
|
||||
* ⚠️ 此文件已废弃,保留仅用于向后兼容
|
||||
*
|
||||
* 新代码应该使用:
|
||||
* import { messageManager } from '@/stores/message'
|
||||
* import type { Message, Conversation, MessageEventManager } from '@/stores/message'
|
||||
* import type { MessageEvent, MessageSubscriber } from '@/stores/message'
|
||||
*
|
||||
* 此文件保持向后兼容性,原有导入方式仍然有效:
|
||||
* import { messageManager } from '@/stores/messageManager'
|
||||
* 此文件将在未来版本中移除
|
||||
*/
|
||||
|
||||
// 从新模块重导出所有内容
|
||||
@@ -18,10 +16,6 @@ export {
|
||||
messageManager,
|
||||
MessageManager,
|
||||
|
||||
// 状态管理器
|
||||
MessageStateManager,
|
||||
messageStateManager,
|
||||
|
||||
// 消息去重服务
|
||||
MessageDeduplication,
|
||||
messageDeduplication,
|
||||
@@ -68,7 +62,6 @@ export type {
|
||||
HandleNewMessageOptions,
|
||||
|
||||
// 服务接口类型
|
||||
IMessageStateManager,
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
IReadReceiptManager,
|
||||
|
||||
Reference in New Issue
Block a user