refactor(message): replace MessageStateManager with zustand store
Some checks failed
Frontend CI / ota-android (push) Successful in 11m15s
Frontend CI / build-and-push-web (push) Successful in 29m13s
Frontend CI / build-android-apk (push) Has been cancelled

- 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:
lafay
2026-03-31 18:22:24 +08:00
parent 1bee7ea551
commit 94c11062f0
15 changed files with 1488 additions and 706 deletions

View File

@@ -13,13 +13,14 @@ export {
} from './userStore'; } from './userStore';
// MessageManager 新架构导出(已替换 conversationStore // MessageManager 新架构导出(已替换 conversationStore
export { messageManager } from './messageManager'; // 重构:从 ./message 目录导入,移除了 messageManager.ts 重导出文件
export { messageManager, MessageManager } from './message';
export type { export type {
MessageEvent, MessageEvent,
MessageEventType, MessageEventType,
MessageSubscriber, MessageSubscriber,
MessageManagerConversationListDeps, MessageManagerConversationListDeps,
} from './messageManager'; } from './message';
export { export {
CONVERSATION_LIST_PAGE_SIZE, CONVERSATION_LIST_PAGE_SIZE,
type ConversationListPage, type ConversationListPage,

View File

@@ -1,59 +1,30 @@
/** /**
* MessageManager - 消息管理核心模块(重构版) * MessageManager - 消息管理核心模块(重构版)
* *
* 作为协调者,整合各个专注的服务模块 * 作为轻量级协调者,整合各个专注的服务模块
* 保持与原始版本完全兼容的 API * 直接使用 zustand store 进行状态管理
*
* 重构说明:
* - 移除了 MessageStateManager 包装层
* - 直接使用 zustand store
* - 服务层移至 services/ 目录
*/ */
import type { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../../types/dto'; 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 { useAuthStore } from '../authStore';
import { import type { MessageManagerConversationListDeps } from './types';
type IConversationListPagedSource,
SqliteConversationListPagedSource,
createRemoteConversationListSource,
CONVERSATION_LIST_PAGE_SIZE,
} from '../conversationListSources';
// 导入新模块 // 导入服务
import { MessageStateManager } from './MessageStateManager'; import { MessageDeduplication, messageDeduplication } from './services/MessageDeduplication';
import { MessageDeduplication } from './MessageDeduplication'; import { UserCacheService, userCacheService } from './services/UserCacheService';
import { UserCacheService } from './UserCacheService'; import { ReadReceiptManager } from './services/ReadReceiptManager';
import { ReadReceiptManager } from './ReadReceiptManager'; import { ConversationOperations } from './services/ConversationOperations';
import { ConversationOperations } from './ConversationOperations'; import { MessageSendService } from './services/MessageSendService';
import { MessageSendService } from './MessageSendService'; import { MessageSyncService } from './services/MessageSyncService';
import { MessageSyncService } from './MessageSyncService'; import { WSMessageHandler } from './services/WSMessageHandler';
import { WSMessageHandler } from './WSMessageHandler';
// 导入 store
import { useMessageStore, subscriptionManager, normalizeConversationId } from './store';
// 重新导出类型,保持兼容性 // 重新导出类型,保持兼容性
export type { export type {
@@ -69,7 +40,6 @@ export type {
class MessageManager { class MessageManager {
// 子模块 // 子模块
private stateManager: MessageStateManager;
private deduplication: MessageDeduplication; private deduplication: MessageDeduplication;
private userCacheService: UserCacheService; private userCacheService: UserCacheService;
private readReceiptManager: ReadReceiptManager; private readReceiptManager: ReadReceiptManager;
@@ -84,21 +54,20 @@ class MessageManager {
private initializePromise: Promise<void> | null = null; private initializePromise: Promise<void> | null = null;
private activatingConversationTasks: Map<string, Promise<void>> = new Map(); 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.deduplication = new MessageDeduplication();
this.userCacheService = new UserCacheService(); this.userCacheService = new UserCacheService();
this.readReceiptManager = new ReadReceiptManager(this.stateManager); this.readReceiptManager = new ReadReceiptManager();
this.conversationOps = new ConversationOperations(this.stateManager); this.conversationOps = new ConversationOperations();
this.sendService = new MessageSendService(this.stateManager, () => this.getCurrentUserId()); this.sendService = new MessageSendService(() => this.getCurrentUserId());
this.syncService = new MessageSyncService( this.syncService = new MessageSyncService(
this.stateManager,
() => this.getCurrentUserId(), () => this.getCurrentUserId(),
this.readReceiptManager,
this.userCacheService,
deps deps
); );
this.wsHandler = new WSMessageHandler( this.wsHandler = new WSMessageHandler(
this.stateManager,
this.deduplication, this.deduplication,
this.userCacheService, this.userCacheService,
() => this.getCurrentUserId(), () => this.getCurrentUserId(),
@@ -119,8 +88,9 @@ class MessageManager {
private initAuthListener() { private initAuthListener() {
const authState = useAuthStore.getState(); const authState = useAuthStore.getState();
this.currentUserId = authState.currentUser?.id || null; this.currentUserId = authState.currentUser?.id || null;
const store = useMessageStore.getState();
if (this.currentUserId && !this.stateManager.getState().isInitialized) { if (this.currentUserId && !store.isInitialized) {
this.initialize(); this.initialize();
} }
@@ -130,17 +100,17 @@ class MessageManager {
const prevUserId = this.currentUserId; const prevUserId = this.currentUserId;
this.currentUserId = nextUserId; this.currentUserId = nextUserId;
if (nextUserId && !this.stateManager.getState().isInitialized) { if (nextUserId && !useMessageStore.getState().isInitialized) {
this.initialize(); this.initialize();
} }
if (nextUserId && this.stateManager.getActiveConversation()) { if (nextUserId && useMessageStore.getState().currentConversationId) {
this.activateConversation(this.stateManager.getActiveConversation()!, { forceSync: true }).catch(error => { this.activateConversation(useMessageStore.getState().currentConversationId!, { forceSync: true }).catch(error => {
console.error('[MessageManager] 登录态就绪后重激活会话失败:', error); console.error('[MessageManager] 登录态就绪后重激活会话失败:', error);
}); });
} }
if (!nextUserId && prevUserId && this.stateManager.getState().isInitialized) { if (!nextUserId && prevUserId && useMessageStore.getState().isInitialized) {
this.destroy(); this.destroy();
} }
}); });
@@ -154,14 +124,12 @@ class MessageManager {
return this.currentUserId; return this.currentUserId;
} }
private normalizeConversationId(conversationId: string | number | null | undefined): string {
return conversationId == null ? '' : String(conversationId);
}
// ==================== 初始化与销毁 ==================== // ==================== 初始化与销毁 ====================
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (this.stateManager.getState().isInitialized) { const store = useMessageStore.getState();
if (store.isInitialized) {
return; return;
} }
@@ -189,26 +157,26 @@ class MessageManager {
// 加载未读数 // 加载未读数
await this.fetchUnreadCount(); await this.fetchUnreadCount();
this.stateManager.setInitialized(true); useMessageStore.getState().setInitialized(true);
// 处理缓冲的 SSE 事件 // 处理缓冲的 SSE 事件
await this.wsHandler.flushBufferedSSEEvents(); await this.wsHandler.flushBufferedSSEEvents();
this.wsHandler.setBootstrapping(false); this.wsHandler.setBootstrapping(false);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: this.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
if (this.stateManager.getActiveConversation()) { if (useMessageStore.getState().currentConversationId) {
await this.fetchMessages(this.stateManager.getActiveConversation()!); await this.fetchMessages(useMessageStore.getState().currentConversationId!);
} }
} catch (error) { } catch (error) {
console.error('[MessageManager] 初始化失败:', error); console.error('[MessageManager] 初始化失败:', error);
this.wsHandler.setBootstrapping(false); this.wsHandler.setBootstrapping(false);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'initialize' }, payload: { error, context: 'initialize' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -225,7 +193,8 @@ class MessageManager {
destroy(): void { destroy(): void {
this.wsHandler.disconnect(); this.wsHandler.disconnect();
this.stateManager.reset(); useMessageStore.getState().reset();
subscriptionManager.clear();
this.syncService.restartSources(); this.syncService.restartSources();
this.readReceiptManager.reset(); this.readReceiptManager.reset();
this.deduplication.reset(); this.deduplication.reset();
@@ -310,61 +279,61 @@ class MessageManager {
return this.conversationOps.decrementSystemUnreadCount(count); return this.conversationOps.decrementSystemUnreadCount(count);
} }
// ==================== 状态查询方法(代理到 stateManager==================== // ==================== 状态查询方法(直接使用 store====================
getConversations(): ConversationResponse[] { getConversations(): ConversationResponse[] {
return this.stateManager.getConversations(); return useMessageStore.getState().getConversations();
} }
getConversation(conversationId: string): ConversationResponse | null { getConversation(conversationId: string): ConversationResponse | null {
return this.stateManager.getConversation(conversationId); return useMessageStore.getState().getConversation(conversationId);
} }
getMessages(conversationId: string): MessageResponse[] { getMessages(conversationId: string): MessageResponse[] {
return this.stateManager.getMessages(conversationId); return useMessageStore.getState().getMessages(conversationId);
} }
getUnreadCount(): { total: number; system: number } { getUnreadCount(): { total: number; system: number } {
return this.stateManager.getUnreadCount(); return useMessageStore.getState().getUnreadCount();
} }
isConnected(): boolean { isConnected(): boolean {
return this.stateManager.isConnected(); return useMessageStore.getState().isConnected();
} }
isLoading(): boolean { isLoading(): boolean {
return this.stateManager.isLoading(); return useMessageStore.getState().isLoading();
} }
isLoadingMessages(conversationId: string): boolean { isLoadingMessages(conversationId: string): boolean {
return this.stateManager.isLoadingMessages(conversationId); return useMessageStore.getState().isLoadingMessages(conversationId);
} }
getTypingUsers(groupId: string): string[] { getTypingUsers(groupId: string): string[] {
return this.stateManager.getTypingUsers(groupId); return useMessageStore.getState().getTypingUsers(groupId);
} }
isMuted(groupId: string): boolean { isMuted(groupId: string): boolean {
return this.stateManager.isMuted(groupId); return useMessageStore.getState().isMuted(groupId);
} }
setMutedStatus(groupId: string, isMuted: boolean): void { setMutedStatus(groupId: string, isMuted: boolean): void {
return this.stateManager.setMutedStatus(groupId, isMuted); return useMessageStore.getState().setMutedStatus(groupId, isMuted);
} }
// ==================== 活动会话管理 ==================== // ==================== 活动会话管理 ====================
setActiveConversation(conversationId: string | null): void { setActiveConversation(conversationId: string | null): void {
const normalizedId = conversationId ? this.normalizeConversationId(conversationId) : null; const normalizedId = conversationId ? normalizeConversationId(conversationId) : null;
this.stateManager.setCurrentConversation(normalizedId); useMessageStore.getState().setCurrentConversation(normalizedId);
} }
getActiveConversation(): string | null { getActiveConversation(): string | null {
return this.stateManager.getActiveConversation(); return useMessageStore.getState().currentConversationId;
} }
async activateConversation(conversationId: string, options?: { forceSync?: boolean }): Promise<void> { async activateConversation(conversationId: string, options?: { forceSync?: boolean }): Promise<void> {
const normalizedId = this.normalizeConversationId(conversationId); const normalizedId = normalizeConversationId(conversationId);
if (!normalizedId) return; if (!normalizedId) return;
await this.initialize(); await this.initialize();
@@ -372,7 +341,7 @@ class MessageManager {
// 先下发当前内存快照 // 先下发当前内存快照
const snapshot = this.getMessages(normalizedId); const snapshot = this.getMessages(normalizedId);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId: normalizedId, conversationId: normalizedId,
@@ -405,16 +374,16 @@ class MessageManager {
// ==================== 订阅机制 ==================== // ==================== 订阅机制 ====================
subscribe(subscriber: import('./types').MessageSubscriber): () => void { subscribe(subscriber: import('./types').MessageSubscriber): () => void {
const unsubscribe = this.stateManager.subscribe(subscriber); const unsubscribe = subscriptionManager.subscribe(subscriber);
// 立即发送当前状态给新订阅者(与原始版本保持一致) // 立即发送当前状态给新订阅者(与原始版本保持一致)
subscriber({ subscriber({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: this.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
const unreadCount = this.stateManager.getUnreadCount(); const unreadCount = this.getUnreadCount();
subscriber({ subscriber({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
@@ -426,14 +395,14 @@ class MessageManager {
subscriber({ subscriber({
type: 'connection_changed', type: 'connection_changed',
payload: { connected: this.stateManager.isConnected() }, payload: { connected: this.isConnected() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
// 如果当前有活动会话,立即发送该会话的消息给新订阅者 // 如果当前有活动会话,立即发送该会话的消息给新订阅者
const activeConversationId = this.stateManager.getActiveConversation(); const activeConversationId = this.getActiveConversation();
if (activeConversationId) { if (activeConversationId) {
const currentMessages = this.stateManager.getMessages(activeConversationId); const currentMessages = this.getMessages(activeConversationId);
if (currentMessages.length > 0) { if (currentMessages.length > 0) {
subscriber({ subscriber({
type: 'messages_updated', type: 'messages_updated',
@@ -451,7 +420,7 @@ class MessageManager {
} }
subscribeToMessages(conversationId: string, callback: (messages: MessageResponse[]) => void): () => void { 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) { if (event.type === 'messages_updated' && event.payload.conversationId === conversationId) {
callback(event.payload.messages); callback(event.payload.messages);
} }
@@ -484,7 +453,7 @@ class MessageManager {
} }
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean { private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
if (!this.stateManager.getActiveConversation()) return false; if (!this.getActiveConversation()) return false;
if (!forceRefresh) return false; if (!forceRefresh) return false;
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh'; return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh';
} }

View File

@@ -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
View 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,
};
}

View File

@@ -1,12 +1,27 @@
/** /**
* 消息模块统一导出 * 消息模块统一导出
* *
* 提供向后兼容的 API 导出 * 重构说明:
* - 移除了 MessageStateManager 包装层
* - 直接使用 zustand store 进行状态管理
* - 服务层移至 services/ 目录
* - 新增 hooks.ts 封装常用逻辑
*/ */
// ==================== 主管理器 ==================== // ==================== 主管理器 ====================
export { messageManager, MessageManager } from './MessageManager'; export { messageManager, MessageManager } from './MessageManager';
// ==================== 状态管理 ====================
export {
useMessageStore,
subscriptionManager,
normalizeConversationId,
mergeMessagesById,
notifySubscribers,
subscribeToMessages,
} from './store';
export type { MessageState, MessageActions, MessageStore } from './store';
// ==================== 类型导出 ==================== // ==================== 类型导出 ====================
// 从 types.ts 导出所有类型 // 从 types.ts 导出所有类型
export type { export type {
@@ -17,6 +32,7 @@ export type {
ReadStateRecord, ReadStateRecord,
MessageManagerConversationListDeps, MessageManagerConversationListDeps,
HandleNewMessageOptions, HandleNewMessageOptions,
// 服务接口类型(保留向后兼容,但服务层已不再使用)
IMessageStateManager, IMessageStateManager,
IMessageDeduplication, IMessageDeduplication,
IUserCacheService, IUserCacheService,
@@ -37,30 +53,57 @@ export {
MIN_RECONNECT_SYNC_INTERVAL, MIN_RECONNECT_SYNC_INTERVAL,
} from './constants'; } from './constants';
// ==================== 子模块导出(可选使用)==================== // ==================== 服务导出 ====================
// 状态管理器 export {
export { MessageStateManager, messageStateManager } from './MessageStateManager'; // 消息去重服务
MessageDeduplication,
messageDeduplication,
// 用户缓存服务
UserCacheService,
userCacheService,
// 已读回执管理器
ReadReceiptManager,
// 会话操作服务
ConversationOperations,
// 消息发送服务
MessageSendService,
// 消息同步服务
MessageSyncService,
// WebSocket 消息处理器
WSMessageHandler,
} from './services';
// 消息去重服务 // ==================== Hooks 导出 ====================
export { MessageDeduplication, messageDeduplication } from './MessageDeduplication'; export {
// 会话相关
// 用户缓存服务 useConversations,
export { UserCacheService, userCacheService } from './UserCacheService'; useConversation,
useCreateConversation,
// 已读回执管理器 useUpdateConversation,
export { ReadReceiptManager } from './ReadReceiptManager'; useActiveConversation,
// 会话操作服务 // 消息相关
export { ConversationOperations } from './ConversationOperations'; useMessages,
useSendMessage,
// 消息发送服务 useMarkAsRead,
export { MessageSendService } from './MessageSendService';
// 未读数相关
// 消息同步服务 useUnreadCount,
export { MessageSyncService } from './MessageSyncService'; useSystemUnreadCount,
// WebSocket 消息处理器 // 群聊相关
export { WSMessageHandler } from './WSMessageHandler'; useGroupTyping,
useGroupMuted,
// 通用
useMessageManager,
} from './hooks';
// ==================== 默认导出 ==================== // ==================== 默认导出 ====================
export { default } from './MessageManager'; export { default } from './MessageManager';

View File

@@ -1,41 +1,40 @@
/** /**
* *
* CRUD * CRUD
*
* 使 zustand store IMessageStateManager
*/ */
import type { ConversationResponse } from '../../types/dto'; import type { ConversationResponse } from '../../../types/dto';
import { messageService } from '../../services/messageService'; import { messageService } from '../../../services/messageService';
import { deleteConversation } from '../../services/database'; import { deleteConversation } from '../../../services/database';
import type { IConversationOperations, IMessageStateManager } from './types'; import type { IConversationOperations } from '../types';
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
export class ConversationOperations implements IConversationOperations { export class ConversationOperations implements IConversationOperations {
private stateManager: IMessageStateManager;
constructor(stateManager: IMessageStateManager) {
this.stateManager = stateManager;
}
/** /**
* *
*/ */
async createConversation(userId: string): Promise<ConversationResponse | null> { async createConversation(userId: string): Promise<ConversationResponse | null> {
const store = useMessageStore.getState();
try { try {
const conversation = await messageService.createConversation(userId); const conversation = await messageService.createConversation(userId);
// 添加到会话列表 // 添加到会话列表
this.stateManager.updateConversation(conversation); store.updateConversation(conversation);
// 通知更新 // 通知更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
return conversation; return conversation;
} catch (error) { } catch (error) {
console.error('[ConversationOperations] 创建会话失败:', error); console.error('[ConversationOperations] 创建会话失败:', error);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'createConversation' }, payload: { error, context: 'createConversation' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -48,7 +47,8 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void { updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) { if (!conversation) {
console.warn('[ConversationOperations] 更新会话失败,会话不存在:', conversationId); console.warn('[ConversationOperations] 更新会话失败,会话不存在:', conversationId);
return; return;
@@ -59,12 +59,12 @@ export class ConversationOperations implements IConversationOperations {
...updates, ...updates,
}; };
this.stateManager.updateConversation(updatedConv); store.updateConversation(updatedConv);
// 通知更新 // 通知更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -73,17 +73,18 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
removeConversation(conversationId: string): void { removeConversation(conversationId: string): void {
this.stateManager.removeConversation(conversationId); const store = useMessageStore.getState();
store.removeConversation(conversationId);
// 通知更新 // 通知更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
const unreadCount = this.stateManager.getUnreadCount(); const unreadCount = store.getUnreadCount();
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: unreadCount.total, totalUnreadCount: unreadCount.total,
@@ -102,15 +103,16 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
clearConversations(): void { clearConversations(): void {
this.stateManager.reset(); const store = useMessageStore.getState();
store.reset();
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: [] }, payload: { conversations: [] },
timestamp: Date.now(), timestamp: Date.now(),
}); });
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: 0, totalUnreadCount: 0,
@@ -124,7 +126,8 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
updateUnreadCount(conversationId: string, count: number): void { updateUnreadCount(conversationId: string, count: number): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) { if (!conversation) {
console.warn('[ConversationOperations] 更新未读数失败,会话不存在:', conversationId); console.warn('[ConversationOperations] 更新未读数失败,会话不存在:', conversationId);
return; return;
@@ -138,28 +141,28 @@ export class ConversationOperations implements IConversationOperations {
...conversation, ...conversation,
unread_count: count, unread_count: count,
}; };
this.stateManager.updateConversation(updatedConv); store.updateConversation(updatedConv);
// 更新总未读数 // 更新总未读数
const currentUnread = this.stateManager.getUnreadCount(); const currentUnread = store.getUnreadCount();
this.stateManager.setUnreadCount( store.setUnreadCount(
Math.max(0, currentUnread.total + diff), Math.max(0, currentUnread.total + diff),
currentUnread.system currentUnread.system
); );
// 通知更新 // 通知更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: this.stateManager.getUnreadCount().total, totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: this.stateManager.getUnreadCount().system, systemUnreadCount: store.getUnreadCount().system,
}, },
timestamp: Date.now(), timestamp: Date.now(),
}); });
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -168,7 +171,8 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
incrementUnreadCount(conversationId: string): void { incrementUnreadCount(conversationId: string): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) return; if (!conversation) return;
const prevUnreadCount = conversation.unread_count || 0; const prevUnreadCount = conversation.unread_count || 0;
@@ -180,27 +184,27 @@ export class ConversationOperations implements IConversationOperations {
unread_count: newUnreadCount, unread_count: newUnreadCount,
}; };
this.stateManager.updateConversation(updatedConv); store.updateConversation(updatedConv);
const currentUnread = this.stateManager.getUnreadCount(); const currentUnread = store.getUnreadCount();
this.stateManager.setUnreadCount( store.setUnreadCount(
currentUnread.total + 1, currentUnread.total + 1,
currentUnread.system currentUnread.system
); );
// 通知更新 // 通知更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: this.stateManager.getUnreadCount().total, totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: this.stateManager.getUnreadCount().system, systemUnreadCount: store.getUnreadCount().system,
}, },
timestamp: Date.now(), timestamp: Date.now(),
}); });
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -209,10 +213,11 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
setSystemUnreadCount(count: number): void { setSystemUnreadCount(count: number): void {
const currentUnread = this.stateManager.getUnreadCount(); const store = useMessageStore.getState();
this.stateManager.setUnreadCount(currentUnread.total, count); const currentUnread = store.getUnreadCount();
store.setUnreadCount(currentUnread.total, count);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: currentUnread.total, totalUnreadCount: currentUnread.total,
@@ -226,10 +231,11 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
incrementSystemUnreadCount(): void { incrementSystemUnreadCount(): void {
const currentUnread = this.stateManager.getUnreadCount(); const store = useMessageStore.getState();
this.stateManager.setUnreadCount(currentUnread.total, currentUnread.system + 1); const currentUnread = store.getUnreadCount();
store.setUnreadCount(currentUnread.total, currentUnread.system + 1);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: currentUnread.total, totalUnreadCount: currentUnread.total,
@@ -243,11 +249,12 @@ export class ConversationOperations implements IConversationOperations {
* *
*/ */
decrementSystemUnreadCount(count = 1): void { 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); 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', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: currentUnread.total, totalUnreadCount: currentUnread.total,
@@ -256,4 +263,4 @@ export class ConversationOperations implements IConversationOperations {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
} }

View File

@@ -3,11 +3,11 @@
* *
*/ */
import type { IMessageDeduplication } from './types'; import type { IMessageDeduplication } from '../types';
import { import {
PROCESSED_MESSAGE_ID_EXPIRY, PROCESSED_MESSAGE_ID_EXPIRY,
PROCESSED_MESSAGE_ID_MAX_SIZE, PROCESSED_MESSAGE_ID_MAX_SIZE,
} from './constants'; } from '../constants';
export class MessageDeduplication implements IMessageDeduplication { export class MessageDeduplication implements IMessageDeduplication {
// 已处理消息ID集合用于去重防止ACK消息和正常消息重复处理 // 已处理消息ID集合用于去重防止ACK消息和正常消息重复处理
@@ -58,4 +58,4 @@ export class MessageDeduplication implements IMessageDeduplication {
} }
// 单例导出 // 单例导出
export const messageDeduplication = new MessageDeduplication(); export const messageDeduplication = new MessageDeduplication();

View File

@@ -1,19 +1,20 @@
/** /**
* *
* *
*
* 使 zustand store IMessageStateManager
*/ */
import type { MessageResponse, MessageSegment, ConversationResponse } from '../../types/dto'; import type { MessageResponse, MessageSegment, ConversationResponse } from '../../../types/dto';
import { messageService } from '../../services/messageService'; import { messageService } from '../../../services/messageService';
import { saveMessage } from '../../services/database'; import { saveMessage } from '../../../services/database';
import type { IMessageSendService, IMessageStateManager } from './types'; import type { IMessageSendService } from '../types';
import { useMessageStore, subscriptionManager, mergeMessagesById } from '../store';
export class MessageSendService implements IMessageSendService { export class MessageSendService implements IMessageSendService {
private stateManager: IMessageStateManager;
private getCurrentUserId: () => string | null; private getCurrentUserId: () => string | null;
constructor(stateManager: IMessageStateManager, getCurrentUserId: () => string | null) { constructor(getCurrentUserId: () => string | null) {
this.stateManager = stateManager;
this.getCurrentUserId = getCurrentUserId; this.getCurrentUserId = getCurrentUserId;
} }
@@ -25,6 +26,8 @@ export class MessageSendService implements IMessageSendService {
segments: MessageSegment[], segments: MessageSegment[],
options?: { replyToId?: string } options?: { replyToId?: string }
): Promise<MessageResponse | null> { ): Promise<MessageResponse | null> {
const store = useMessageStore.getState();
try { try {
// 调用API发送 // 调用API发送
const response = await messageService.sendMessage(conversationId, { const response = await messageService.sendMessage(conversationId, {
@@ -36,7 +39,7 @@ export class MessageSendService implements IMessageSendService {
const currentUserId = this.getCurrentUserId(); const currentUserId = this.getCurrentUserId();
// 添加到本地消息列表 // 添加到本地消息列表
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const newMessage: MessageResponse = { const newMessage: MessageResponse = {
id: response.id, id: response.id,
conversation_id: conversationId, conversation_id: conversationId,
@@ -47,11 +50,11 @@ export class MessageSendService implements IMessageSendService {
status: 'normal', status: 'normal',
}; };
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]); const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
this.stateManager.setMessages(conversationId, updatedMessages); store.setMessages(conversationId, updatedMessages);
// 关键:立即广播消息列表更新 // 关键:立即广播消息列表更新
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -66,7 +69,7 @@ export class MessageSendService implements IMessageSendService {
this.updateConversationWithNewMessage(conversationId, newMessage); this.updateConversationWithNewMessage(conversationId, newMessage);
// 通知消息已发送 // 通知消息已发送
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_sent', type: 'message_sent',
payload: { conversationId, message: newMessage }, payload: { conversationId, message: newMessage },
timestamp: Date.now(), timestamp: Date.now(),
@@ -97,7 +100,7 @@ export class MessageSendService implements IMessageSendService {
return null; return null;
} catch (error) { } catch (error) {
console.error('[MessageSendService] 发送消息失败:', error); console.error('[MessageSendService] 发送消息失败:', error);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'sendMessage' }, payload: { error, context: 'sendMessage' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -113,7 +116,8 @@ export class MessageSendService implements IMessageSendService {
conversationId: string, conversationId: string,
message: MessageResponse message: MessageResponse
): void { ): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
const createdAt = message.created_at || new Date().toISOString(); const createdAt = message.created_at || new Date().toISOString();
if (conversation) { if (conversation) {
@@ -126,19 +130,7 @@ export class MessageSendService implements IMessageSendService {
updated_at: createdAt, 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);
}
}

View File

@@ -1,28 +1,33 @@
/** /**
* *
* *
*
* 使 zustand store IMessageStateManager
*/ */
import type { ConversationResponse, MessageResponse } from '../../types/dto'; import type { ConversationResponse, MessageResponse } from '../../../types/dto';
import { messageService } from '../../services/messageService'; import { messageService } from '../../../services/messageService';
import { import {
getMessagesByConversation, getMessagesByConversation,
getMaxSeq, getMaxSeq,
saveMessagesBatch, saveMessagesBatch,
saveConversationsWithRelatedCache, saveConversationsWithRelatedCache,
getMessagesBeforeSeq, getMessagesBeforeSeq,
} from '../../services/database'; } from '../../../services/database';
import { import {
type IConversationListPagedSource, type IConversationListPagedSource,
SqliteConversationListPagedSource, SqliteConversationListPagedSource,
createRemoteConversationListSource, createRemoteConversationListSource,
CONVERSATION_LIST_PAGE_SIZE, CONVERSATION_LIST_PAGE_SIZE,
} from '../conversationListSources'; } from '../../conversationListSources';
import type { IMessageSyncService, IMessageStateManager, MessageManagerConversationListDeps } from './types'; import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types';
import { useMessageStore, subscriptionManager, normalizeConversationId, mergeMessagesById } from '../store';
import { ReadReceiptManager } from './ReadReceiptManager';
export class MessageSyncService implements IMessageSyncService { export class MessageSyncService implements IMessageSyncService {
private stateManager: IMessageStateManager;
private getCurrentUserId: () => string | null; private getCurrentUserId: () => string | null;
private readReceiptManager: ReadReceiptManager;
private userCacheService: IUserCacheService;
/** 远端会话列表(游标或页码) */ /** 远端会话列表(游标或页码) */
private readonly remoteConversationListSource: IConversationListPagedSource; private readonly remoteConversationListSource: IConversationListPagedSource;
@@ -35,12 +40,14 @@ export class MessageSyncService implements IMessageSyncService {
private deferredConversationRefresh = false; private deferredConversationRefresh = false;
constructor( constructor(
stateManager: IMessageStateManager,
getCurrentUserId: () => string | null, getCurrentUserId: () => string | null,
readReceiptManager: ReadReceiptManager,
userCacheService: IUserCacheService,
deps?: MessageManagerConversationListDeps deps?: MessageManagerConversationListDeps
) { ) {
this.stateManager = stateManager;
this.getCurrentUserId = getCurrentUserId; this.getCurrentUserId = getCurrentUserId;
this.readReceiptManager = readReceiptManager;
this.userCacheService = userCacheService;
const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE; const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE;
this.remoteConversationListSource = this.remoteConversationListSource =
@@ -57,7 +64,9 @@ export class MessageSyncService implements IMessageSyncService {
* *
*/ */
async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> { async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> {
if (this.stateManager.isLoading() && !forceRefresh) { const store = useMessageStore.getState();
if (store.isLoading() && !forceRefresh) {
return; return;
} }
@@ -66,7 +75,7 @@ export class MessageSyncService implements IMessageSyncService {
if (__DEV__) { if (__DEV__) {
console.log('[MessageSyncService] defer fetchConversations', { console.log('[MessageSyncService] defer fetchConversations', {
source, source,
activeConversationId: this.stateManager.getActiveConversation(), activeConversationId: store.getActiveConversation(),
}); });
} }
return; return;
@@ -76,12 +85,12 @@ export class MessageSyncService implements IMessageSyncService {
console.log('[MessageSyncService] fetchConversations', { console.log('[MessageSyncService] fetchConversations', {
source, source,
forceRefresh, forceRefresh,
activeConversationId: this.stateManager.getActiveConversation(), activeConversationId: store.getActiveConversation(),
}); });
} }
// 非强制刷新且内存为空:先用本地源暖机 // 非强制刷新且内存为空:先用本地源暖机
const currentState = this.stateManager.getState(); const currentState = store.getState();
if (!forceRefresh && currentState.conversations.size === 0) { if (!forceRefresh && currentState.conversations.size === 0) {
const warmed = await this.hydrateConversationsFromLocalSource(); const warmed = await this.hydrateConversationsFromLocalSource();
if (warmed) { if (warmed) {
@@ -89,10 +98,10 @@ export class MessageSyncService implements IMessageSyncService {
} }
} }
this.stateManager.setLoading(true); store.setLoading(true);
const emitLoadingToUi = this.stateManager.getConversations().length === 0; const emitLoadingToUi = store.getConversations().length === 0;
if (emitLoadingToUi) { if (emitLoadingToUi) {
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_loading', type: 'conversations_loading',
payload: { loading: true }, payload: { loading: true },
timestamp: Date.now(), timestamp: Date.now(),
@@ -103,32 +112,41 @@ export class MessageSyncService implements IMessageSyncService {
this.remoteConversationListSource.restart(); this.remoteConversationListSource.restart();
const page = await this.remoteConversationListSource.loadNext(); const page = await this.remoteConversationListSource.loadNext();
currentState.conversations.clear(); // 使用 zustand set 函数正确更新状态
const newConversations = new Map<string, ConversationResponse>();
page.items.forEach(conv => { page.items.forEach(conv => {
const normalizedConv = this.normalizeConversationFromFetch(conv); const normalizedConv = this.normalizeConversationFromFetch(conv);
currentState.conversations.set(normalizedConv.id, normalizedConv); newConversations.set(normalizedConv.id, normalizedConv);
}); });
this.updateConversationList(); // 使用 store.setConversations 会自动排序并更新 conversationList
this.recomputeConversationTotalUnread(); 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(); this.emitConversationListAndUnreadUpdates();
} catch (error) { } catch (error) {
console.error('[MessageSyncService] 获取会话列表失败:', error); console.error('[MessageSyncService] 获取会话列表失败:', error);
if (this.stateManager.getConversations().length === 0) { if (store.getConversations().length === 0) {
const recovered = await this.hydrateConversationsFromLocalSource(); const recovered = await this.hydrateConversationsFromLocalSource();
if (recovered) { if (recovered) {
this.emitConversationListAndUnreadUpdates(); this.emitConversationListAndUnreadUpdates();
} }
} }
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'fetchConversations' }, payload: { error, context: 'fetchConversations' },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} finally { } finally {
this.stateManager.setLoading(false); store.setLoading(false);
if (emitLoadingToUi) { if (emitLoadingToUi) {
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_loading', type: 'conversations_loading',
payload: { loading: false }, payload: { loading: false },
timestamp: Date.now(), timestamp: Date.now(),
@@ -141,10 +159,12 @@ export class MessageSyncService implements IMessageSyncService {
* *
*/ */
async loadMoreConversations(): Promise<void> { async loadMoreConversations(): Promise<void> {
const store = useMessageStore.getState();
if (!this.remoteConversationListSource.hasMore) { if (!this.remoteConversationListSource.hasMore) {
return; return;
} }
if (this.loadingMoreConversations || this.stateManager.isLoading()) { if (this.loadingMoreConversations || store.isLoading()) {
return; return;
} }
@@ -154,14 +174,14 @@ export class MessageSyncService implements IMessageSyncService {
page.items.forEach(conv => { page.items.forEach(conv => {
const normalizedConv = this.normalizeConversationFromFetch(conv); const normalizedConv = this.normalizeConversationFromFetch(conv);
this.stateManager.updateConversation(normalizedConv); store.updateConversation(normalizedConv);
}); });
this.recomputeConversationTotalUnread(); this.recomputeConversationTotalUnread();
this.emitConversationListAndUnreadUpdates(); this.emitConversationListAndUnreadUpdates();
} catch (error) { } catch (error) {
console.error('[MessageSyncService] 加载更多会话失败:', error); console.error('[MessageSyncService] 加载更多会话失败:', error);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'loadMoreConversations' }, payload: { error, context: 'loadMoreConversations' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -175,12 +195,14 @@ export class MessageSyncService implements IMessageSyncService {
* *
*/ */
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> { async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
const normalizedId = this.normalizeConversationId(conversationId); const store = useMessageStore.getState();
const normalizedId = normalizeConversationId(conversationId);
try { try {
const detail = await messageService.getConversationById(normalizedId); const detail = await messageService.getConversationById(normalizedId);
if (detail) { if (detail) {
const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse); const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse);
this.stateManager.updateConversation(normalizedDetail); store.updateConversation(normalizedDetail);
return normalizedDetail; return normalizedDetail;
} }
return null; return null;
@@ -194,15 +216,17 @@ export class MessageSyncService implements IMessageSyncService {
* *
*/ */
async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> { async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> {
const store = useMessageStore.getState();
// 防止重复加载 // 防止重复加载
if (this.stateManager.isLoadingMessages(conversationId)) { if (store.isLoadingMessages(conversationId)) {
return; return;
} }
this.stateManager.setLoadingMessages(conversationId, true); store.setLoadingMessages(conversationId, true);
try { try {
const existingMessagesAtStart = this.stateManager.getMessages(conversationId); const existingMessagesAtStart = store.getMessages(conversationId);
const hasInMemoryMessages = existingMessagesAtStart.length > 0; const hasInMemoryMessages = existingMessagesAtStart.length > 0;
let baselineMaxSeq = hasInMemoryMessages let baselineMaxSeq = hasInMemoryMessages
? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0) ? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0)
@@ -227,8 +251,8 @@ export class MessageSyncService implements IMessageSyncService {
created_at: m.createdAt, created_at: m.createdAt,
})); }));
this.stateManager.setMessages(conversationId, formattedMessages); store.setMessages(conversationId, formattedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -239,8 +263,8 @@ export class MessageSyncService implements IMessageSyncService {
}); });
} else { } else {
// 冷启动兜底:先下发空列表事件 // 冷启动兜底:先下发空列表事件
this.stateManager.setMessages(conversationId, []); store.setMessages(conversationId, []);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -261,11 +285,11 @@ export class MessageSyncService implements IMessageSyncService {
const snapshotMessages = snapshotResp?.messages || []; const snapshotMessages = snapshotResp?.messages || [];
if (snapshotMessages.length > 0) { if (snapshotMessages.length > 0) {
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const mergedSnapshot = this.mergeMessagesById(existingMessages, snapshotMessages); const mergedSnapshot = mergeMessagesById(existingMessages, snapshotMessages);
this.stateManager.setMessages(conversationId, mergedSnapshot); store.setMessages(conversationId, mergedSnapshot);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -300,11 +324,11 @@ export class MessageSyncService implements IMessageSyncService {
const newMessages = incrementalResp?.messages || []; const newMessages = incrementalResp?.messages || [];
if (newMessages.length > 0) { if (newMessages.length > 0) {
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages); const mergedMessages = mergeMessagesById(existingMessages, newMessages);
this.stateManager.setMessages(conversationId, mergedMessages); store.setMessages(conversationId, mergedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -340,11 +364,11 @@ export class MessageSyncService implements IMessageSyncService {
if (response?.messages && response.messages.length > 0) { if (response?.messages && response.messages.length > 0) {
const newMessages = response.messages; const newMessages = response.messages;
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages); const mergedMessages = mergeMessagesById(existingMessages, newMessages);
this.stateManager.setMessages(conversationId, mergedMessages); store.setMessages(conversationId, mergedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -373,13 +397,33 @@ export class MessageSyncService implements IMessageSyncService {
} }
} catch (error) { } catch (error) {
console.error('[MessageSyncService] 获取消息失败:', error); console.error('[MessageSyncService] 获取消息失败:', error);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'error', type: 'error',
payload: { error, context: 'fetchMessages', conversationId }, payload: { error, context: 'fetchMessages', conversationId },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} finally { } 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[]> { async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
const store = useMessageStore.getState();
try { try {
// 先从本地获取 // 先从本地获取
const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit); const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit);
@@ -402,11 +448,11 @@ export class MessageSyncService implements IMessageSyncService {
created_at: m.createdAt, created_at: m.createdAt,
})); }));
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages); const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages);
this.stateManager.setMessages(conversationId, mergedMessages); store.setMessages(conversationId, mergedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'local_history' }, payload: { conversationId, messages: mergedMessages, source: 'local_history' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -434,11 +480,11 @@ export class MessageSyncService implements IMessageSyncService {
segments: m.segments, segments: m.segments,
}))); })));
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages); const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
this.stateManager.setMessages(conversationId, mergedMessages); store.setMessages(conversationId, mergedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'server_history' }, payload: { conversationId, messages: mergedMessages, source: 'server_history' },
timestamp: Date.now(), timestamp: Date.now(),
@@ -458,6 +504,8 @@ export class MessageSyncService implements IMessageSyncService {
* *
*/ */
async fetchUnreadCount(): Promise<void> { async fetchUnreadCount(): Promise<void> {
const store = useMessageStore.getState();
try { try {
const [unreadData, systemUnreadData] = await Promise.all([ const [unreadData, systemUnreadData] = await Promise.all([
messageService.getUnreadCount(), messageService.getUnreadCount(),
@@ -467,30 +515,32 @@ export class MessageSyncService implements IMessageSyncService {
const totalUnread = unreadData?.total_unread_count ?? 0; const totalUnread = unreadData?.total_unread_count ?? 0;
const systemUnread = systemUnreadData?.unread_count ?? 0; const systemUnread = systemUnreadData?.unread_count ?? 0;
this.stateManager.setUnreadCount(totalUnread, systemUnread); store.setUnreadCount(totalUnread, systemUnread);
// 服务端汇总未读为 0 时,清掉内存中残留的红点 // 服务端汇总未读为 0 时,清掉内存中残留的红点
if (totalUnread === 0) { if (totalUnread === 0) {
const currentState = this.stateManager.getState(); const currentConversations = store.getState().conversations;
let anyCleared = false; 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) { if ((conv.unread_count || 0) > 0) {
currentState.conversations.set(cid, { ...conv, unread_count: 0 }); newConversations.set(cid, { ...conv, unread_count: 0 });
anyCleared = true; anyCleared = true;
} }
} }
if (anyCleared) { if (anyCleared) {
this.updateConversationList(); // 使用 zustand set 函数正确更新状态
store.setConversations(newConversations);
this.persistConversationListCache(); this.persistConversationListCache();
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
} }
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: totalUnread, totalUnreadCount: totalUnread,
@@ -522,56 +572,43 @@ export class MessageSyncService implements IMessageSyncService {
// ==================== 私有工具方法 ==================== // ==================== 私有工具方法 ====================
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean { 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; if (!forceRefresh) return false;
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh'; // hooks-initial-refresh 是冷启动兜底刷新,不应该被延迟
} return source === 'sse-reconnect' || source === 'prefetch';
private normalizeConversationId(conversationId: string | number | null | undefined): string {
return conversationId == null ? '' : String(conversationId);
} }
private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse { private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse {
const id = this.normalizeConversationId(conv.id); const id = normalizeConversationId(conv.id);
return { // 应用已读保护逻辑
return this.readReceiptManager.applyConversationFromFetch({
...conv, ...conv,
id, 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 { private recomputeConversationTotalUnread(): void {
const currentState = this.stateManager.getState(); const store = useMessageStore.getState();
currentState.totalUnreadCount = Array.from(currentState.conversations.values()).reduce( const totalUnread = Array.from(store.getState().conversations.values()).reduce(
(sum, conv) => sum + (conv.unread_count || 0), (sum, conv) => sum + (conv.unread_count || 0),
0 0
); );
// 使用 zustand set 函数正确更新状态
store.setUnreadCount(totalUnread, store.getUnreadCount().system);
} }
private emitConversationListAndUnreadUpdates(): void { private emitConversationListAndUnreadUpdates(): void {
const store = useMessageStore.getState();
this.persistConversationListCache(); this.persistConversationListCache();
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
const unreadCount = this.stateManager.getUnreadCount(); const unreadCount = store.getUnreadCount();
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: unreadCount.total, totalUnreadCount: unreadCount.total,
@@ -582,7 +619,8 @@ export class MessageSyncService implements IMessageSyncService {
} }
private persistConversationListCache(): void { private persistConversationListCache(): void {
const currentState = this.stateManager.getState(); const store = useMessageStore.getState();
const currentState = store.getState();
const list = Array.from(currentState.conversations.values()); const list = Array.from(currentState.conversations.values());
const users = list.flatMap(conv => [ const users = list.flatMap(conv => [
...(conv.participants || []), ...(conv.participants || []),
@@ -598,33 +636,33 @@ export class MessageSyncService implements IMessageSyncService {
} }
private async hydrateConversationsFromLocalSource(): Promise<boolean> { private async hydrateConversationsFromLocalSource(): Promise<boolean> {
const store = useMessageStore.getState();
this.localConversationListSource.restart(); this.localConversationListSource.restart();
const page = await this.localConversationListSource.loadNext(); const page = await this.localConversationListSource.loadNext();
if (!page.items.length) { if (!page.items.length) {
return false; return false;
} }
const currentState = this.stateManager.getState(); // 使用 zustand set 函数正确更新状态
currentState.conversations.clear(); const newConversations = new Map<string, ConversationResponse>();
page.items.forEach(conv => { page.items.forEach(conv => {
const normalizedConv = this.normalizeConversationFromFetch(conv); const normalizedConv = this.normalizeConversationFromFetch(conv);
currentState.conversations.set(normalizedConv.id, normalizedConv); newConversations.set(normalizedConv.id, normalizedConv);
}); });
this.updateConversationList(); // 使用 store.setConversations 会自动排序并更新 conversationList
this.recomputeConversationTotalUnread(); 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; 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[] { private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
if (incoming.length === 0) return existing; if (incoming.length === 0) return existing;
@@ -638,6 +676,6 @@ export class MessageSyncService implements IMessageSyncService {
return [...incomingAsc, ...existing]; return [...incomingAsc, ...existing];
} }
return this.mergeMessagesById(existing, incomingAsc); return mergeMessagesById(existing, incomingAsc);
} }
} }

View File

@@ -1,17 +1,18 @@
/** /**
* *
* *
*
* 使 zustand store IMessageStateManager
*/ */
import type { ConversationResponse } from '../../types/dto'; import type { ConversationResponse } from '../../../types/dto';
import { messageService } from '../../services/messageService'; import { messageService } from '../../../services/messageService';
import { markConversationAsRead, updateConversationCacheUnreadCount } from '../../services/database'; import { markConversationAsRead, updateConversationCacheUnreadCount } from '../../../services/database';
import type { ReadStateRecord, IReadReceiptManager, IMessageStateManager } from './types'; import type { ReadStateRecord, IReadReceiptManager } from '../types';
import { READ_STATE_PROTECTION_DELAY } from './constants'; import { READ_STATE_PROTECTION_DELAY } from '../constants';
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
export class ReadReceiptManager implements IReadReceiptManager { export class ReadReceiptManager implements IReadReceiptManager {
private stateManager: IMessageStateManager;
/** /**
* API * API
* fetchConversations * fetchConversations
@@ -23,17 +24,14 @@ export class ReadReceiptManager implements IReadReceiptManager {
*/ */
private readStateVersion: number = 0; private readStateVersion: number = 0;
constructor(stateManager: IMessageStateManager) {
this.stateManager = stateManager;
}
/** /**
* *
* *
*/ */
async markAsRead(conversationId: string, seq: number): Promise<void> { async markAsRead(conversationId: string, seq: number): Promise<void> {
const normalizedId = this.normalizeConversationId(conversationId); const normalizedId = normalizeConversationId(conversationId);
const conversation = this.stateManager.getConversation(normalizedId); const store = useMessageStore.getState();
const conversation = store.getConversation(normalizedId);
if (!conversation) { if (!conversation) {
console.warn('[ReadReceiptManager] 会话不存在:', normalizedId); 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), 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); const newTotalUnread = Math.max(0, currentUnread.total - prevUnreadCount);
this.stateManager.setUnreadCount(newTotalUnread, currentUnread.system); store.setUnreadCount(newTotalUnread, currentUnread.system);
// 3. 更新本地数据库 // 3. 更新本地数据库
markConversationAsRead(normalizedId).catch(console.error); markConversationAsRead(normalizedId).catch(console.error);
// 4. 立即通知所有订阅者 // 4. 立即通知所有订阅者
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_read', type: 'message_read',
payload: { payload: {
conversationId: normalizedId, conversationId: normalizedId,
@@ -91,7 +89,7 @@ export class ReadReceiptManager implements IReadReceiptManager {
timestamp: Date.now(), timestamp: Date.now(),
}); });
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: newTotalUnread, totalUnreadCount: newTotalUnread,
@@ -107,10 +105,10 @@ export class ReadReceiptManager implements IReadReceiptManager {
console.error('[ReadReceiptManager] 标记已读API失败:', error); console.error('[ReadReceiptManager] 标记已读API失败:', error);
// 失败时回滚状态 // 失败时回滚状态
this.stateManager.updateConversation(conversation); store.updateConversation(conversation);
this.stateManager.setUnreadCount(currentUnread.total, currentUnread.system); store.setUnreadCount(currentUnread.total, currentUnread.system);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_read', type: 'message_read',
payload: { payload: {
conversationId: normalizedId, conversationId: normalizedId,
@@ -146,18 +144,19 @@ export class ReadReceiptManager implements IReadReceiptManager {
* *
*/ */
async markAllAsRead(): Promise<void> { async markAllAsRead(): Promise<void> {
const conversations = this.stateManager.getState().conversations; const store = useMessageStore.getState();
const prevTotalUnread = this.stateManager.getUnreadCount().total; const conversations = store.conversations;
const currentSystem = this.stateManager.getUnreadCount().system; const prevTotalUnread = store.getUnreadCount().total;
const currentSystem = store.getUnreadCount().system;
// 乐观更新 // 乐观更新
conversations.forEach(conv => { conversations.forEach(conv => {
conv.unread_count = 0; conv.unread_count = 0;
}); });
this.stateManager.setUnreadCount(0, currentSystem); store.setUnreadCount(0, currentSystem);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: 0, totalUnreadCount: 0,
@@ -182,9 +181,9 @@ export class ReadReceiptManager implements IReadReceiptManager {
} catch (error) { } catch (error) {
console.error('[ReadReceiptManager] 标记所有已读失败:', error); console.error('[ReadReceiptManager] 标记所有已读失败:', error);
// 回滚 // 回滚
this.stateManager.setUnreadCount(prevTotalUnread, currentSystem); store.setUnreadCount(prevTotalUnread, currentSystem);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: prevTotalUnread, totalUnreadCount: prevTotalUnread,
@@ -258,18 +257,11 @@ export class ReadReceiptManager implements IReadReceiptManager {
return this.pendingReadMap; return this.pendingReadMap;
} }
/**
* ID
*/
private normalizeConversationId(conversationId: string | number | null | undefined): string {
return conversationId == null ? '' : String(conversationId);
}
/** /**
* *
*/ */
applyConversationFromFetch(conv: ConversationResponse): ConversationResponse { applyConversationFromFetch(conv: ConversationResponse): ConversationResponse {
const id = this.normalizeConversationId(conv.id); const id = normalizeConversationId(conv.id);
const readRecord = this.pendingReadMap.get(id); const readRecord = this.pendingReadMap.get(id);
let next: ConversationResponse; let next: ConversationResponse;
@@ -310,4 +302,4 @@ export class ReadReceiptManager implements IReadReceiptManager {
this.pendingReadMap.clear(); this.pendingReadMap.clear();
this.readStateVersion = 0; this.readStateVersion = 0;
} }
} }

View File

@@ -3,10 +3,10 @@
* *
*/ */
import type { MessageResponse, UserDTO } from '../../types/dto'; import type { MessageResponse, UserDTO } from '../../../types/dto';
import { api } from '../../services/api'; import { api } from '../../../services/api';
import { getUserCache, saveUserCache } from '../../services/database'; import { getUserCache, saveUserCache } from '../../../services/database';
import type { IUserCacheService } from './types'; import type { IUserCacheService } from '../types';
export class UserCacheService implements IUserCacheService { export class UserCacheService implements IUserCacheService {
// 正在获取用户信息的请求映射(用于去重) // 正在获取用户信息的请求映射(用于去重)
@@ -116,4 +116,4 @@ export class UserCacheService implements IUserCacheService {
} }
// 单例导出 // 单例导出
export const userCacheService = new UserCacheService(); export const userCacheService = new UserCacheService();

View File

@@ -1,6 +1,8 @@
/** /**
* WebSocket * WebSocket
* WebSocket * WebSocket
*
* 使 zustand store IMessageStateManager
*/ */
import type { import type {
@@ -12,22 +14,21 @@ import type {
WSGroupRecallMessage, WSGroupRecallMessage,
WSGroupTypingMessage, WSGroupTypingMessage,
WSGroupNoticeMessage, WSGroupNoticeMessage,
} from '../../services/wsService'; } from '../../../services/wsService';
import { wsService, GroupNoticeType } from '../../services/wsService'; import { wsService, GroupNoticeType } from '../../../services/wsService';
import { vibrateOnMessage } from '../../services/messageVibrationService'; import { vibrateOnMessage } from '../../../services/messageVibrationService';
import { saveMessage, updateMessageStatus } from '../../services/database'; import { saveMessage, updateMessageStatus } from '../../../services/database';
import type { MessageResponse, ConversationResponse } from '../../types/dto'; import type { MessageResponse, ConversationResponse } from '../../../types/dto';
import type { import type {
IWSMessageHandler, IWSMessageHandler,
IMessageStateManager,
IMessageDeduplication, IMessageDeduplication,
IUserCacheService, IUserCacheService,
HandleNewMessageOptions, HandleNewMessageOptions,
} from './types'; } from '../types';
import { MAX_FLUSH_ITERATIONS } from './constants'; import { MAX_FLUSH_ITERATIONS } from '../constants';
import { useMessageStore, subscriptionManager, normalizeConversationId, mergeMessagesById } from '../store';
export class WSMessageHandler implements IWSMessageHandler { export class WSMessageHandler implements IWSMessageHandler {
private stateManager: IMessageStateManager;
private deduplication: IMessageDeduplication; private deduplication: IMessageDeduplication;
private userCacheService: IUserCacheService; private userCacheService: IUserCacheService;
private getCurrentUserId: () => string | null; private getCurrentUserId: () => string | null;
@@ -45,7 +46,6 @@ export class WSMessageHandler implements IWSMessageHandler {
private bufferedReadReceipts: Array<WSReadMessage | WSGroupReadMessage> = []; private bufferedReadReceipts: Array<WSReadMessage | WSGroupReadMessage> = [];
constructor( constructor(
stateManager: IMessageStateManager,
deduplication: IMessageDeduplication, deduplication: IMessageDeduplication,
userCacheService: IUserCacheService, userCacheService: IUserCacheService,
getCurrentUserId: () => string | null, getCurrentUserId: () => string | null,
@@ -56,7 +56,6 @@ export class WSMessageHandler implements IWSMessageHandler {
fetchMessages: (conversationId: string) => Promise<void>; fetchMessages: (conversationId: string) => Promise<void>;
} }
) { ) {
this.stateManager = stateManager;
this.deduplication = deduplication; this.deduplication = deduplication;
this.userCacheService = userCacheService; this.userCacheService = userCacheService;
this.getCurrentUserId = getCurrentUserId; this.getCurrentUserId = getCurrentUserId;
@@ -74,9 +73,11 @@ export class WSMessageHandler implements IWSMessageHandler {
this.sseUnsubscribe(); this.sseUnsubscribe();
} }
const store = useMessageStore.getState();
// 监听私聊消息 // 监听私聊消息
wsService.on('chat', (message: WSChatMessage) => { wsService.on('chat', (message: WSChatMessage) => {
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) { if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message); this.bufferedChatMessages.push(message);
return; return;
} }
@@ -85,7 +86,7 @@ export class WSMessageHandler implements IWSMessageHandler {
// 监听群聊消息 // 监听群聊消息
wsService.on('group_message', (message: WSGroupChatMessage) => { wsService.on('group_message', (message: WSGroupChatMessage) => {
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) { if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message); this.bufferedChatMessages.push(message);
return; return;
} }
@@ -94,7 +95,7 @@ export class WSMessageHandler implements IWSMessageHandler {
// 监听私聊已读回执 // 监听私聊已读回执
wsService.on('read', (message: WSReadMessage) => { wsService.on('read', (message: WSReadMessage) => {
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) { if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message); this.bufferedReadReceipts.push(message);
return; return;
} }
@@ -103,7 +104,7 @@ export class WSMessageHandler implements IWSMessageHandler {
// 监听群聊已读回执 // 监听群聊已读回执
wsService.on('group_read', (message: WSGroupReadMessage) => { wsService.on('group_read', (message: WSGroupReadMessage) => {
if (!this.stateManager.getState().isInitialized || this.isBootstrapping) { if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message); this.bufferedReadReceipts.push(message);
return; return;
} }
@@ -132,8 +133,8 @@ export class WSMessageHandler implements IWSMessageHandler {
// 监听连接状态 // 监听连接状态
wsService.onConnect(() => { wsService.onConnect(() => {
this.stateManager.setSSEConnected(true); store.setSSEConnected(true);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'connection_changed', type: 'connection_changed',
payload: { connected: true }, payload: { connected: true },
timestamp: Date.now(), timestamp: Date.now(),
@@ -143,7 +144,7 @@ export class WSMessageHandler implements IWSMessageHandler {
const now = Date.now(); const now = Date.now();
if (now - this.lastReconnectSyncAt > 1500) { if (now - this.lastReconnectSyncAt > 1500) {
this.lastReconnectSyncAt = now; this.lastReconnectSyncAt = now;
const activeConversation = this.stateManager.getActiveConversation(); const activeConversation = store.getActiveConversation();
if (!activeConversation) { if (!activeConversation) {
this.fetchConversationsCallback(true, 'sse-reconnect').catch(error => { this.fetchConversationsCallback(true, 'sse-reconnect').catch(error => {
@@ -164,8 +165,8 @@ export class WSMessageHandler implements IWSMessageHandler {
}); });
wsService.onDisconnect(() => { wsService.onDisconnect(() => {
this.stateManager.setSSEConnected(false); store.setSSEConnected(false);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'connection_changed', type: 'connection_changed',
payload: { connected: false }, payload: { connected: false },
timestamp: Date.now(), timestamp: Date.now(),
@@ -187,7 +188,7 @@ export class WSMessageHandler implements IWSMessageHandler {
* *
*/ */
isConnected(): boolean { isConnected(): boolean {
return this.stateManager.isConnected(); return useMessageStore.getState().isConnected();
} }
/** /**
@@ -242,8 +243,9 @@ export class WSMessageHandler implements IWSMessageHandler {
message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean }, message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean },
options?: HandleNewMessageOptions options?: HandleNewMessageOptions
): Promise<void> { ): Promise<void> {
const store = useMessageStore.getState();
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message; 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 currentUserId = this.getCurrentUserId();
const suppressUnreadIncrement = options?.suppressUnreadIncrement ?? false; const suppressUnreadIncrement = options?.suppressUnreadIncrement ?? false;
const suppressVibration = options?.suppressVibration ?? 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') { if (message.type === 'group_message' && sender_id && sender_id !== currentUserId && sender_id !== '10000') {
this.userCacheService.getSenderInfo(sender_id).then(user => { this.userCacheService.getSenderInfo(sender_id).then(user => {
if (user) { if (user) {
const currentMessages = this.stateManager.getMessages(normalizedConversationId); const currentMessages = store.getMessages(normalizedConversationId);
if (currentMessages) { if (currentMessages) {
const updatedMessages = currentMessages.map(m => const updatedMessages = currentMessages.map(m =>
m.id === id ? { ...m, sender: user } : m m.id === id ? { ...m, sender: user } : m
); );
this.stateManager.setMessages(normalizedConversationId, updatedMessages); store.setMessages(normalizedConversationId, updatedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId: normalizedConversationId, 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)); const messageExists = existingMessages.some(m => String(m.id) === String(id));
if (!messageExists) { if (!messageExists) {
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]); const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
this.stateManager.setMessages(normalizedConversationId, updatedMessages); store.setMessages(normalizedConversationId, updatedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { payload: {
conversationId: normalizedConversationId, conversationId: normalizedConversationId,
@@ -318,7 +320,7 @@ export class WSMessageHandler implements IWSMessageHandler {
// 更新未读数 // 更新未读数
const isCurrentUserMessage = sender_id === currentUserId; const isCurrentUserMessage = sender_id === currentUserId;
const isActiveConversation = normalizedConversationId === this.stateManager.getActiveConversation(); const isActiveConversation = normalizedConversationId === store.getActiveConversation();
const shouldIncrementUnread = const shouldIncrementUnread =
!suppressUnreadIncrement && !suppressUnreadIncrement &&
@@ -358,7 +360,7 @@ export class WSMessageHandler implements IWSMessageHandler {
}); });
// 通知收到新消息 // 通知收到新消息
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_received', type: 'message_received',
payload: { payload: {
conversationId: conversation_id, conversationId: conversation_id,
@@ -388,12 +390,12 @@ export class WSMessageHandler implements IWSMessageHandler {
*/ */
handleRecallMessage(message: WSRecallMessage): void { handleRecallMessage(message: WSRecallMessage): void {
const { conversation_id, message_id } = message; const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id); const normalizedConversationId = normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id); this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id); this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_recalled', type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id }, payload: { conversationId: normalizedConversationId, messageId: message_id },
timestamp: Date.now(), timestamp: Date.now(),
@@ -409,12 +411,12 @@ export class WSMessageHandler implements IWSMessageHandler {
*/ */
handleGroupRecallMessage(message: WSGroupRecallMessage): void { handleGroupRecallMessage(message: WSGroupRecallMessage): void {
const { conversation_id, message_id } = message; const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id); const normalizedConversationId = normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id); this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id); this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'message_recalled', type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true }, payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
timestamp: Date.now(), timestamp: Date.now(),
@@ -429,10 +431,11 @@ export class WSMessageHandler implements IWSMessageHandler {
* *
*/ */
handleGroupTyping(message: WSGroupTypingMessage): void { handleGroupTyping(message: WSGroupTypingMessage): void {
const store = useMessageStore.getState();
const { group_id, user_id, is_typing } = message; const { group_id, user_id, is_typing } = message;
const groupIdStr = String(group_id); const groupIdStr = String(group_id);
const currentTypingUsers = this.stateManager.getTypingUsers(groupIdStr); const currentTypingUsers = store.getTypingUsers(groupIdStr);
let updatedTypingUsers: string[]; let updatedTypingUsers: string[];
if (is_typing) { if (is_typing) {
@@ -446,8 +449,8 @@ export class WSMessageHandler implements IWSMessageHandler {
} }
if (updatedTypingUsers.length !== currentTypingUsers.length) { if (updatedTypingUsers.length !== currentTypingUsers.length) {
this.stateManager.setTypingUsers(groupIdStr, updatedTypingUsers); store.setTypingUsers(groupIdStr, updatedTypingUsers);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'typing_status', type: 'typing_status',
payload: { groupId: groupIdStr, typingUsers: updatedTypingUsers }, payload: { groupId: groupIdStr, typingUsers: updatedTypingUsers },
timestamp: Date.now(), timestamp: Date.now(),
@@ -459,6 +462,7 @@ export class WSMessageHandler implements IWSMessageHandler {
* *
*/ */
handleGroupNotice(message: WSGroupNoticeMessage): void { handleGroupNotice(message: WSGroupNoticeMessage): void {
const store = useMessageStore.getState();
const { notice_type, group_id, data, timestamp, message_id, seq } = message; const { notice_type, group_id, data, timestamp, message_id, seq } = message;
const groupIdStr = String(group_id); const groupIdStr = String(group_id);
@@ -469,7 +473,7 @@ export class WSMessageHandler implements IWSMessageHandler {
if (mutedUserId && mutedUserId === currentUserId) { if (mutedUserId && mutedUserId === currentUserId) {
const isMuted = notice_type === 'muted'; 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) { if (message_id && typeof seq === 'number' && seq > 0) {
const conversationId = this.findConversationIdByGroupId(groupIdStr); const conversationId = this.findConversationIdByGroupId(groupIdStr);
if (conversationId) { if (conversationId) {
const existingMessages = this.stateManager.getMessages(conversationId); const existingMessages = store.getMessages(conversationId);
const exists = existingMessages.some(m => String(m.id) === String(message_id)); const exists = existingMessages.some(m => String(m.id) === String(message_id));
if (!exists) { if (!exists) {
const noticeText = this.buildGroupNoticeText(notice_type, data); 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); 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', type: 'messages_updated',
payload: { payload: {
conversationId, conversationId,
@@ -512,7 +516,7 @@ export class WSMessageHandler implements IWSMessageHandler {
} }
// 通知订阅者群通知 // 通知订阅者群通知
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'group_notice', type: 'group_notice',
payload: { payload: {
groupId: groupIdStr, 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( private updateConversationWithNewMessage(
conversationId: string, conversationId: string,
message: MessageResponse, message: MessageResponse,
createdAt: string createdAt: string
): void { ): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (conversation) { if (conversation) {
const updatedConv: ConversationResponse = { const updatedConv: ConversationResponse = {
@@ -556,48 +548,50 @@ export class WSMessageHandler implements IWSMessageHandler {
last_message_at: createdAt, last_message_at: createdAt,
updated_at: createdAt, updated_at: createdAt,
}; };
this.stateManager.updateConversation(updatedConv); store.updateConversation(updatedConv);
} }
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
private incrementUnreadCount(conversationId: string): void { private incrementUnreadCount(conversationId: string): void {
const conversation = this.stateManager.getConversation(conversationId); const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (conversation) { if (conversation) {
const prevUnreadCount = conversation.unread_count || 0; const prevUnreadCount = conversation.unread_count || 0;
const updatedConv: ConversationResponse = { const updatedConv: ConversationResponse = {
...conversation, ...conversation,
unread_count: prevUnreadCount + 1, unread_count: prevUnreadCount + 1,
}; };
this.stateManager.updateConversation(updatedConv); store.updateConversation(updatedConv);
const currentUnread = this.stateManager.getUnreadCount(); const currentUnread = store.getUnreadCount();
this.stateManager.setUnreadCount(currentUnread.total + 1, currentUnread.system); store.setUnreadCount(currentUnread.total + 1, currentUnread.system);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'unread_count_updated', type: 'unread_count_updated',
payload: { payload: {
totalUnreadCount: this.stateManager.getUnreadCount().total, totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: this.stateManager.getUnreadCount().system, systemUnreadCount: store.getUnreadCount().system,
}, },
timestamp: Date.now(), timestamp: Date.now(),
}); });
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'conversations_updated', type: 'conversations_updated',
payload: { conversations: this.stateManager.getConversations() }, payload: { conversations: store.getConversations() },
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
} }
private markMessageAsRecalled(conversationId: string, messageId: string): void { 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; if (!messages) return;
let changed = false; let changed = false;
@@ -615,8 +609,8 @@ export class WSMessageHandler implements IWSMessageHandler {
if (!changed) return; if (!changed) return;
this.stateManager.setMessages(conversationId, updatedMessages); store.setMessages(conversationId, updatedMessages);
this.stateManager.notifySubscribers({ subscriptionManager.notifySubscribers({
type: 'messages_updated', type: 'messages_updated',
payload: { conversationId, messages: updatedMessages }, payload: { conversationId, messages: updatedMessages },
timestamp: Date.now(), timestamp: Date.now(),
@@ -624,7 +618,8 @@ export class WSMessageHandler implements IWSMessageHandler {
} }
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void { 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 (!conversation?.last_message) return;
if (String(conversation.last_message.id) !== String(messageId)) return; if (String(conversation.last_message.id) !== String(messageId)) return;
if (conversation.last_message.status === 'recalled') return; if (conversation.last_message.status === 'recalled') return;
@@ -636,11 +631,12 @@ export class WSMessageHandler implements IWSMessageHandler {
status: 'recalled', status: 'recalled',
}, },
}; };
this.stateManager.updateConversation(updatedConversation); store.updateConversation(updatedConversation);
} }
private findConversationIdByGroupId(groupId: string): string | null { private findConversationIdByGroupId(groupId: string): string | null {
const conversations = this.stateManager.getConversations(); const store = useMessageStore.getState();
const conversations = store.getConversations();
for (const conv of conversations) { for (const conv of conversations) {
if (String(conv.group?.id || '') === groupId) { if (String(conv.group?.id || '') === groupId) {
return conv.id; return conv.id;
@@ -666,4 +662,4 @@ export class WSMessageHandler implements IWSMessageHandler {
return ''; return '';
} }
} }
} }

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

View File

@@ -1,15 +1,13 @@
/** /**
* MessageManager - 消息管理核心模块 * MessageManager - 消息管理核心模块
* *
* 向后兼容的重导出文件 * ⚠️ 此文件已废弃,保留仅用于向后兼容
* 所有实现已迁移到 ./message 目录下的模块化组件
* *
* 新代码应该使用: * 新代码应该使用:
* import { messageManager } from '@/stores/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,
MessageManager, MessageManager,
// 状态管理器
MessageStateManager,
messageStateManager,
// 消息去重服务 // 消息去重服务
MessageDeduplication, MessageDeduplication,
messageDeduplication, messageDeduplication,
@@ -68,7 +62,6 @@ export type {
HandleNewMessageOptions, HandleNewMessageOptions,
// 服务接口类型 // 服务接口类型
IMessageStateManager,
IMessageDeduplication, IMessageDeduplication,
IUserCacheService, IUserCacheService,
IReadReceiptManager, IReadReceiptManager,