Files
frontend/src/stores/messageManager.ts
lafay 619f08275c
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 9m17s
Frontend CI / ota-android (push) Successful in 11m48s
Frontend CI / build-android-apk (push) Successful in 1h12m38s
Frontend CI / ota-android (pull_request) Has been skipped
Frontend CI / build-and-push-web (pull_request) Failing after 37s
Frontend CI / build-android-apk (pull_request) Has been cancelled
fix(PostImages, CreatePostScreen): adjust image gap for improved layout consistency
- Reduced image gap in PostImages and CreatePostScreen components from 12 to 4 (desktop) and 8 to 2 (mobile) for better spacing.
- Updated ImageGrid component to modify grid item widths for enhanced layout.
- Refactored PostRepository to improve local caching logic, ensuring non-blocking UI updates during cache writes.
- Introduced saveConversationsWithRelatedCache function to streamline conversation, user, and group data saving in the database.
2026-03-25 17:08:11 +08:00

2559 lines
84 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MessageManager - 消息管理核心模块
*
* 单一数据源原则:所有消息相关状态统一在此管理
* 解决核心问题:
* 1. 竞态条件:确保消息立即处理并同步到所有订阅者
* 2. 状态同步:已读状态在所有组件间保持一致
*
* 架构特点:
* - 管理conversations和messages状态
* - 统一处理SSE消息
* - 统一处理本地数据库读写
* - 提供订阅机制供React组件使用
*
* UI 只应订阅 MessageManager / hooks列表数据可能来自网络游标或 SQLite 缓存回退,对界面透明。
*/
import { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../types/dto';
import { messageService } from '../services/messageService';
import {
sseService,
WSChatMessage,
WSGroupChatMessage,
WSReadMessage,
WSGroupReadMessage,
WSRecallMessage,
WSGroupRecallMessage,
WSGroupTypingMessage,
WSGroupNoticeMessage,
GroupNoticeType,
} from '../services/sseService';
import {
saveMessage,
saveMessagesBatch,
getMessagesByConversation,
getMaxSeq,
getMinSeq,
getMessagesBeforeSeq,
markConversationAsRead,
updateConversationCacheUnreadCount,
CachedMessage,
getUserCache,
saveUserCache,
updateMessageStatus,
deleteConversation as deleteConversationFromDb,
saveConversationsWithRelatedCache,
} from '../services/database';
import { api } from '../services/api';
import { vibrateOnMessage } from '../services/messageVibrationService';
import { useAuthStore } from './authStore';
import {
type IConversationListPagedSource,
SqliteConversationListPagedSource,
createRemoteConversationListSource,
CONVERSATION_LIST_PAGE_SIZE,
} from './conversationListSources';
// ==================== 类型定义 ====================
export type MessageEventType =
| 'conversations_updated'
| 'conversations_loading'
| 'messages_updated'
| 'unread_count_updated'
| 'connection_changed'
| 'message_sent'
| 'message_read'
| 'message_received'
| 'message_recalled'
| 'typing_status'
| 'group_notice'
| 'error';
export interface MessageEvent {
type: MessageEventType;
payload: any;
timestamp: number;
}
export type MessageSubscriber = (event: MessageEvent) => void;
// ==================== 内部状态接口 ====================
interface MessageManagerState {
// 会话相关
conversations: Map<string, ConversationResponse>;
conversationList: ConversationResponse[];
// 消息相关 - 按会话ID存储
messagesMap: Map<string, MessageResponse[]>;
// 未读数
totalUnreadCount: number;
systemUnreadCount: number;
// 连接状态
isSSEConnected: boolean;
// 当前活动会话ID用户正在查看的会话
currentConversationId: string | null;
// 加载状态
isLoadingConversations: boolean;
loadingMessagesSet: Set<string>; // 正在加载消息的会话ID集合
// 初始化状态
isInitialized: boolean;
// 订阅者
subscribers: Set<MessageSubscriber>;
// 输入状态 - 按群组ID存储正在输入的用户ID列表
typingUsersMap: Map<string, string[]>;
// 当前用户的禁言状态 - 按群组ID存储
mutedStatusMap: Map<string, boolean>;
}
// ==================== 已读状态保护机制常量 ====================
/**
* 已读状态保护延迟时间(毫秒)
* API 完成后,继续保护一段时间,防止 fetchConversations 的旧数据覆盖
*/
const READ_STATE_PROTECTION_DELAY = 5000; // 5秒
/** 可注入会话列表源,便于测试或替换实现 */
export interface MessageManagerConversationListDeps {
remoteConversationListSource?: IConversationListPagedSource;
localConversationListSource?: IConversationListPagedSource;
/**
* 未传 remoteConversationListSource 时:默认 `cursor`/conversations/cursor
* `offset` 为常规页码(/conversations?page=&page_size=)。
*/
remoteListKind?: 'cursor' | 'offset';
/** 与 remoteListKind 搭配,默认与 CONVERSATION_LIST_PAGE_SIZE 一致 */
remoteListPageSize?: number;
}
/**
* 已读状态记录接口
*/
interface ReadStateRecord {
/** 标记已读的时间戳 */
timestamp: number;
/** 状态版本号,用于防止旧数据覆盖新数据 */
version: number;
/** 已上报的已读序号(用于去重) */
lastReadSeq: number;
/** 清除保护的定时器ID */
clearTimer?: NodeJS.Timeout;
}
// ==================== MessageManager 类 ====================
class MessageManager {
private state: MessageManagerState;
private sseUnsubscribe: (() => void) | null = null;
private currentUserId: string | null = null;
private authUnsubscribe: (() => void) | null = null;
private lastReconnectSyncAt: number = 0;
private initializePromise: Promise<void> | null = null;
private activatingConversationTasks: Map<string, Promise<void>> = new Map();
/** 正在加载会话列表下一页 */
private loadingMoreConversations = false;
/** 聊天页活跃期间延迟的会话列表刷新 */
private deferredConversationRefresh = false;
/** 远端会话列表(游标或页码,由注入/remoteListKind 决定) */
private readonly remoteConversationListSource: IConversationListPagedSource;
/** 本地会话列表缓存SQLite单批 */
private readonly localConversationListSource: IConversationListPagedSource;
/**
* 正在进行中的已读 API 请求集合
* 用于防止 fetchConversations 的服务器数据覆盖乐观已读状态
* key: conversationId, value: 已读状态记录(包含时间戳和版本号)
*/
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
/**
* 全局已读状态版本号,每次标记已读时递增
* 用于判断服务器数据是否比本地状态更旧
*/
private readStateVersion: number = 0;
// 初始化阶段缓冲来自 SSE 的消息事件:在已拉取服务器权威状态(会话/未读)前
// 不把 SSE 事件直接用于震动/未读增量,避免竞态导致“闪一下又消失”
private bufferedChatMessages: Array<(WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean }> = [];
private bufferedReadReceipts: Array<WSReadMessage | WSGroupReadMessage> = [];
// 用于覆盖“初始化完成但仍在刷新服务器权威状态”的过渡窗口
private isBootstrapping: boolean = false;
constructor(deps?: MessageManagerConversationListDeps) {
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();
this.state = {
conversations: new Map(),
conversationList: [],
messagesMap: new Map(),
totalUnreadCount: 0,
systemUnreadCount: 0,
isSSEConnected: false,
currentConversationId: null,
isLoadingConversations: false,
loadingMessagesSet: new Set(),
isInitialized: false,
subscribers: new Set(),
typingUsersMap: new Map(),
mutedStatusMap: new Map(),
};
// 监听认证状态变化
this.initAuthListener();
}
// ==================== 私有工具方法 ====================
private initAuthListener() {
// 获取当前用户ID
const authState = useAuthStore.getState();
this.currentUserId = authState.currentUser?.id || null;
// 如果已登录,自动初始化
if (this.currentUserId && !this.state.isInitialized) {
this.initialize();
}
// 监听登录态变化,修复冷启动时 currentUser 延迟就绪导致未初始化的问题
if (!this.authUnsubscribe) {
this.authUnsubscribe = useAuthStore.subscribe((state) => {
const nextUserId = state.currentUser?.id || null;
const prevUserId = this.currentUserId;
this.currentUserId = nextUserId;
if (nextUserId && !this.state.isInitialized) {
this.initialize();
}
// 冷启动时若用户信息后到,自动重激活当前会话,保证首进 Chat 不丢首次拉取
if (nextUserId && this.state.currentConversationId) {
this.activateConversation(this.state.currentConversationId, { forceSync: true }).catch(error => {
console.error('[MessageManager] 登录态就绪后重激活会话失败:', error);
});
}
// 退出登录时清理内存状态,避免脏数据残留
if (!nextUserId && prevUserId && this.state.isInitialized) {
this.destroy();
}
});
}
}
private getCurrentUserId(): string | null {
if (!this.currentUserId) {
this.currentUserId = useAuthStore.getState().currentUser?.id || null;
}
return this.currentUserId;
}
private normalizeConversationId(conversationId: string | number | null | undefined): string {
return conversationId == null ? '' : String(conversationId);
}
private notifySubscribers(event: MessageEvent) {
this.state.subscribers.forEach(subscriber => {
try {
subscriber(event);
} catch (error) {
console.error('[MessageManager] 订阅者执行失败:', error);
}
});
}
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
if (!this.state.currentConversationId) return false;
if (!forceRefresh) return false;
// 列表域后台刷新来源:聊天活跃期间一律延后执行
return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh';
}
async requestConversationListRefresh(
source: string,
options?: { force?: boolean; allowDefer?: boolean }
): Promise<void> {
const forceRefresh = options?.force ?? true;
const allowDefer = options?.allowDefer ?? true;
if (allowDefer && this.shouldDeferConversationRefresh(source, forceRefresh)) {
this.deferredConversationRefresh = true;
if (__DEV__) {
console.log('[MessageManager] defer conversation refresh request', {
source,
activeConversationId: this.state.currentConversationId,
});
}
return;
}
await this.fetchConversations(forceRefresh, source);
}
/**
* 按 message_id 合并消息并按 seq 排序。
* 后者覆盖前者,可用来吸收更完整的同 ID 消息体(例如服务端回包/SSE
*/
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);
}
/**
* 历史消息合并优化:
* - 常见场景incoming 全部比 existing 更旧,直接前插,避免全量排序
* - 异常/重叠场景:回退到通用 mergeMessagesById
*/
private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
if (incoming.length === 0) return existing;
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;
// 纯历史前插快路径:避免全量 merge + sort
if (incomingMaxSeq < existingMinSeq) {
return [...incomingAsc, ...existing];
}
// 兜底:存在重叠/乱序时走通用路径
return this.mergeMessagesById(existing, incomingAsc);
}
private updateConversationList() {
// 会话排序:置顶优先,再按最后消息时间排序
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;
}
/**
* 当已读游标已追上 last_seq 时,强制未读为 0修复本地 SQLite 中 unread_count 与已读状态不一致)
*/
private normalizeConversationUnreadFromReadCursor(conv: ConversationResponse): ConversationResponse {
const lastSeq = Number(conv.last_seq || 0);
const myRead = Number(conv.my_last_read_seq ?? 0);
if (lastSeq > 0 && myRead >= lastSeq && (conv.unread_count || 0) > 0) {
return { ...conv, unread_count: 0 };
}
return conv;
}
/**
* 将游标接口返回的单条会话写入 Map统一 string id、尊重 pendingReadMap 已读保护)
*/
private applyConversationFromFetch(conv: ConversationResponse): void {
const id = this.normalizeConversationId(conv.id);
const readRecord = this.pendingReadMap.get(id);
let next: ConversationResponse;
if (readRecord) {
const shouldPreserveLocalRead = (conv.unread_count || 0) > 0;
next = shouldPreserveLocalRead
? { ...conv, id, unread_count: 0 }
: { ...conv, id };
} else {
next = { ...conv, id };
}
next = this.normalizeConversationUnreadFromReadCursor(next);
this.state.conversations.set(id, next);
}
/** 与会话列表同步写入本地缓存(参与者 / 群 / 列表) */
private persistConversationListCache(): void {
const list = Array.from(this.state.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('[MessageManager] 持久化会话列表失败:', error);
});
}
private recomputeConversationTotalUnread(): void {
this.state.totalUnreadCount = Array.from(this.state.conversations.values()).reduce(
(sum, conv) => sum + (conv.unread_count || 0),
0
);
}
private emitConversationListAndUnreadUpdates(): void {
this.persistConversationListCache();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
}
/** 通过本地分页源灌入内存(暖机 / 网络失败回退) */
private async hydrateConversationsFromLocalSource(): Promise<boolean> {
this.localConversationListSource.restart();
const page = await this.localConversationListSource.loadNext();
if (!page.items.length) {
return false;
}
this.state.conversations.clear();
page.items.forEach(conv => this.applyConversationFromFetch(conv));
this.updateConversationList();
this.recomputeConversationTotalUnread();
return true;
}
// ==================== 初始化与销毁 ====================
async initialize(): Promise<void> {
if (this.state.isInitialized) {
return;
}
if (this.initializePromise) {
await this.initializePromise;
return;
}
this.initializePromise = (async () => {
try {
// 1. 获取当前用户ID
this.currentUserId = this.getCurrentUserId();
if (!this.currentUserId) {
console.warn('[MessageManager] 用户未登录,延迟初始化');
return;
}
this.isBootstrapping = true;
// 2. 初始化SSE监听
this.initSSEListeners();
// 3. 加载会话列表
await this.fetchConversations(false, 'initialize');
// 4. 加载未读数
await this.fetchUnreadCount();
this.state.isInitialized = true;
// 初始化窗口期内缓冲的 SSE 事件:刷新为服务器权威,避免“未读闪一下又消失”
await this.flushBufferedSSEEvents();
this.isBootstrapping = false;
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
// 若初始化完成时已有活动会话(用户已在 Chat 页面),立即补齐该会话同步
if (this.state.currentConversationId) {
await this.fetchMessages(this.state.currentConversationId);
}
} catch (error) {
console.error('[MessageManager] 初始化失败:', error);
this.isBootstrapping = false;
this.notifySubscribers({
type: 'error',
payload: { error, context: 'initialize' },
timestamp: Date.now(),
});
}
})();
try {
await this.initializePromise;
} finally {
this.initializePromise = null;
}
}
/**
* 初始化完成后,处理在初始化阶段被缓冲的 SSE 事件。
*
* 架构目标:
* 1) 初始化阶段不把 SSE 用于未读增量/震动(避免竞态“先加后清”)
* 2) 初始化完成后,用服务器权威状态刷新会话/未读,再把事件用于消息内容展示
*/
private async flushBufferedSSEEvents(): Promise<void> {
// 为了覆盖“刷新服务器权威状态过程中又到了一些 SSE”这种窗口这里做一个有上限的循环清空缓冲。
// 如果在线流持续不断,会产生更多轮次;因此使用上限避免长时间阻塞初始化。
const MAX_FLUSH_ITERATIONS = 3;
for (let i = 0; i < MAX_FLUSH_ITERATIONS; i++) {
const hasBuffered = this.bufferedChatMessages.length > 0 || this.bufferedReadReceipts.length > 0;
if (!hasBuffered) return;
const chatEvents = this.bufferedChatMessages;
const readEvents = this.bufferedReadReceipts;
this.bufferedChatMessages = [];
this.bufferedReadReceipts = [];
// 先把消息内容灌进内存(用于 ChatScreen 如果正好激活能立刻看到)
// 不在这里修改 unread_count / 不触发震动 / 不做自动标记已读。
for (const m of chatEvents) {
await this.handleNewMessage(m, {
suppressUnreadIncrement: true,
suppressVibration: true,
suppressConversationUpdates: true,
suppressAutoMarkAsRead: true,
});
}
// 已读回执目前在本仓库实现为空handleReadReceipt / handleGroupReadReceipt 是 no-op保留扩展位
for (const r of readEvents) {
if (r.type === 'read') {
this.handleReadReceipt(r);
} else {
this.handleGroupReadReceipt(r as WSGroupReadMessage);
}
}
// 使用服务器权威刷新会话与未读,避免初始化窗口期内漏算/重复算
await this.fetchConversations(true, 'sse-buffer-flush');
await this.fetchUnreadCount();
}
}
destroy(): void {
if (this.sseUnsubscribe) {
this.sseUnsubscribe();
this.sseUnsubscribe = null;
}
this.state.subscribers.clear();
this.state.isInitialized = false;
this.initializePromise = null;
this.activatingConversationTasks.clear();
this.remoteConversationListSource.restart();
this.localConversationListSource.restart();
this.loadingMoreConversations = false;
}
// ==================== SSE 处理 ====================
private initSSEListeners(): void {
if (this.sseUnsubscribe) {
this.sseUnsubscribe();
}
// 监听私聊消息
sseService.on('chat', (message: WSChatMessage) => {
if (!this.state.isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message);
return;
}
this.handleNewMessage(message);
});
// 监听群聊消息
sseService.on('group_message', (message: WSGroupChatMessage) => {
if (!this.state.isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message);
return;
}
this.handleNewMessage(message);
});
// 监听私聊已读回执
sseService.on('read', (message: WSReadMessage) => {
if (!this.state.isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message);
return;
}
this.handleReadReceipt(message);
});
// 监听群聊已读回执
sseService.on('group_read', (message: WSGroupReadMessage) => {
if (!this.state.isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message);
return;
}
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
sseService.on('recall', (message: WSRecallMessage) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
sseService.on('group_recall', (message: WSGroupRecallMessage) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
sseService.on('group_typing', (message: WSGroupTypingMessage) => {
this.handleGroupTyping(message);
});
// 监听群通知
sseService.on('group_notice', (message: WSGroupNoticeMessage) => {
this.handleGroupNotice(message);
});
// 监听连接状态
sseService.onConnect(() => {
this.state.isSSEConnected = true;
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: true },
timestamp: Date.now(),
});
// 冷启动/重连兜底:连接恢复后同步会话和当前会话消息,避免首屏空数据
const now = Date.now();
if (now - this.lastReconnectSyncAt > 1500) {
this.lastReconnectSyncAt = now;
// 聊天页活跃时不强刷会话列表,避免触发不必要的 UI 抖动/重算
if (!this.state.currentConversationId) {
this.requestConversationListRefresh('sse-reconnect', { force: true, allowDefer: true }).catch(error => {
console.error('[MessageManager] 连接后同步会话失败:', error);
});
} else {
this.fetchUnreadCount().catch(error => {
console.error('[MessageManager] 连接后同步未读数失败:', error);
});
}
if (this.state.currentConversationId) {
this.fetchMessages(this.state.currentConversationId).catch(error => {
console.error('[MessageManager] 连接后同步当前会话消息失败:', error);
});
}
}
});
sseService.onDisconnect(() => {
this.state.isSSEConnected = false;
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: false },
timestamp: Date.now(),
});
});
}
// ==================== 消息处理核心方法 ====================
// 已处理消息ID集合用于去重防止ACK消息和正常消息重复处理
private processedMessageIds: Set<string> = new Set();
private processedMessageIdsExpiry: Map<string, number> = new Map();
/**
* 清理过期的消息ID防止内存泄漏
*/
private cleanupProcessedMessageIds(): void {
const now = Date.now();
const expiryTime = 5 * 60 * 1000; // 5分钟过期
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
if (now - timestamp > expiryTime) {
this.processedMessageIds.delete(id);
this.processedMessageIdsExpiry.delete(id);
}
}
}
/**
* 检查消息是否已处理
*/
private isMessageProcessed(messageId: string): boolean {
return this.processedMessageIds.has(messageId);
}
/**
* 标记消息已处理
*/
private markMessageAsProcessed(messageId: string): void {
this.processedMessageIds.add(messageId);
this.processedMessageIdsExpiry.set(messageId, Date.now());
// 定期清理
if (this.processedMessageIds.size > 1000) {
this.cleanupProcessedMessageIds();
}
}
// 正在获取用户信息的请求映射(用于去重)
private pendingUserRequests: Map<string, Promise<UserDTO | null>> = new Map();
/**
* 根据群通知类型构建系统提示文案
*/
private buildGroupNoticeText(noticeType: GroupNoticeType, data: any): string {
const username = data?.username || '用户';
switch (noticeType) {
case 'member_join':
return `"${username}" 加入了群聊`;
case 'member_leave':
return `"${username}" 退出了群聊`;
case 'member_removed':
return `"${username}" 被移出群聊`;
case 'muted':
return `"${username}" 已被管理员禁言`;
case 'unmuted':
return `"${username}" 已被管理员解除禁言`;
default:
return '';
}
}
/**
* 根据群组ID查找会话ID
*/
private findConversationIdByGroupId(groupId: string): string | null {
for (const conv of this.state.conversationList) {
if (String(conv.group?.id || '') === groupId) {
return conv.id;
}
}
return null;
}
/**
* 批量异步填充消息的 sender 信息,填充完成后更新内存并通知订阅者
*/
private enrichMessagesWithSenderInfo(conversationId: string, messages: MessageResponse[]): void {
const currentUserId = this.getCurrentUserId();
// 收集所有需要查询的唯一 sender_id排除当前用户
const senderIds = [...new Set(
messages
.filter(m => m.sender_id && m.sender_id !== currentUserId && m.sender_id !== '10000' && !m.sender)
.map(m => m.sender_id)
)];
if (senderIds.length === 0) return;
// 并发获取所有 sender 信息
Promise.all(senderIds.map(id => this.getSenderInfo(id).then(user => ({ id, user }))))
.then(results => {
const senderMap = new Map<string, UserDTO>();
results.forEach(({ id, user }) => {
if (user) senderMap.set(id, user);
});
if (senderMap.size === 0) return;
const current = this.state.messagesMap.get(conversationId);
if (!current) return;
let changed = false;
const updated = current.map(m => {
if (!m.sender && m.sender_id && senderMap.has(m.sender_id)) {
changed = true;
return { ...m, sender: senderMap.get(m.sender_id) };
}
return m;
});
if (!changed) return;
this.state.messagesMap.set(conversationId, updated);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: [...updated] },
timestamp: Date.now(),
});
})
.catch(error => {
console.error('[MessageManager] 批量获取 sender 信息失败:', error);
});
}
/**
* 获取用户信息(带缓存和去重)
*/
private async getSenderInfo(userId: string): Promise<UserDTO | null> {
// 1. 先检查本地缓存
const cachedUser = await getUserCache(userId);
if (cachedUser) {
return cachedUser;
}
// 2. 检查是否已有正在进行的请求
const pendingRequest = this.pendingUserRequests.get(userId);
if (pendingRequest) {
return pendingRequest;
}
// 3. 发起新请求
const request = this.fetchUserInfo(userId);
this.pendingUserRequests.set(userId, request);
try {
const user = await request;
return user;
} finally {
// 请求完成后清理
this.pendingUserRequests.delete(userId);
}
}
/**
* 从服务器获取用户信息
*/
private async fetchUserInfo(userId: string): Promise<UserDTO | null> {
try {
const response = await api.get<UserDTO>(`/users/${userId}`);
if (response.code === 0 && response.data) {
// 缓存到本地数据库
await saveUserCache(response.data);
return response.data;
}
return null;
} catch (error) {
console.error(`[MessageManager] 获取用户信息失败: ${userId}`, error);
return null;
}
}
/**
* 处理新消息SSE推送
* 这是核心方法,必须立即处理并同步到所有订阅者
*/
private async handleNewMessage(
message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean },
options?: {
suppressUnreadIncrement?: boolean;
suppressVibration?: boolean;
suppressConversationUpdates?: boolean;
suppressAutoMarkAsRead?: boolean;
}
): Promise<void> {
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
const currentUserId = this.getCurrentUserId();
const suppressUnreadIncrement = options?.suppressUnreadIncrement ?? false;
const suppressVibration = options?.suppressVibration ?? false;
const suppressConversationUpdates = options?.suppressConversationUpdates ?? false;
const suppressAutoMarkAsRead = options?.suppressAutoMarkAsRead ?? false;
// 1. 消息去重检查 - 防止ACK消息和正常消息重复处理
if (this.isMessageProcessed(id)) {
return;
}
this.markMessageAsProcessed(id);
// 2. 对于群聊消息,获取发送者信息(如果不是当前用户发送的)
if (message.type === 'group_message' && sender_id && sender_id !== currentUserId && sender_id !== '10000') {
// 异步获取发送者信息,不阻塞消息显示
this.getSenderInfo(sender_id).then(user => {
if (user) {
// 更新消息对象中的发送者信息
const messages = this.state.messagesMap.get(normalizedConversationId);
if (messages) {
// 创建新的数组和对象确保React能检测到变化
const updatedMessages = messages.map(m =>
m.id === id ? { ...m, sender: user } : m
);
this.state.messagesMap.set(normalizedConversationId, updatedMessages);
// 通知订阅者消息已更新
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: normalizedConversationId,
messages: [...updatedMessages], // 创建新数组确保React检测到变化
},
timestamp: Date.now(),
});
}
}
}).catch(error => {
console.error('[MessageManager][ERROR] 获取发送者信息失败:', { userId: sender_id, error });
});
}
// 3. 构造消息对象
const newMessage: MessageResponse = {
id,
conversation_id: normalizedConversationId,
sender_id,
seq,
segments: segments || [],
created_at,
status: 'normal',
};
// 2. 立即更新内存中的消息列表关键确保ChatScreen能立即看到
const existingMessages = this.state.messagesMap.get(normalizedConversationId) || [];
const messageExists = existingMessages.some(m => String(m.id) === String(id));
if (!messageExists) {
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]);
this.state.messagesMap.set(normalizedConversationId, updatedMessages);
// 3. 立即通知订阅者(关键:解决竞态条件)
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: normalizedConversationId,
messages: updatedMessages,
newMessage,
},
timestamp: Date.now(),
});
}
// 4. 更新会话信息(初始化缓冲刷新阶段可选择跳过,避免会话未读/排序竞态)
if (!suppressConversationUpdates) {
this.updateConversationWithNewMessage(normalizedConversationId, newMessage, created_at);
}
// 5. 更新未读数(如果不是当前用户发送且不是当前活动会话)
const isCurrentUserMessage = sender_id === currentUserId;
const isActiveConversation = normalizedConversationId === this.state.currentConversationId;
const shouldIncrementUnread =
!suppressUnreadIncrement &&
!isCurrentUserMessage &&
!isActiveConversation &&
!!currentUserId &&
!_isAck;
if (shouldIncrementUnread) {
if (!suppressVibration) {
const vibrationType = message.type === 'group_message' ? 'group_message' : 'chat';
vibrateOnMessage(vibrationType).catch(() => {});
}
this.incrementUnreadCount(normalizedConversationId);
}
// 6. 如果是当前活动会话,自动标记已读
if (isActiveConversation && !isCurrentUserMessage && !suppressAutoMarkAsRead) {
this.markAsRead(normalizedConversationId, seq);
}
// 7. 异步保存到本地数据库不阻塞UI更新
const textContent = segments?.filter((s: any) => s.type === 'text').map((s: any) => s.data?.text || '').join('') || '';
saveMessage({
id,
conversationId: normalizedConversationId,
senderId: sender_id || '',
content: textContent,
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
isRead: isActiveConversation, // 如果是当前活动会话,标记为已读
createdAt: new Date(created_at).toISOString(),
seq,
status: 'normal',
segments,
}).catch(error => {
console.error('[MessageManager] 保存消息到本地失败:', error);
});
// 8. 通知收到新消息
this.notifySubscribers({
type: 'message_received',
payload: {
conversationId: conversation_id,
message: newMessage,
isCurrentUser: isCurrentUserMessage,
},
timestamp: Date.now(),
});
}
/**
* 更新会话的最后消息信息
*/
private updateConversationWithNewMessage(
conversationId: string,
message: MessageResponse,
createdAt: string
): void {
const conversation = this.state.conversations.get(conversationId);
if (conversation) {
// 更新现有会话
const updatedConv: ConversationResponse = {
...conversation,
last_seq: message.seq,
last_message: message,
last_message_at: createdAt,
updated_at: createdAt,
};
this.state.conversations.set(conversationId, updatedConv);
} else {
// 新会话,需要获取详情
// 异步获取会话详情
this.fetchConversationDetail(conversationId);
}
// 更新会话列表排序
this.updateConversationList();
// 通知会话列表更新
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 处理私聊已读回执
*/
private handleReadReceipt(message: WSReadMessage): void {
// 可以在这里处理对方已读的状态更新
}
/**
* 处理群聊已读回执
*/
private handleGroupReadReceipt(message: WSGroupReadMessage): void {
}
/**
* 将指定消息标记为已撤回(保留占位,不删除)
*/
private markMessageAsRecalled(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const messages = this.state.messagesMap.get(normalizedConversationId);
if (!messages) {
return;
}
let changed = false;
const updatedMessages: MessageResponse[] = messages.map((m): MessageResponse => {
if (String(m.id) !== String(messageId) || m.status === 'recalled') {
return m;
}
changed = true;
return {
...m,
status: 'recalled' as MessageResponse['status'],
segments: [],
};
});
if (!changed) {
return;
}
this.state.messagesMap.set(normalizedConversationId, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: normalizedConversationId, messages: updatedMessages },
timestamp: Date.now(),
});
}
/**
* 如果撤回的是会话最后一条消息,同步会话列表中的 last_message 状态
*/
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const conversation = this.state.conversations.get(normalizedConversationId);
if (!conversation?.last_message) {
return;
}
if (String(conversation.last_message.id) !== String(messageId)) {
return;
}
if (conversation.last_message.status === 'recalled') {
return;
}
const updatedConversation: ConversationResponse = {
...conversation,
last_message: {
...conversation.last_message,
status: 'recalled',
},
};
this.state.conversations.set(normalizedConversationId, updatedConversation);
this.updateConversationList();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 处理私聊消息撤回
*/
private handleRecallMessage(message: WSRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id },
timestamp: Date.now(),
});
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}
/**
* 处理群聊消息撤回
*/
private handleGroupRecallMessage(message: WSGroupRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
timestamp: Date.now(),
});
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}
/**
* 处理群聊输入状态
*/
private handleGroupTyping(message: WSGroupTypingMessage): void {
const { group_id, user_id, is_typing } = message;
const groupIdStr = String(group_id);
const currentTypingUsers = this.state.typingUsersMap.get(groupIdStr) || [];
let updatedTypingUsers: string[];
if (is_typing) {
// 添加用户到输入列表
if (!currentTypingUsers.includes(user_id)) {
updatedTypingUsers = [...currentTypingUsers, user_id];
} else {
updatedTypingUsers = currentTypingUsers;
}
} else {
// 从输入列表移除用户
updatedTypingUsers = currentTypingUsers.filter(id => id !== user_id);
}
// 只有变化时才更新
if (updatedTypingUsers.length !== currentTypingUsers.length) {
this.state.typingUsersMap.set(groupIdStr, updatedTypingUsers);
this.notifySubscribers({
type: 'typing_status',
payload: { groupId: groupIdStr, typingUsers: updatedTypingUsers },
timestamp: Date.now(),
});
}
}
/**
* 处理群通知
*/
private handleGroupNotice(message: WSGroupNoticeMessage): void {
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
const groupIdStr = String(group_id);
// 处理禁言/解除禁言通知 - 更新本地状态
if (notice_type === 'muted' || notice_type === 'unmuted') {
const mutedUserId = data?.user_id;
const currentUserId = this.getCurrentUserId();
// 如果是当前用户被禁言/解除禁言,更新状态
if (mutedUserId && mutedUserId === currentUserId) {
const isMuted = notice_type === 'muted';
this.state.mutedStatusMap.set(groupIdStr, isMuted);
}
}
// 对于携带 message_id/seq 的群通知,补一条系统消息到当前会话消息流
// 这样 UI 会按系统通知样式渲染,而不是当作普通用户消息。
if (message_id && typeof seq === 'number' && seq > 0) {
const conversationId = this.findConversationIdByGroupId(groupIdStr);
if (conversationId) {
const existingMessages = this.state.messagesMap.get(conversationId) || [];
const exists = existingMessages.some(m => String(m.id) === String(message_id));
if (!exists) {
const noticeText = this.buildGroupNoticeText(notice_type, data);
const systemNoticeMessage: MessageResponse = {
id: String(message_id),
conversation_id: conversationId,
sender_id: '10000',
seq,
segments: noticeText
? [{ type: 'text', data: { text: noticeText } as any }]
: [],
status: 'normal',
category: 'notification',
created_at: new Date(timestamp || Date.now()).toISOString(),
};
const updatedMessages = [...existingMessages, systemNoticeMessage].sort((a, b) => a.seq - b.seq);
this.state.messagesMap.set(conversationId, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: updatedMessages,
newMessage: systemNoticeMessage,
source: 'group_notice',
},
timestamp: Date.now(),
});
}
}
}
// 通知订阅者群通知
this.notifySubscribers({
type: 'group_notice',
payload: {
groupId: groupIdStr,
noticeType: notice_type,
data,
timestamp,
messageId: message_id,
seq,
},
timestamp: Date.now(),
});
}
// ==================== 数据获取方法 ====================
/**
* 获取会话列表(首屏/下拉刷新:优先网络游标;可回退 SQLite对 UI 透明)
*/
async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> {
if (this.state.isLoadingConversations && !forceRefresh) {
return;
}
if (this.shouldDeferConversationRefresh(source, forceRefresh)) {
this.deferredConversationRefresh = true;
if (__DEV__) {
console.log('[MessageManager] defer fetchConversations', {
source,
activeConversationId: this.state.currentConversationId,
});
}
return;
}
if (__DEV__) {
console.log('[MessageManager] fetchConversations', {
source,
forceRefresh,
activeConversationId: this.state.currentConversationId,
});
}
// 非强制刷新且内存为空:先用本地源暖机,便于离线/弱网先展示列表
if (!forceRefresh && this.state.conversations.size === 0) {
const warmed = await this.hydrateConversationsFromLocalSource();
if (warmed) {
this.emitConversationListAndUnreadUpdates();
}
}
this.state.isLoadingConversations = true;
const emitLoadingToUi = this.state.conversationList.length === 0;
if (emitLoadingToUi) {
this.notifySubscribers({
type: 'conversations_loading',
payload: { loading: true },
timestamp: Date.now(),
});
}
try {
this.remoteConversationListSource.restart();
const page = await this.remoteConversationListSource.loadNext();
this.state.conversations.clear();
page.items.forEach(conv => this.applyConversationFromFetch(conv));
this.updateConversationList();
this.recomputeConversationTotalUnread();
if (this.pendingReadMap.size > 0) {
for (const convId of this.pendingReadMap.keys()) {
updateConversationCacheUnreadCount(convId, 0).catch(error => {
console.error('[MessageManager] 修复本地缓存未读数失败:', convId, error);
});
}
}
this.emitConversationListAndUnreadUpdates();
} catch (error) {
console.error('[MessageManager] 获取会话列表失败:', error);
if (this.state.conversationList.length === 0) {
const recovered = await this.hydrateConversationsFromLocalSource();
if (recovered) {
this.emitConversationListAndUnreadUpdates();
}
}
this.notifySubscribers({
type: 'error',
payload: { error, context: 'fetchConversations' },
timestamp: Date.now(),
});
} finally {
this.state.isLoadingConversations = false;
if (emitLoadingToUi) {
this.notifySubscribers({
type: 'conversations_loading',
payload: { loading: false },
timestamp: Date.now(),
});
}
}
}
/**
* 会话列表加载下一页(游标追加,不清空已有会话)
*/
async loadMoreConversations(): Promise<void> {
if (!this.remoteConversationListSource.hasMore) {
return;
}
if (this.loadingMoreConversations || this.state.isLoadingConversations) {
return;
}
this.loadingMoreConversations = true;
try {
const page = await this.remoteConversationListSource.loadNext();
page.items.forEach(conv => this.applyConversationFromFetch(conv));
this.updateConversationList();
this.recomputeConversationTotalUnread();
this.emitConversationListAndUnreadUpdates();
} catch (error) {
console.error('[MessageManager] 加载更多会话失败:', error);
this.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) {
// 若此会话有进行中的已读请求或处于保护期内,保留 unread_count=0
const readRecord = this.pendingReadMap.get(normalizedId);
const isPending = !!readRecord;
// 使用智能合并:如果服务器返回 unread_count > 0 但本地有更晚的已读记录,保留本地状态
const shouldPreserveLocalRead = isPending && (detail.unread_count || 0) > 0;
const safeDetail = this.normalizeConversationUnreadFromReadCursor(
shouldPreserveLocalRead
? { ...(detail as ConversationResponse), id: normalizedId, unread_count: 0 }
: { ...(detail as ConversationResponse), id: normalizedId }
);
this.state.conversations.set(normalizedId, safeDetail);
this.updateConversationList();
// getConversationById 内部会调用 saveConversationCache 写入服务器旧数据,
// 若有 pending 已读请求或处于保护期内,需要修复本地缓存
if (isPending) {
updateConversationCacheUnreadCount(normalizedId, 0).catch(error => {
console.error('[MessageManager] 修复本地缓存未读数失败:', normalizedId, error);
});
}
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
return detail as ConversationResponse;
} catch (error) {
console.error('[MessageManager] 获取会话详情失败:', error);
return null;
}
}
/**
* 获取会话消息(增量同步)
* 关键方法:确保消息立即显示,解决竞态条件
*/
async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> {
// 防止重复加载
if (this.state.loadingMessagesSet.has(conversationId)) {
return;
}
this.state.loadingMessagesSet.add(conversationId);
try {
const mergeMessages = (base: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] => {
return this.mergeMessagesById(base, incoming);
};
const existingMessagesAtStart = this.state.messagesMap.get(conversationId) || [];
const hasInMemoryMessages = existingMessagesAtStart.length > 0;
let baselineMaxSeq = hasInMemoryMessages
? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0)
: 0;
// 1. 先从本地数据库加载(确保立即有数据展示)
if (!afterSeq) {
let localMessages: any[] = [];
let localMaxSeq = 0;
if (!hasInMemoryMessages) {
try {
localMessages = await getMessagesByConversation(conversationId, 20);
localMaxSeq = await getMaxSeq(conversationId);
baselineMaxSeq = localMaxSeq;
} catch (error) {
// 架构原则:本地存储异常不阻断在线同步
console.warn('[MessageManager] 读取本地消息失败,回退到服务端同步:', error);
}
}
if (!hasInMemoryMessages && 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.state.messagesMap.set(conversationId, formattedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: formattedMessages,
source: 'local',
},
timestamp: Date.now(),
});
// 异步填充 sender 信息
this.enrichMessagesWithSenderInfo(conversationId, formattedMessages);
} else if (!hasInMemoryMessages) {
// 冷启动兜底:先下发空列表事件,避免 ChatScreen 长时间停留在 loading
this.state.messagesMap.set(conversationId, []);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: [],
source: 'local_empty',
},
timestamp: Date.now(),
});
// 再直接向服务端拉一页最新消息,不依赖 detail.last_seq避免缓存 last_seq 过旧导致首屏空白
try {
const latestResponse = await messageService.getMessages(conversationId, undefined, undefined, 20);
const latestMessages = latestResponse?.messages || [];
if (latestMessages.length > 0) {
const sortedLatestMessages = [...latestMessages].sort((a, b) => a.seq - b.seq);
this.state.messagesMap.set(conversationId, sortedLatestMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: sortedLatestMessages,
newMessages: sortedLatestMessages,
source: 'server_bootstrap',
},
timestamp: Date.now(),
});
this.enrichMessagesWithSenderInfo(conversationId, sortedLatestMessages);
// 持久化是副作用,不阻断 UI
saveMessagesBatch(latestMessages.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('[MessageManager] 保存最新消息到本地失败:', error);
});
}
} catch (error) {
console.error('[MessageManager] 冷启动拉取最新消息失败:', error);
}
}
// 2. 服务端快照 + 增量同步(架构修复:不再依赖 detail.last_seq 单点判断)
try {
// 2.1 总是先拉一页最新快照,确保 Chat 首次激活有权威来源
const snapshotResp = await messageService.getMessages(conversationId, undefined, undefined, 50);
const snapshotMessages = snapshotResp?.messages || [];
if (snapshotMessages.length > 0) {
const existingMessages = this.state.messagesMap.get(conversationId) || [];
const mergedSnapshot = mergeMessages(existingMessages, snapshotMessages);
this.state.messagesMap.set(conversationId, mergedSnapshot);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedSnapshot,
newMessages: snapshotMessages,
source: 'server_snapshot',
},
timestamp: Date.now(),
});
this.enrichMessagesWithSenderInfo(conversationId, mergedSnapshot);
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('[MessageManager] 保存快照消息到本地失败:', error);
});
}
// 2.2 再基于 localMaxSeq 做增量补齐(防止快照窗口不足导致漏更老的新消息)
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.state.messagesMap.get(conversationId) || [];
const mergedMessages = mergeMessages(existingMessages, newMessages);
this.state.messagesMap.set(conversationId, mergedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedMessages,
newMessages,
source: 'server_incremental',
},
timestamp: Date.now(),
});
this.enrichMessagesWithSenderInfo(conversationId, mergedMessages);
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('[MessageManager] 保存增量消息到本地失败:', error);
});
}
}
} catch (error) {
console.error('[MessageManager] 快照/增量同步失败:', error);
}
} else {
// 指定了afterSeq直接请求服务端
const response = await messageService.getMessages(conversationId, afterSeq);
if (response?.messages && response.messages.length > 0) {
const newMessages = response.messages;
// 合并消息
const existingMessages = this.state.messagesMap.get(conversationId) || [];
const mergedMessages = mergeMessages(existingMessages, newMessages);
this.state.messagesMap.set(conversationId, mergedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedMessages,
newMessages,
source: 'server',
},
timestamp: Date.now(),
});
// 异步填充 sender 信息
this.enrichMessagesWithSenderInfo(conversationId, mergedMessages);
// 持久化是副作用,不阻断 UI
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('[MessageManager] 保存 afterSeq 消息到本地失败:', error);
});
}
}
} catch (error) {
console.error('[MessageManager] 获取消息失败:', error);
this.notifySubscribers({
type: 'error',
payload: { error, context: 'fetchMessages', conversationId },
timestamp: Date.now(),
});
} finally {
this.state.loadingMessagesSet.delete(conversationId);
}
}
/**
* 加载更多历史消息
*/
async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
try {
// 先从本地获取
const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit);
if (localMessages.length >= limit) {
// 本地有足够数据
// 本地查询是 seq DESC这里转成 ASC匹配渲染时间序
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.state.messagesMap.get(conversationId) || [];
const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages);
this.state.messagesMap.set(conversationId, mergedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'local_history' },
timestamp: Date.now(),
});
// 性能优化:历史分页仅对新批次做 sender 补全,避免每次扫描全量消息
this.enrichMessagesWithSenderInfo(conversationId, formattedMessages);
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.state.messagesMap.get(conversationId) || [];
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
this.state.messagesMap.set(conversationId, mergedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'server_history' },
timestamp: Date.now(),
});
// 性能优化:仅对本次服务端批次做 sender 补全
this.enrichMessagesWithSenderInfo(conversationId, serverMessages);
return serverMessages;
}
return [];
} catch (error) {
console.error('[MessageManager] 加载更多消息失败:', error);
return [];
}
}
/**
* 获取未读数
*/
async fetchUnreadCount(): Promise<void> {
try {
const [unreadData, systemUnreadData] = await Promise.all([
messageService.getUnreadCount(),
messageService.getSystemUnreadCount(),
]);
this.state.totalUnreadCount = unreadData?.total_unread_count ?? 0;
this.state.systemUnreadCount = systemUnreadData?.unread_count ?? 0;
// 服务端汇总未读为 0 时,清掉内存/本地缓存中残留的逐会话红点(常见于仅暖机了 SQLite 旧数据)
if (this.state.totalUnreadCount === 0 && this.state.conversations.size > 0) {
let anyCleared = false;
for (const [cid, conv] of this.state.conversations) {
if ((conv.unread_count || 0) > 0) {
this.state.conversations.set(cid, { ...conv, unread_count: 0 });
anyCleared = true;
}
}
if (anyCleared) {
this.updateConversationList();
this.persistConversationListCache();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
}
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
} catch (error) {
console.error('[MessageManager] 获取未读数失败:', error);
}
}
// ==================== 数据操作方法 ====================
/**
* 发送消息
*/
async sendMessage(
conversationId: string,
segments: MessageSegment[],
options?: { replyToId?: string }
): Promise<MessageResponse | null> {
try {
// 调用API发送
const response = await messageService.sendMessage(conversationId, {
segments,
reply_to_id: options?.replyToId,
});
if (response) {
// 添加到本地消息列表
const existingMessages = this.state.messagesMap.get(conversationId) || [];
const newMessage: MessageResponse = {
id: response.id,
conversation_id: conversationId,
sender_id: this.getCurrentUserId() || '',
seq: response.seq,
segments,
created_at: new Date().toISOString(),
status: 'normal',
};
const updatedMessages = this.mergeMessagesById(existingMessages, [newMessage]);
this.state.messagesMap.set(conversationId, updatedMessages);
// 关键:立即广播消息列表更新,确保 ChatScreen 立刻显示新消息
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: updatedMessages,
newMessage,
source: 'send',
},
timestamp: Date.now(),
});
// 更新会话
this.updateConversationWithNewMessage(conversationId, newMessage, newMessage.created_at);
// 通知
this.notifySubscribers({
type: 'message_sent',
payload: { conversationId, message: newMessage },
timestamp: Date.now(),
});
// 保存到本地数据库
const textContent = segments?.filter((s: any) => s.type === 'text').map((s: any) => s.data?.text || '').join('') || '';
saveMessage({
id: response.id,
conversationId,
senderId: this.getCurrentUserId() || '',
content: textContent,
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
isRead: true,
createdAt: newMessage.created_at,
seq: response.seq,
status: 'normal',
segments,
}).catch(console.error);
return newMessage;
}
return null;
} catch (error) {
console.error('[MessageManager] 发送消息失败:', error);
this.notifySubscribers({
type: 'error',
payload: { error, context: 'sendMessage' },
timestamp: Date.now(),
});
return null;
}
}
/**
* 标记会话已读
* 关键方法:确保已读状态在所有组件间同步
*
* 修复:使用 pendingReadMap 和 readStateVersion 防止已读状态被旧数据覆盖
* 1. 记录已读时间戳和版本号
* 2. API 完成后延迟清除保护5秒
* 3. fetchConversations 使用版本号判断数据新旧
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
const normalizedId = this.normalizeConversationId(conversationId);
const conversation = this.state.conversations.get(normalizedId);
if (!conversation) {
console.warn('[MessageManager] 会话不存在:', normalizedId);
return;
}
const prevUnreadCount = conversation.unread_count || 0;
const existingRecord = this.pendingReadMap.get(normalizedId);
// 使用 seq 去重:同一会话若已上报到更大/相同 seq则跳过重复上报
if (existingRecord && seq <= existingRecord.lastReadSeq) {
return;
}
// 清除可能存在的旧定时器
if (existingRecord?.clearTimer) {
clearTimeout(existingRecord.clearTimer);
}
// 递增全局版本号
this.readStateVersion++;
const currentVersion = this.readStateVersion;
// 1. 标记此会话有进行中的已读请求(防止 fetchConversations 覆盖乐观状态)
// 记录时间戳和版本号,用于后续智能合并
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
});
// 2. 乐观更新本地状态立即反映到UI同步已读游标避免与 last_seq 不一致)
const updatedConv: ConversationResponse = {
...conversation,
unread_count: 0,
my_last_read_seq: Math.max(Number(conversation.my_last_read_seq || 0), seq),
};
this.state.conversations.set(normalizedId, updatedConv);
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - prevUnreadCount);
this.updateConversationList();
// 3. 更新本地数据库
markConversationAsRead(normalizedId).catch(console.error);
// 4. 立即通知所有订阅者
this.notifySubscribers({
type: 'message_read',
payload: {
conversationId: normalizedId,
unreadCount: 0,
totalUnreadCount: this.state.totalUnreadCount,
},
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
// 5. 调用 API完成后设置延迟清除保护
try {
await messageService.markAsRead(normalizedId, seq);
} catch (error) {
console.error('[MessageManager] 标记已读API失败:', error);
// 失败时回滚状态
this.state.conversations.set(normalizedId, conversation);
this.state.totalUnreadCount += prevUnreadCount;
this.updateConversationList();
this.notifySubscribers({
type: 'message_read',
payload: {
conversationId: normalizedId,
unreadCount: prevUnreadCount,
totalUnreadCount: this.state.totalUnreadCount,
},
timestamp: Date.now(),
});
// API 失败时立即清除保护,允许后续 fetch 恢复状态
this.pendingReadMap.delete(normalizedId);
return;
}
// API 成功后,设置延迟清除保护
// 这确保在 API 完成后的一段时间内fetchConversations 不会用服务器旧数据覆盖已读状态
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(normalizedId);
// 只有版本号匹配时才清除(防止清除新的已读请求的保护)
if (record && record.version === currentVersion) {
this.pendingReadMap.delete(normalizedId);
}
}, READ_STATE_PROTECTION_DELAY);
// 更新记录保存定时器ID
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
clearTimer,
});
}
/**
* 标记所有消息已读
*/
async markAllAsRead(): Promise<void> {
// 乐观更新
const prevConversations = new Map(this.state.conversations);
const prevTotalUnread = this.state.totalUnreadCount;
this.state.conversations.forEach(conv => {
conv.unread_count = 0;
});
this.state.totalUnreadCount = 0;
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: 0,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
try {
// 标记系统消息已读
await messageService.markAllSystemMessagesRead();
// 标记所有会话已读
const promises = Array.from(prevConversations.values())
.filter(conv => (conv.unread_count || 0) > 0)
.map(conv => messageService.markAsRead(conv.id, conv.last_seq));
await Promise.all(promises);
} catch (error) {
console.error('[MessageManager] 标记所有已读失败:', error);
// 回滚
this.state.conversations = prevConversations;
this.state.totalUnreadCount = prevTotalUnread;
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: prevTotalUnread,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
}
}
/**
* 创建私聊会话
*/
async createConversation(userId: string): Promise<ConversationResponse | null> {
try {
const conversation = await messageService.createConversation(userId);
// 添加到会话列表
this.state.conversations.set(conversation.id, conversation);
this.updateConversationList();
// 通知更新
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
return conversation;
} catch (error) {
console.error('[MessageManager] 创建会话失败:', error);
this.notifySubscribers({
type: 'error',
payload: { error, context: 'createConversation' },
timestamp: Date.now(),
});
return null;
}
}
/**
* 本地更新会话信息
*/
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void {
const conversation = this.state.conversations.get(conversationId);
if (!conversation) {
console.warn('[MessageManager] 更新会话失败,会话不存在:', conversationId);
return;
}
const updatedConv: ConversationResponse = {
...conversation,
...updates,
};
this.state.conversations.set(conversationId, updatedConv);
this.updateConversationList();
// 通知更新
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 仅自己删除会话后的本地状态回收
*/
removeConversation(conversationId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const target = this.state.conversations.get(normalizedConversationId);
const removedUnread = target?.unread_count || 0;
this.state.conversations.delete(normalizedConversationId);
this.state.messagesMap.delete(normalizedConversationId);
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - removedUnread);
if (this.state.currentConversationId === normalizedConversationId) {
this.state.currentConversationId = null;
}
this.updateConversationList();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
deleteConversationFromDb(normalizedConversationId).catch(error => {
console.error('[MessageManager] 删除本地会话失败:', error);
});
}
/**
* 设置系统消息未读数
*/
setSystemUnreadCount(count: number): void {
this.state.systemUnreadCount = count;
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
}
/**
* 增加系统消息未读数
*/
incrementSystemUnreadCount(): void {
this.state.systemUnreadCount += 1;
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
}
/**
* 减少系统消息未读数
*/
decrementSystemUnreadCount(count = 1): void {
this.state.systemUnreadCount = Math.max(0, this.state.systemUnreadCount - count);
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
}
/**
* 更新单个会话未读数
*/
updateUnreadCount(conversationId: string, count: number): void {
const conversation = this.state.conversations.get(conversationId);
if (!conversation) {
console.warn('[MessageManager] 更新未读数失败,会话不存在:', conversationId);
return;
}
const prevUnreadCount = conversation.unread_count || 0;
const diff = count - prevUnreadCount;
// 更新会话未读数
const updatedConv: ConversationResponse = {
...conversation,
unread_count: count,
};
this.state.conversations.set(conversationId, updatedConv);
// 更新总未读数
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount + diff);
this.updateConversationList();
// 通知更新
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 清空会话列表
*/
clearConversations(): void {
this.state.conversations.clear();
this.state.conversationList = [];
this.state.totalUnreadCount = 0;
this.state.systemUnreadCount = 0;
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: [] },
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: 0,
systemUnreadCount: 0,
},
timestamp: Date.now(),
});
}
/**
* 使缓存失效(与旧版兼容的接口)
* 实际 MessageManager 使用内存状态,此方法用于兼容性
*/
invalidateCache(type: 'all' | 'list' | 'unread' = 'all'): void {
// MessageManager 不使用外部缓存,内存状态始终是最新的
// 此方法仅用于 API 兼容性
}
/**
* 增加未读数
*/
private incrementUnreadCount(conversationId: string): void {
const conversation = this.state.conversations.get(conversationId);
if (conversation) {
const prevUnreadCount = conversation.unread_count || 0;
const newUnreadCount = prevUnreadCount + 1;
// 创建新对象而不是直接修改,确保状态一致性
const updatedConv: ConversationResponse = {
...conversation,
unread_count: newUnreadCount,
};
this.state.conversations.set(conversationId, updatedConv);
this.state.totalUnreadCount += 1;
// 更新会话列表排序因为unread_count变化了
this.updateConversationList();
this.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
}
// ==================== 状态查询方法 ====================
/**
* 会话列表是否还能加载更多(由数据源响应推导,不暴露游标本身)
*/
canLoadMoreConversations(): boolean {
return this.remoteConversationListSource.hasMore;
}
/**
* 获取会话列表
*/
getConversations(): ConversationResponse[] {
return this.state.conversationList;
}
/**
* 获取指定会话
*/
getConversation(conversationId: string): ConversationResponse | null {
const normalizedConversationId = this.normalizeConversationId(conversationId);
return this.state.conversations.get(normalizedConversationId) || null;
}
/**
* 获取指定会话的消息
*/
getMessages(conversationId: string): MessageResponse[] {
const normalizedConversationId = this.normalizeConversationId(conversationId);
return this.state.messagesMap.get(normalizedConversationId) || [];
}
/**
* 获取总未读数
*/
getUnreadCount(): { total: number; system: number } {
return {
total: this.state.totalUnreadCount,
system: this.state.systemUnreadCount,
};
}
/**
* 获取SSE连接状态
*/
isConnected(): boolean {
return this.state.isSSEConnected;
}
/**
* 获取加载状态
*/
isLoading(): boolean {
return this.state.isLoadingConversations;
}
/**
* 获取会话消息加载状态
*/
isLoadingMessages(conversationId: string): boolean {
const normalizedConversationId = this.normalizeConversationId(conversationId);
return this.state.loadingMessagesSet.has(normalizedConversationId);
}
// ==================== 活动会话管理 ====================
/**
* 设置当前活动会话(进入聊天页面时调用)
*/
setActiveConversation(conversationId: string | null): void {
const normalizedConversationId = conversationId ? this.normalizeConversationId(conversationId) : null;
this.state.currentConversationId = normalizedConversationId;
if (!normalizedConversationId && this.deferredConversationRefresh) {
this.deferredConversationRefresh = false;
this.requestConversationListRefresh('deferred-after-chat', { force: true, allowDefer: false }).catch(error => {
console.error('[MessageManager] 延迟会话刷新失败:', error);
});
}
}
/**
* 激活会话(架构入口)
* 单一职责:设置活动会话 + 保证初始化 + 串行同步该会话消息
*/
async activateConversation(conversationId: string, options?: { forceSync?: boolean }): Promise<void> {
const normalizedConversationId = this.normalizeConversationId(conversationId);
if (!normalizedConversationId) return;
await this.initialize();
this.setActiveConversation(normalizedConversationId);
// 先下发当前内存快照,确保 UI 有确定性起点
const snapshot = this.getMessages(normalizedConversationId);
this.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: normalizedConversationId,
messages: [...snapshot],
source: 'activation_snapshot',
},
timestamp: Date.now(),
});
const existingTask = this.activatingConversationTasks.get(normalizedConversationId);
if (existingTask && !options?.forceSync) {
await existingTask;
return;
}
const task = (async () => {
await this.fetchMessages(normalizedConversationId);
})();
this.activatingConversationTasks.set(normalizedConversationId, task);
try {
await task;
} finally {
if (this.activatingConversationTasks.get(normalizedConversationId) === task) {
this.activatingConversationTasks.delete(normalizedConversationId);
}
}
}
/**
* 获取当前活动会话ID
*/
getActiveConversation(): string | null {
return this.state.currentConversationId;
}
// ==================== 订阅机制 ====================
/**
* 订阅消息事件
* 返回取消订阅函数
*/
subscribe(subscriber: MessageSubscriber): () => void {
this.state.subscribers.add(subscriber);
// 立即发送当前状态给新订阅者
subscriber({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
subscriber({
type: 'unread_count_updated',
payload: {
totalUnreadCount: this.state.totalUnreadCount,
systemUnreadCount: this.state.systemUnreadCount,
},
timestamp: Date.now(),
});
subscriber({
type: 'connection_changed',
payload: { connected: this.state.isSSEConnected },
timestamp: Date.now(),
});
// 【修复】如果当前有活动会话,立即发送该会话的消息给新订阅者
// 这样新订阅者可以立即获取当前消息状态,而不需要等待下一次更新
if (this.state.currentConversationId) {
const currentMessages = this.state.messagesMap.get(this.state.currentConversationId);
if (currentMessages && currentMessages.length > 0) {
subscriber({
type: 'messages_updated',
payload: {
conversationId: this.state.currentConversationId,
messages: [...currentMessages],
source: 'initial_sync',
},
timestamp: Date.now(),
});
}
}
// 返回取消订阅函数
return () => {
this.state.subscribers.delete(subscriber);
};
}
/**
* 订阅特定会话的消息更新
*/
subscribeToMessages(conversationId: string, callback: (messages: MessageResponse[]) => void): () => void {
const subscriber: MessageSubscriber = (event) => {
if (event.type === 'messages_updated' && event.payload.conversationId === conversationId) {
callback(event.payload.messages);
}
};
return this.subscribe(subscriber);
}
// ==================== 群聊相关状态查询 ====================
/**
* 获取群组的输入状态
*/
getTypingUsers(groupId: string): string[] {
return this.state.typingUsersMap.get(groupId) || [];
}
/**
* 获取群组的禁言状态
*/
isMuted(groupId: string): boolean {
return this.state.mutedStatusMap.get(groupId) || false;
}
/**
* 设置群组的禁言状态(本地)
*/
setMutedStatus(groupId: string, isMuted: boolean): void {
this.state.mutedStatusMap.set(groupId, isMuted);
}
}
// ==================== 单例导出 ====================
export const messageManager = new MessageManager();
// 默认导出
export default messageManager;