feat(Dependencies): add new libraries for enhanced functionality and update existing ones
- Added `katex`, `markdown-it`, and `prismjs` for improved rendering of mathematical expressions and markdown content. - Included `react-native-webview` to support web content within the app. - Updated package versions in `package-lock.json` and `package.json` for better compatibility and performance. - Refactored notification handling in `sseService` and `systemNotificationService` to improve user experience with vibration feedback. - Introduced buffering for SSE messages in `messageManager` to prevent race conditions during initialization.
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
saveGroupsCache,
|
||||
} from '../services/database';
|
||||
import { api } from '../services/api';
|
||||
import { vibrateOnMessage } from '../services/messageVibrationService';
|
||||
import { useAuthStore } from './authStore';
|
||||
import {
|
||||
type IConversationListPagedSource,
|
||||
@@ -186,6 +187,14 @@ class MessageManager {
|
||||
*/
|
||||
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 =
|
||||
@@ -467,6 +476,8 @@ class MessageManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isBootstrapping = true;
|
||||
|
||||
// 2. 初始化SSE监听
|
||||
this.initSSEListeners();
|
||||
|
||||
@@ -478,6 +489,11 @@ class MessageManager {
|
||||
|
||||
this.state.isInitialized = true;
|
||||
|
||||
// 初始化窗口期内缓冲的 SSE 事件:刷新为服务器权威,避免“未读闪一下又消失”
|
||||
await this.flushBufferedSSEEvents();
|
||||
|
||||
this.isBootstrapping = false;
|
||||
|
||||
this.notifySubscribers({
|
||||
type: 'conversations_updated',
|
||||
payload: { conversations: this.state.conversationList },
|
||||
@@ -490,6 +506,7 @@ class MessageManager {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MessageManager] 初始化失败:', error);
|
||||
this.isBootstrapping = false;
|
||||
this.notifySubscribers({
|
||||
type: 'error',
|
||||
payload: { error, context: 'initialize' },
|
||||
@@ -505,6 +522,52 @@ class MessageManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化完成后,处理在初始化阶段被缓冲的 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();
|
||||
@@ -529,21 +592,37 @@ class MessageManager {
|
||||
|
||||
// 监听私聊消息
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -784,10 +863,22 @@ class MessageManager {
|
||||
* 处理新消息(SSE推送)
|
||||
* 这是核心方法,必须立即处理并同步到所有订阅者
|
||||
*/
|
||||
private async handleNewMessage(message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean }): Promise<void> {
|
||||
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)) {
|
||||
@@ -855,24 +946,32 @@ class MessageManager {
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 更新会话信息
|
||||
this.updateConversationWithNewMessage(normalizedConversationId, newMessage, created_at);
|
||||
// 4. 更新会话信息(初始化缓冲刷新阶段可选择跳过,避免会话未读/排序竞态)
|
||||
if (!suppressConversationUpdates) {
|
||||
this.updateConversationWithNewMessage(normalizedConversationId, newMessage, created_at);
|
||||
}
|
||||
|
||||
// 5. 更新未读数(如果不是当前用户发送且不是当前活动会话)
|
||||
const isCurrentUserMessage = sender_id === currentUserId;
|
||||
const isActiveConversation = normalizedConversationId === this.state.currentConversationId;
|
||||
|
||||
// 确保当前用户发送的消息不会增加未读数
|
||||
// 同时确保currentUserId有效(避免undefined === undefined的情况)
|
||||
// ACK消息(发送确认)也不应该增加未读数
|
||||
const shouldIncrementUnread = !isCurrentUserMessage && !isActiveConversation && !!currentUserId && !_isAck;
|
||||
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) {
|
||||
if (isActiveConversation && !isCurrentUserMessage && !suppressAutoMarkAsRead) {
|
||||
this.markAsRead(normalizedConversationId, seq);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user