refactor(message): modularize MessageManager architecture
重构消息管理模块,将单体 MessageManager 拆分为职责单一的子模块: - MessageStateManager: 纯状态管理 - MessageDeduplication: 消息去重服务 - UserCacheService: 用户缓存服务 - ReadReceiptManager: 已读回执管理 - ConversationOperations: 会话操作 - MessageSendService: 消息发送 - MessageSyncService: 消息同步 - WSMessageHandler: WebSocket 消息处理 新增类型定义和常量文件,保持原有 API 向后兼容。修复 CallScreen 状态值错误,补充 PostDetailScreen 缺失样式,优化 GroupInfoPanel 导入。
This commit is contained in:
@@ -1,180 +1,643 @@
|
||||
/**
|
||||
* 消息同步服务
|
||||
* 负责从服务器同步消息和会话
|
||||
* 处理消息和会话的服务器同步逻辑
|
||||
*/
|
||||
|
||||
import type { ConversationResponse, MessageResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { messageRepository } from '../../data/repositories/MessageRepository';
|
||||
import type { Message, Conversation } from '../../core/entities/Message';
|
||||
import type { ConversationListResponse, MessageListResponse } from '../../types/dto';
|
||||
import {
|
||||
getMessagesByConversation,
|
||||
getMaxSeq,
|
||||
saveMessagesBatch,
|
||||
saveConversationsWithRelatedCache,
|
||||
getMessagesBeforeSeq,
|
||||
} from '../../services/database';
|
||||
import {
|
||||
type IConversationListPagedSource,
|
||||
SqliteConversationListPagedSource,
|
||||
createRemoteConversationListSource,
|
||||
CONVERSATION_LIST_PAGE_SIZE,
|
||||
} from '../conversationListSources';
|
||||
import type { IMessageSyncService, IMessageStateManager, MessageManagerConversationListDeps } from './types';
|
||||
|
||||
export interface SyncOptions {
|
||||
force?: boolean;
|
||||
lastSeq?: number;
|
||||
}
|
||||
export class MessageSyncService implements IMessageSyncService {
|
||||
private stateManager: IMessageStateManager;
|
||||
private getCurrentUserId: () => string | null;
|
||||
|
||||
export interface SyncResult {
|
||||
conversations: Conversation[];
|
||||
messages: Message[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
/** 远端会话列表(游标或页码) */
|
||||
private readonly remoteConversationListSource: IConversationListPagedSource;
|
||||
/** 本地会话列表缓存(SQLite) */
|
||||
private readonly localConversationListSource: IConversationListPagedSource;
|
||||
|
||||
export class MessageSyncService {
|
||||
private syncingConversations: boolean = false;
|
||||
private syncingMessages: Map<string, boolean> = new Map();
|
||||
/** 正在加载会话列表下一页 */
|
||||
private loadingMoreConversations = false;
|
||||
/** 聊天页活跃期间延迟的会话列表刷新 */
|
||||
private deferredConversationRefresh = false;
|
||||
|
||||
async syncConversations(): Promise<Conversation[]> {
|
||||
if (this.syncingConversations) {
|
||||
console.log('[MessageSyncService] Already syncing conversations');
|
||||
constructor(
|
||||
stateManager: IMessageStateManager,
|
||||
getCurrentUserId: () => string | null,
|
||||
deps?: MessageManagerConversationListDeps
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.getCurrentUserId = getCurrentUserId;
|
||||
|
||||
const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE;
|
||||
this.remoteConversationListSource =
|
||||
deps?.remoteConversationListSource ??
|
||||
createRemoteConversationListSource(
|
||||
deps?.remoteListKind === 'offset' ? 'offset' : 'cursor',
|
||||
pageSize
|
||||
);
|
||||
this.localConversationListSource =
|
||||
deps?.localConversationListSource ?? new SqliteConversationListPagedSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> {
|
||||
if (this.stateManager.isLoading() && !forceRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldDeferConversationRefresh(source, forceRefresh)) {
|
||||
this.deferredConversationRefresh = true;
|
||||
if (__DEV__) {
|
||||
console.log('[MessageSyncService] defer fetchConversations', {
|
||||
source,
|
||||
activeConversationId: this.stateManager.getActiveConversation(),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('[MessageSyncService] fetchConversations', {
|
||||
source,
|
||||
forceRefresh,
|
||||
activeConversationId: this.stateManager.getActiveConversation(),
|
||||
});
|
||||
}
|
||||
|
||||
// 非强制刷新且内存为空:先用本地源暖机
|
||||
const currentState = this.stateManager.getState();
|
||||
if (!forceRefresh && currentState.conversations.size === 0) {
|
||||
const warmed = await this.hydrateConversationsFromLocalSource();
|
||||
if (warmed) {
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
this.stateManager.setLoading(true);
|
||||
const emitLoadingToUi = this.stateManager.getConversations().length === 0;
|
||||
if (emitLoadingToUi) {
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'conversations_loading',
|
||||
payload: { loading: true },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.remoteConversationListSource.restart();
|
||||
const page = await this.remoteConversationListSource.loadNext();
|
||||
|
||||
currentState.conversations.clear();
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
currentState.conversations.set(normalizedConv.id, normalizedConv);
|
||||
});
|
||||
|
||||
this.updateConversationList();
|
||||
this.recomputeConversationTotalUnread();
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取会话列表失败:', error);
|
||||
if (this.stateManager.getConversations().length === 0) {
|
||||
const recovered = await this.hydrateConversationsFromLocalSource();
|
||||
if (recovered) {
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
}
|
||||
}
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'fetchConversations' },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
this.stateManager.setLoading(false);
|
||||
if (emitLoadingToUi) {
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'conversations_loading',
|
||||
payload: { loading: false },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话列表加载下一页
|
||||
*/
|
||||
async loadMoreConversations(): Promise<void> {
|
||||
if (!this.remoteConversationListSource.hasMore) {
|
||||
return;
|
||||
}
|
||||
if (this.loadingMoreConversations || this.stateManager.isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingMoreConversations = true;
|
||||
try {
|
||||
const page = await this.remoteConversationListSource.loadNext();
|
||||
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
this.stateManager.updateConversation(normalizedConv);
|
||||
});
|
||||
|
||||
this.recomputeConversationTotalUnread();
|
||||
this.emitConversationListAndUnreadUpdates();
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 加载更多会话失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'loadMoreConversations' },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
this.loadingMoreConversations = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个会话详情
|
||||
*/
|
||||
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
|
||||
const normalizedId = this.normalizeConversationId(conversationId);
|
||||
try {
|
||||
const detail = await messageService.getConversationById(normalizedId);
|
||||
if (detail) {
|
||||
const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse);
|
||||
this.stateManager.updateConversation(normalizedDetail);
|
||||
return normalizedDetail;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取会话详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息(增量同步)
|
||||
*/
|
||||
async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> {
|
||||
// 防止重复加载
|
||||
if (this.stateManager.isLoadingMessages(conversationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateManager.setLoadingMessages(conversationId, true);
|
||||
|
||||
try {
|
||||
const existingMessagesAtStart = this.stateManager.getMessages(conversationId);
|
||||
const hasInMemoryMessages = existingMessagesAtStart.length > 0;
|
||||
let baselineMaxSeq = hasInMemoryMessages
|
||||
? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0)
|
||||
: 0;
|
||||
|
||||
if (!afterSeq) {
|
||||
// 先从本地数据库加载
|
||||
if (!hasInMemoryMessages) {
|
||||
try {
|
||||
const localMessages = await getMessagesByConversation(conversationId, 20);
|
||||
const localMaxSeq = await getMaxSeq(conversationId);
|
||||
baselineMaxSeq = localMaxSeq;
|
||||
|
||||
if (localMessages.length > 0) {
|
||||
const formattedMessages: MessageResponse[] = localMessages.map(m => ({
|
||||
id: m.id,
|
||||
conversation_id: m.conversationId,
|
||||
sender_id: m.senderId,
|
||||
seq: m.seq,
|
||||
segments: m.segments || [],
|
||||
status: m.status as any,
|
||||
created_at: m.createdAt,
|
||||
}));
|
||||
|
||||
this.stateManager.setMessages(conversationId, formattedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: formattedMessages,
|
||||
source: 'local',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
// 冷启动兜底:先下发空列表事件
|
||||
this.stateManager.setMessages(conversationId, []);
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: [],
|
||||
source: 'local_empty',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MessageSyncService] 读取本地消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务端快照 + 增量同步
|
||||
try {
|
||||
const snapshotResp = await messageService.getMessages(conversationId, undefined, undefined, 50);
|
||||
const snapshotMessages = snapshotResp?.messages || [];
|
||||
|
||||
if (snapshotMessages.length > 0) {
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedSnapshot = this.mergeMessagesById(existingMessages, snapshotMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedSnapshot);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: mergedSnapshot,
|
||||
newMessages: snapshotMessages,
|
||||
source: 'server_snapshot',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 持久化到本地
|
||||
saveMessagesBatch(snapshotMessages.map((m: any) => ({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
isRead: m.is_read || false,
|
||||
createdAt: m.created_at,
|
||||
seq: m.seq,
|
||||
status: m.status || 'normal',
|
||||
segments: m.segments,
|
||||
}))).catch(error => {
|
||||
console.error('[MessageSyncService] 保存快照消息到本地失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 增量补齐
|
||||
const snapshotMaxSeq = snapshotMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0);
|
||||
if (snapshotMaxSeq > baselineMaxSeq) {
|
||||
const incrementalResp = await messageService.getMessages(conversationId, baselineMaxSeq);
|
||||
const newMessages = incrementalResp?.messages || [];
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: mergedMessages,
|
||||
newMessages,
|
||||
source: 'server_incremental',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
saveMessagesBatch(newMessages.map((m: any) => ({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
isRead: m.is_read || false,
|
||||
createdAt: m.created_at,
|
||||
seq: m.seq,
|
||||
status: m.status || 'normal',
|
||||
segments: m.segments,
|
||||
}))).catch(error => {
|
||||
console.error('[MessageSyncService] 保存增量消息到本地失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 快照/增量同步失败:', error);
|
||||
}
|
||||
} else {
|
||||
// 指定了 afterSeq
|
||||
const response = await messageService.getMessages(conversationId, afterSeq);
|
||||
|
||||
if (response?.messages && response.messages.length > 0) {
|
||||
const newMessages = response.messages;
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeMessagesById(existingMessages, newMessages);
|
||||
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: mergedMessages,
|
||||
newMessages,
|
||||
source: 'server',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
saveMessagesBatch(newMessages.map((m: any) => ({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
isRead: m.is_read || false,
|
||||
createdAt: m.created_at,
|
||||
seq: m.seq,
|
||||
status: m.status || 'normal',
|
||||
segments: m.segments,
|
||||
}))).catch(error => {
|
||||
console.error('[MessageSyncService] 保存消息到本地失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取消息失败:', error);
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'fetchMessages', conversationId },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
this.stateManager.setLoadingMessages(conversationId, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多历史消息
|
||||
*/
|
||||
async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
|
||||
try {
|
||||
// 先从本地获取
|
||||
const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit);
|
||||
|
||||
if (localMessages.length >= limit) {
|
||||
const formattedMessages: MessageResponse[] = [...localMessages].reverse().map(m => ({
|
||||
id: m.id,
|
||||
conversation_id: m.conversationId,
|
||||
sender_id: m.senderId,
|
||||
seq: m.seq,
|
||||
segments: m.segments || [],
|
||||
status: m.status as any,
|
||||
created_at: m.createdAt,
|
||||
}));
|
||||
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: { conversationId, messages: mergedMessages, source: 'local_history' },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return formattedMessages;
|
||||
}
|
||||
|
||||
// 本地数据不足,从服务端获取
|
||||
const response = await messageService.getMessages(conversationId, undefined, beforeSeq, limit);
|
||||
|
||||
if (response?.messages && response.messages.length > 0) {
|
||||
const serverMessages = response.messages;
|
||||
|
||||
await saveMessagesBatch(serverMessages.map((m: any) => ({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
isRead: m.is_read || false,
|
||||
createdAt: m.created_at,
|
||||
seq: m.seq,
|
||||
status: m.status || 'normal',
|
||||
segments: m.segments,
|
||||
})));
|
||||
|
||||
const existingMessages = this.stateManager.getMessages(conversationId);
|
||||
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
|
||||
this.stateManager.setMessages(conversationId, mergedMessages);
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'messages_updated',
|
||||
payload: { conversationId, messages: mergedMessages, source: 'server_history' },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return serverMessages;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 加载更多消息失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
this.syncingConversations = true;
|
||||
|
||||
/**
|
||||
* 获取未读数
|
||||
*/
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
try {
|
||||
const response: ConversationListResponse = await messageService.getConversations();
|
||||
const conversations = this.mapConversations(response.list || []);
|
||||
return conversations;
|
||||
const [unreadData, systemUnreadData] = await Promise.all([
|
||||
messageService.getUnreadCount(),
|
||||
messageService.getSystemUnreadCount(),
|
||||
]);
|
||||
|
||||
const totalUnread = unreadData?.total_unread_count ?? 0;
|
||||
const systemUnread = systemUnreadData?.unread_count ?? 0;
|
||||
|
||||
this.stateManager.setUnreadCount(totalUnread, systemUnread);
|
||||
|
||||
// 服务端汇总未读为 0 时,清掉内存中残留的红点
|
||||
if (totalUnread === 0) {
|
||||
const currentState = this.stateManager.getState();
|
||||
let anyCleared = false;
|
||||
for (const [cid, conv] of currentState.conversations) {
|
||||
if ((conv.unread_count || 0) > 0) {
|
||||
currentState.conversations.set(cid, { ...conv, unread_count: 0 });
|
||||
anyCleared = true;
|
||||
}
|
||||
}
|
||||
if (anyCleared) {
|
||||
this.updateConversationList();
|
||||
this.persistConversationListCache();
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: totalUnread,
|
||||
systemUnreadCount: systemUnread,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to sync conversations:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.syncingConversations = false;
|
||||
console.error('[MessageSyncService] 获取未读数失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async syncMessages(
|
||||
conversationId: string,
|
||||
options: SyncOptions = {}
|
||||
): Promise<SyncResult> {
|
||||
const key = conversationId;
|
||||
/**
|
||||
* 检查是否可加载更多会话
|
||||
*/
|
||||
canLoadMoreConversations(): boolean {
|
||||
return this.remoteConversationListSource.hasMore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启数据源
|
||||
*/
|
||||
restartSources(): void {
|
||||
this.remoteConversationListSource.restart();
|
||||
this.localConversationListSource.restart();
|
||||
this.loadingMoreConversations = false;
|
||||
}
|
||||
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
|
||||
if (!this.stateManager.getActiveConversation()) return false;
|
||||
if (!forceRefresh) return false;
|
||||
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh';
|
||||
}
|
||||
|
||||
private normalizeConversationId(conversationId: string | number | null | undefined): string {
|
||||
return conversationId == null ? '' : String(conversationId);
|
||||
}
|
||||
|
||||
private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse {
|
||||
const id = this.normalizeConversationId(conv.id);
|
||||
return {
|
||||
...conv,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
private updateConversationList(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
const list = Array.from(currentState.conversations.values()).sort((a, b) => {
|
||||
const aPinned = a.is_pinned ? 1 : 0;
|
||||
const bPinned = b.is_pinned ? 1 : 0;
|
||||
if (aPinned !== bPinned) {
|
||||
return bPinned - aPinned;
|
||||
}
|
||||
const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime();
|
||||
const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime();
|
||||
return bTime - aTime;
|
||||
});
|
||||
currentState.conversationList = list;
|
||||
}
|
||||
|
||||
private recomputeConversationTotalUnread(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
currentState.totalUnreadCount = Array.from(currentState.conversations.values()).reduce(
|
||||
(sum, conv) => sum + (conv.unread_count || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private emitConversationListAndUnreadUpdates(): void {
|
||||
this.persistConversationListCache();
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.stateManager.getConversations() },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (this.syncingMessages.get(key)) {
|
||||
console.log('[MessageSyncService] Already syncing messages for:', conversationId);
|
||||
return { conversations: [], messages: [], hasMore: false };
|
||||
const unreadCount = this.stateManager.getUnreadCount();
|
||||
this.stateManager.notifySubscribers({
|
||||
type: 'unread_count_updated',
|
||||
payload: {
|
||||
totalUnreadCount: unreadCount.total,
|
||||
systemUnreadCount: unreadCount.system,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
private persistConversationListCache(): void {
|
||||
const currentState = this.stateManager.getState();
|
||||
const list = Array.from(currentState.conversations.values());
|
||||
const users = list.flatMap(conv => [
|
||||
...(conv.participants || []),
|
||||
...(conv.last_message?.sender ? [conv.last_message.sender] : []),
|
||||
]);
|
||||
const groups = list
|
||||
.map(conv => conv.group)
|
||||
.filter((group): group is NonNullable<ConversationResponse['group']> => Boolean(group));
|
||||
|
||||
saveConversationsWithRelatedCache(list, users, groups).catch(error => {
|
||||
console.error('[MessageSyncService] 持久化会话列表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
private async hydrateConversationsFromLocalSource(): Promise<boolean> {
|
||||
this.localConversationListSource.restart();
|
||||
const page = await this.localConversationListSource.loadNext();
|
||||
if (!page.items.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.syncingMessages.set(key, true);
|
||||
const currentState = this.stateManager.getState();
|
||||
currentState.conversations.clear();
|
||||
page.items.forEach(conv => {
|
||||
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
||||
currentState.conversations.set(normalizedConv.id, normalizedConv);
|
||||
});
|
||||
|
||||
this.updateConversationList();
|
||||
this.recomputeConversationTotalUnread();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const lastSeq = options.lastSeq ?? await messageRepository.getMaxSeq(conversationId);
|
||||
|
||||
const response: MessageListResponse = await messageService.getMessages(
|
||||
conversationId,
|
||||
lastSeq,
|
||||
undefined,
|
||||
50
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
const messages = this.mapMessages(response.messages || []);
|
||||
|
||||
if (messages.length > 0) {
|
||||
await messageRepository.saveMessages(messages, true);
|
||||
}
|
||||
private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
|
||||
if (incoming.length === 0) return existing;
|
||||
|
||||
return {
|
||||
conversations: [],
|
||||
messages,
|
||||
hasMore: messages.length >= 50,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to sync messages:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.syncingMessages.delete(key);
|
||||
const incomingAsc = [...incoming].sort((a, b) => a.seq - b.seq);
|
||||
if (existing.length === 0) return incomingAsc;
|
||||
|
||||
const existingMinSeq = existing[0]?.seq ?? Number.MAX_SAFE_INTEGER;
|
||||
const incomingMaxSeq = incomingAsc[incomingAsc.length - 1]?.seq ?? Number.MIN_SAFE_INTEGER;
|
||||
|
||||
if (incomingMaxSeq < existingMinSeq) {
|
||||
return [...incomingAsc, ...existing];
|
||||
}
|
||||
}
|
||||
|
||||
async loadHistoryMessages(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
try {
|
||||
const response: MessageListResponse = await messageService.getMessages(
|
||||
conversationId,
|
||||
undefined,
|
||||
beforeSeq,
|
||||
limit
|
||||
);
|
||||
|
||||
const messages = this.mapMessages(response.messages || []);
|
||||
|
||||
if (messages.length > 0) {
|
||||
await messageRepository.saveMessages(messages, true);
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] Failed to load history:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadLocalMessages(
|
||||
conversationId: string,
|
||||
limit: number = 50
|
||||
): Promise<Message[]> {
|
||||
return messageRepository.getMessagesByConversation(conversationId, limit);
|
||||
}
|
||||
|
||||
async loadHistoryFromLocal(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
return messageRepository.getMessagesBeforeSeq(conversationId, beforeSeq, limit);
|
||||
}
|
||||
|
||||
private mapConversations(list: ConversationListResponse['list']): Conversation[] {
|
||||
return list.map(conv => ({
|
||||
id: conv.id,
|
||||
type: conv.type || 'private',
|
||||
isPinned: conv.is_pinned || false,
|
||||
lastSeq: conv.last_seq || 0,
|
||||
lastMessageAt: conv.last_message_at || new Date().toISOString(),
|
||||
unreadCount: conv.unread_count || 0,
|
||||
participants: (conv.participants || []).map(p => ({
|
||||
id: p.id,
|
||||
username: p.username,
|
||||
avatar: p.avatar || undefined,
|
||||
nickname: p.nickname,
|
||||
})),
|
||||
group: conv.group ? {
|
||||
id: String(conv.group.id),
|
||||
name: conv.group.name,
|
||||
avatar: conv.group.avatar,
|
||||
} : undefined,
|
||||
createdAt: conv.created_at || new Date().toISOString(),
|
||||
updatedAt: conv.updated_at || new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
private mapMessages(messages: MessageListResponse['messages']): Message[] {
|
||||
return messages.map(msg => ({
|
||||
id: msg.id,
|
||||
conversationId: msg.conversation_id,
|
||||
senderId: msg.sender_id,
|
||||
seq: msg.seq,
|
||||
segments: msg.segments || [],
|
||||
createdAt: msg.created_at,
|
||||
status: msg.status || 'normal',
|
||||
sender: msg.sender ? {
|
||||
id: msg.sender.id,
|
||||
username: msg.sender.username,
|
||||
avatar: msg.sender.avatar || undefined,
|
||||
nickname: msg.sender.nickname,
|
||||
} : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
isSyncingConversations(): boolean {
|
||||
return this.syncingConversations;
|
||||
}
|
||||
|
||||
isSyncingMessages(conversationId: string): boolean {
|
||||
return this.syncingMessages.get(conversationId) || false;
|
||||
return this.mergeMessagesById(existing, incomingAsc);
|
||||
}
|
||||
}
|
||||
|
||||
export const messageSyncService = new MessageSyncService();
|
||||
Reference in New Issue
Block a user