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';
// MessageManager 新架构导出(已替换 conversationStore
export { messageManager } from './messageManager';
// 重构:从 ./message 目录导入,移除了 messageManager.ts 重导出文件
export { messageManager, MessageManager } from './message';
export type {
MessageEvent,
MessageEventType,
MessageSubscriber,
MessageManagerConversationListDeps,
} from './messageManager';
} from './message';
export {
CONVERSATION_LIST_PAGE_SIZE,
type ConversationListPage,

View File

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

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 {
useMessageStore,
subscriptionManager,
normalizeConversationId,
mergeMessagesById,
notifySubscribers,
subscribeToMessages,
} from './store';
export type { MessageState, MessageActions, MessageStore } from './store';
// ==================== 类型导出 ====================
// 从 types.ts 导出所有类型
export type {
@@ -17,6 +32,7 @@ export type {
ReadStateRecord,
MessageManagerConversationListDeps,
HandleNewMessageOptions,
// 服务接口类型(保留向后兼容,但服务层已不再使用)
IMessageStateManager,
IMessageDeduplication,
IUserCacheService,
@@ -37,30 +53,57 @@ export {
MIN_RECONNECT_SYNC_INTERVAL,
} from './constants';
// ==================== 子模块导出(可选使用)====================
// 状态管理器
export { MessageStateManager, messageStateManager } from './MessageStateManager';
// ==================== 服务导出 ====================
export {
// 消息去重服务
MessageDeduplication,
messageDeduplication,
// 消息去重服务
export { MessageDeduplication, messageDeduplication } from './MessageDeduplication';
// 用户缓存服务
UserCacheService,
userCacheService,
// 用户缓存服务
export { UserCacheService, userCacheService } from './UserCacheService';
// 已读回执管理器
ReadReceiptManager,
// 已读回执管理器
export { ReadReceiptManager } from './ReadReceiptManager';
// 会话操作服务
ConversationOperations,
// 会话操作服务
export { ConversationOperations } from './ConversationOperations';
// 消息发送服务
MessageSendService,
// 消息发送服务
export { MessageSendService } from './MessageSendService';
// 消息同步服务
MessageSyncService,
// 消息同步服务
export { MessageSyncService } from './MessageSyncService';
// WebSocket 消息处理器
WSMessageHandler,
} from './services';
// WebSocket 消息处理器
export { WSMessageHandler } from './WSMessageHandler';
// ==================== Hooks 导出 ====================
export {
// 会话相关
useConversations,
useConversation,
useCreateConversation,
useUpdateConversation,
useActiveConversation,
// 消息相关
useMessages,
useSendMessage,
useMarkAsRead,
// 未读数相关
useUnreadCount,
useSystemUnreadCount,
// 群聊相关
useGroupTyping,
useGroupMuted,
// 通用
useMessageManager,
} from './hooks';
// ==================== 默认导出 ====================
export { default } from './MessageManager';

View File

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

View File

@@ -3,11 +3,11 @@
*
*/
import type { IMessageDeduplication } from './types';
import type { IMessageDeduplication } from '../types';
import {
PROCESSED_MESSAGE_ID_EXPIRY,
PROCESSED_MESSAGE_ID_MAX_SIZE,
} from './constants';
} from '../constants';
export class MessageDeduplication implements IMessageDeduplication {
// 已处理消息ID集合用于去重防止ACK消息和正常消息重复处理

View File

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

View File

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

View File

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

View File

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

View File

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

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 - 消息管理核心模块
*
* 向后兼容的重导出文件
* 所有实现已迁移到 ./message 目录下的模块化组件
* ⚠️ 此文件已废弃,保留仅用于向后兼容
*
* 新代码应该使用:
* import { messageManager } from '@/stores/message'
* import type { Message, Conversation, MessageEventManager } from '@/stores/message'
* import type { MessageEvent, MessageSubscriber } from '@/stores/message'
*
* 此文件保持向后兼容性,原有导入方式仍然有效:
* import { messageManager } from '@/stores/messageManager'
* 此文件将在未来版本中移除
*/
// 从新模块重导出所有内容
@@ -18,10 +16,6 @@ export {
messageManager,
MessageManager,
// 状态管理器
MessageStateManager,
messageStateManager,
// 消息去重服务
MessageDeduplication,
messageDeduplication,
@@ -68,7 +62,6 @@ export type {
HandleNewMessageOptions,
// 服务接口类型
IMessageStateManager,
IMessageDeduplication,
IUserCacheService,
IReadReceiptManager,