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