refactor(ConversationList): streamline conversation list handling and enhance unread count management
- Removed debug display for unread count in ConversationListRow to clean up UI. - Updated updateConversationCacheUnreadCount to optionally handle myLastReadSeq for better synchronization of read status. - Refactored message service to unify offset and cursor pagination methods, improving data fetching consistency. - Introduced a new method to normalize unread counts based on read cursor, ensuring accurate display of unread messages. - Consolidated conversation list source implementations to simplify remote data fetching logic.
This commit is contained in:
@@ -262,9 +262,6 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{__DEV__ && (
|
|
||||||
<Text style={{ fontSize: 10, color: '#999', marginLeft: 8 }}>[DEBUG: {item.unread_count || 0}]</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</AnimatedTouchable>
|
</AnimatedTouchable>
|
||||||
|
|||||||
@@ -942,10 +942,12 @@ export const updateConversationListCacheEntry = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话 SWR 缓存中的未读数(标记已读后立即同步清零,避免下次从缓存加载时仍显示旧红点)
|
* 更新会话 SWR 缓存中的未读数(标记已读后立即同步清零,避免下次从缓存加载时仍显示旧红点)
|
||||||
|
* @param myLastReadSeq 若传入,同时写入 my_last_read_seq,便于冷启动用 last_seq 与已读游标校正未读
|
||||||
*/
|
*/
|
||||||
export const updateConversationCacheUnreadCount = async (
|
export const updateConversationCacheUnreadCount = async (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
count: number
|
count: number,
|
||||||
|
myLastReadSeq?: number
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await enqueueWrite(async () => {
|
await enqueueWrite(async () => {
|
||||||
const database = await getDb();
|
const database = await getDb();
|
||||||
@@ -959,6 +961,9 @@ export const updateConversationCacheUnreadCount = async (
|
|||||||
const conv = safeParseJson<ConversationResponse>(listRow.data);
|
const conv = safeParseJson<ConversationResponse>(listRow.data);
|
||||||
if (conv) {
|
if (conv) {
|
||||||
conv.unread_count = count;
|
conv.unread_count = count;
|
||||||
|
if (typeof myLastReadSeq === 'number' && !Number.isNaN(myLastReadSeq)) {
|
||||||
|
conv.my_last_read_seq = myLastReadSeq;
|
||||||
|
}
|
||||||
await database.runAsync(
|
await database.runAsync(
|
||||||
`UPDATE conversation_list_cache SET data = ? WHERE id = ?`,
|
`UPDATE conversation_list_cache SET data = ? WHERE id = ?`,
|
||||||
[JSON.stringify(conv), String(conversationId)]
|
[JSON.stringify(conv), String(conversationId)]
|
||||||
@@ -975,6 +980,9 @@ export const updateConversationCacheUnreadCount = async (
|
|||||||
const conv = safeParseJson<Record<string, unknown>>(cacheRow.data);
|
const conv = safeParseJson<Record<string, unknown>>(cacheRow.data);
|
||||||
if (conv) {
|
if (conv) {
|
||||||
conv.unread_count = count;
|
conv.unread_count = count;
|
||||||
|
if (typeof myLastReadSeq === 'number' && !Number.isNaN(myLastReadSeq)) {
|
||||||
|
conv.my_last_read_seq = myLastReadSeq;
|
||||||
|
}
|
||||||
await database.runAsync(
|
await database.runAsync(
|
||||||
`UPDATE conversation_cache SET data = ? WHERE id = ?`,
|
`UPDATE conversation_cache SET data = ? WHERE id = ?`,
|
||||||
[JSON.stringify(conv), String(conversationId)]
|
[JSON.stringify(conv), String(conversationId)]
|
||||||
|
|||||||
@@ -31,17 +31,23 @@ import {
|
|||||||
updateConversationCacheUnreadCount,
|
updateConversationCacheUnreadCount,
|
||||||
} from './database';
|
} from './database';
|
||||||
|
|
||||||
|
/** 远端会话列表分页(offset / cursor)统一结果:拉取成功后均已执行本地缓存同步 */
|
||||||
|
export interface RemoteConversationListPageResult {
|
||||||
|
items: ConversationResponse[];
|
||||||
|
hasMore: boolean;
|
||||||
|
/** cursor 模式:下一页游标;offset 模式固定为 null */
|
||||||
|
nextCursor: string | null;
|
||||||
|
/** offset 模式:下一次 loadNext 应请求的页码;cursor 模式为 undefined */
|
||||||
|
nextPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 消息服务类
|
// 消息服务类
|
||||||
class MessageService {
|
class MessageService {
|
||||||
private async refreshConversationsFromServer(
|
/** 与会话 offset 列表一致:将本页会话写入 SQLite,避免游标路径不落库导致冷启动未读与线上一致 */
|
||||||
page = 1,
|
private async persistConversationListPageToLocalCache(
|
||||||
pageSize = 20
|
list: ConversationResponse[]
|
||||||
): Promise<ConversationListResponse> {
|
): Promise<void> {
|
||||||
const response = await api.get<ConversationListResponse>('/conversations', {
|
if (!list.length) return;
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
});
|
|
||||||
const list = response.data.list || [];
|
|
||||||
await saveConversationsCache(list);
|
await saveConversationsCache(list);
|
||||||
await saveUsersCache(
|
await saveUsersCache(
|
||||||
list.flatMap(conv => [
|
list.flatMap(conv => [
|
||||||
@@ -54,9 +60,98 @@ class MessageService {
|
|||||||
.map(conv => conv.group)
|
.map(conv => conv.group)
|
||||||
.filter((group): group is NonNullable<typeof group> => Boolean(group))
|
.filter((group): group is NonNullable<typeof group> => Boolean(group))
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** offset 会话列表单页:请求 + 落库 */
|
||||||
|
private async requestOffsetConversationsPage(
|
||||||
|
page: number,
|
||||||
|
pageSize: number
|
||||||
|
): Promise<ConversationListResponse> {
|
||||||
|
const response = await api.get<ConversationListResponse>('/conversations', {
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
const list = response.data.list || [];
|
||||||
|
await this.persistConversationListPageToLocalCache(list);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** cursor 会话列表单页:请求 + 落库 */
|
||||||
|
private async requestCursorConversationsPage(
|
||||||
|
params: CursorPaginationRequest
|
||||||
|
): Promise<CursorPaginationResponse<ConversationResponse>> {
|
||||||
|
try {
|
||||||
|
const response = await api.get<CursorPaginationResponse<ConversationResponse>>(
|
||||||
|
'/conversations/cursor',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const raw = response.data;
|
||||||
|
const list = Array.isArray(raw?.list) ? raw.list : [];
|
||||||
|
await this.persistConversationListPageToLocalCache(list);
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
next_cursor: raw?.next_cursor ?? null,
|
||||||
|
prev_cursor: raw?.prev_cursor ?? null,
|
||||||
|
has_more: raw?.has_more ?? false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取会话列表失败:', error);
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
next_cursor: null,
|
||||||
|
prev_cursor: null,
|
||||||
|
has_more: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshConversationsFromServer(
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20
|
||||||
|
): Promise<ConversationListResponse> {
|
||||||
|
return this.requestOffsetConversationsPage(page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远端会话列表单页(offset / cursor 统一入口)
|
||||||
|
* 两种模式均先写 SQLite 再返回,行为与 NetworkRemoteConversationListPagedSource 配套。
|
||||||
|
*/
|
||||||
|
async fetchRemoteConversationListPage(args: {
|
||||||
|
mode: 'offset' | 'cursor';
|
||||||
|
pageSize: number;
|
||||||
|
/** offset:本次页码,默认 1 */
|
||||||
|
page?: number;
|
||||||
|
/** cursor:本次游标;首屏不传或 null */
|
||||||
|
cursor?: string | null;
|
||||||
|
direction?: CursorPaginationRequest['direction'];
|
||||||
|
}): Promise<RemoteConversationListPageResult> {
|
||||||
|
if (args.mode === 'offset') {
|
||||||
|
const page = args.page ?? 1;
|
||||||
|
const data = await this.requestOffsetConversationsPage(page, args.pageSize);
|
||||||
|
const totalPages = Math.max(0, data.total_pages ?? 0);
|
||||||
|
const currentPage = data.page ?? page;
|
||||||
|
const hasMore = totalPages > 0 && currentPage < totalPages;
|
||||||
|
return {
|
||||||
|
items: data.list || [],
|
||||||
|
hasMore,
|
||||||
|
nextCursor: null,
|
||||||
|
nextPage: currentPage + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await this.requestCursorConversationsPage({
|
||||||
|
page_size: args.pageSize,
|
||||||
|
...(args.cursor != null && args.cursor !== ''
|
||||||
|
? { cursor: args.cursor }
|
||||||
|
: {}),
|
||||||
|
...(args.direction ? { direction: args.direction } : {}),
|
||||||
|
});
|
||||||
|
const items = raw.list || [];
|
||||||
|
const nextCursor = raw.next_cursor ?? null;
|
||||||
|
const hasMore = Boolean(raw.has_more && nextCursor != null);
|
||||||
|
return { items, hasMore, nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
private async refreshConversationDetailFromServer(id: string): Promise<ConversationDetailResponse> {
|
private async refreshConversationDetailFromServer(id: string): Promise<ConversationDetailResponse> {
|
||||||
const response = await api.get<ConversationDetailResponse>(
|
const response = await api.get<ConversationDetailResponse>(
|
||||||
`/conversations/${encodeURIComponent(id)}`
|
`/conversations/${encodeURIComponent(id)}`
|
||||||
@@ -433,8 +528,8 @@ class MessageService {
|
|||||||
await api.post(`/conversations/${encodeURIComponent(conversationId)}/read`, {
|
await api.post(`/conversations/${encodeURIComponent(conversationId)}/read`, {
|
||||||
last_read_seq: Number(seq),
|
last_read_seq: Number(seq),
|
||||||
});
|
});
|
||||||
// 立即清零本地缓存中的 unread_count,避免回到列表时仍显示旧红点
|
// 立即清零本地缓存中的 unread_count,并写入已读游标,避免冷启动仍显示旧红点
|
||||||
updateConversationCacheUnreadCount(conversationId, 0).catch(error => {
|
updateConversationCacheUnreadCount(conversationId, 0, Number(seq)).catch(error => {
|
||||||
console.error('更新本地会话未读数失败:', error);
|
console.error('更新本地会话未读数失败:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -560,37 +655,6 @@ class MessageService {
|
|||||||
|
|
||||||
// ==================== 游标分页方法 ====================
|
// ==================== 游标分页方法 ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取会话列表(游标分页)
|
|
||||||
* GET /api/v1/conversations/cursor
|
|
||||||
* @param params 游标分页请求参数
|
|
||||||
*/
|
|
||||||
async getConversationsCursor(
|
|
||||||
params: CursorPaginationRequest = {}
|
|
||||||
): Promise<CursorPaginationResponse<ConversationResponse>> {
|
|
||||||
try {
|
|
||||||
const response = await api.get<CursorPaginationResponse<ConversationResponse>>(
|
|
||||||
'/conversations/cursor',
|
|
||||||
params
|
|
||||||
);
|
|
||||||
const raw = response.data;
|
|
||||||
return {
|
|
||||||
list: Array.isArray(raw?.list) ? raw.list : [],
|
|
||||||
next_cursor: raw?.next_cursor ?? null,
|
|
||||||
prev_cursor: raw?.prev_cursor ?? null,
|
|
||||||
has_more: raw?.has_more ?? false,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取会话列表失败:', error);
|
|
||||||
return {
|
|
||||||
list: [],
|
|
||||||
next_cursor: null,
|
|
||||||
prev_cursor: null,
|
|
||||||
has_more: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取消息列表(游标分页)
|
* 获取消息列表(游标分页)
|
||||||
* GET /api/v1/conversations/:id/messages/cursor
|
* GET /api/v1/conversations/:id/messages/cursor
|
||||||
|
|||||||
@@ -25,51 +25,28 @@ export interface IConversationListPagedSource {
|
|||||||
readonly hasMore: boolean;
|
readonly hasMore: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 远端常规分页:GET /conversations?page=&page_size=(始终请求网络,避免走 getConversations 的本地短路) */
|
/**
|
||||||
export class NetworkOffsetConversationListPagedSource implements IConversationListPagedSource {
|
* 远端会话列表(offset 与 cursor 共用实现)
|
||||||
|
* 均通过 messageService.fetchRemoteConversationListPage 拉取并落库,行为一致。
|
||||||
|
*/
|
||||||
|
export class NetworkRemoteConversationListPagedSource implements IConversationListPagedSource {
|
||||||
private nextPage = 1;
|
private nextPage = 1;
|
||||||
|
private nextCursor: string | null = null;
|
||||||
private hasMoreAfterLastLoad = false;
|
private hasMoreAfterLastLoad = false;
|
||||||
private loadedOnce = false;
|
private loadedOnce = false;
|
||||||
private readonly pageSize: number;
|
private readonly pageSize: number;
|
||||||
|
private readonly mode: RemoteConversationListSourceKind;
|
||||||
|
|
||||||
constructor(pageSize: number = CONVERSATION_LIST_PAGE_SIZE) {
|
constructor(
|
||||||
|
mode: RemoteConversationListSourceKind,
|
||||||
|
pageSize: number = CONVERSATION_LIST_PAGE_SIZE
|
||||||
|
) {
|
||||||
|
this.mode = mode;
|
||||||
this.pageSize = pageSize;
|
this.pageSize = pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
restart(): void {
|
restart(): void {
|
||||||
this.nextPage = 1;
|
this.nextPage = 1;
|
||||||
this.hasMoreAfterLastLoad = false;
|
|
||||||
this.loadedOnce = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasMore(): boolean {
|
|
||||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNext(): Promise<ConversationListPage> {
|
|
||||||
const response = await messageService.getConversations(this.nextPage, this.pageSize, true);
|
|
||||||
const items = response.list || [];
|
|
||||||
const totalPages = Math.max(0, response.total_pages ?? 0);
|
|
||||||
const currentPage = response.page ?? this.nextPage;
|
|
||||||
this.hasMoreAfterLastLoad = totalPages > 0 && currentPage < totalPages;
|
|
||||||
this.nextPage = currentPage + 1;
|
|
||||||
this.loadedOnce = true;
|
|
||||||
return { items, hasMore: this.hasMoreAfterLastLoad };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 远端游标接口 */
|
|
||||||
export class NetworkCursorConversationListPagedSource implements IConversationListPagedSource {
|
|
||||||
private nextCursor: string | null = null;
|
|
||||||
private hasMoreAfterLastLoad = false;
|
|
||||||
private loadedOnce = false;
|
|
||||||
private readonly pageSize: number;
|
|
||||||
|
|
||||||
constructor(pageSize: number = CONVERSATION_LIST_PAGE_SIZE) {
|
|
||||||
this.pageSize = pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
restart(): void {
|
|
||||||
this.nextCursor = null;
|
this.nextCursor = null;
|
||||||
this.hasMoreAfterLastLoad = false;
|
this.hasMoreAfterLastLoad = false;
|
||||||
this.loadedOnce = false;
|
this.loadedOnce = false;
|
||||||
@@ -80,16 +57,22 @@ export class NetworkCursorConversationListPagedSource implements IConversationLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadNext(): Promise<ConversationListPage> {
|
async loadNext(): Promise<ConversationListPage> {
|
||||||
const response = await messageService.getConversationsCursor(
|
const result = await messageService.fetchRemoteConversationListPage({
|
||||||
this.nextCursor == null
|
mode: this.mode,
|
||||||
? { page_size: this.pageSize }
|
pageSize: this.pageSize,
|
||||||
: { cursor: this.nextCursor, page_size: this.pageSize }
|
...(this.mode === 'offset'
|
||||||
);
|
? { page: this.nextPage }
|
||||||
const items = response.list || [];
|
: { cursor: this.nextCursor }),
|
||||||
this.nextCursor = response.next_cursor ?? null;
|
});
|
||||||
this.hasMoreAfterLastLoad = Boolean(response.has_more && response.next_cursor != null);
|
|
||||||
|
this.hasMoreAfterLastLoad = result.hasMore;
|
||||||
|
if (this.mode === 'offset') {
|
||||||
|
this.nextPage = result.nextPage ?? this.nextPage + 1;
|
||||||
|
} else {
|
||||||
|
this.nextCursor = result.nextCursor ?? null;
|
||||||
|
}
|
||||||
this.loadedOnce = true;
|
this.loadedOnce = true;
|
||||||
return { items, hasMore: this.hasMoreAfterLastLoad };
|
return { items: result.items, hasMore: result.hasMore };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +103,12 @@ export class SqliteConversationListPagedSource implements IConversationListPaged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 未注入 remote 时,按类型创建默认远端源 */
|
/** 远端会话列表分页策略 */
|
||||||
export type RemoteConversationListSourceKind = 'cursor' | 'offset';
|
export type RemoteConversationListSourceKind = 'cursor' | 'offset';
|
||||||
|
|
||||||
export function createRemoteConversationListSource(
|
export function createRemoteConversationListSource(
|
||||||
kind: RemoteConversationListSourceKind,
|
kind: RemoteConversationListSourceKind,
|
||||||
pageSize: number = CONVERSATION_LIST_PAGE_SIZE
|
pageSize: number = CONVERSATION_LIST_PAGE_SIZE
|
||||||
): IConversationListPagedSource {
|
): IConversationListPagedSource {
|
||||||
return kind === 'offset'
|
return new NetworkRemoteConversationListPagedSource(kind, pageSize);
|
||||||
? new NetworkOffsetConversationListPagedSource(pageSize)
|
|
||||||
: new NetworkCursorConversationListPagedSource(pageSize);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export {
|
|||||||
type ConversationListPage,
|
type ConversationListPage,
|
||||||
type IConversationListPagedSource,
|
type IConversationListPagedSource,
|
||||||
type RemoteConversationListSourceKind,
|
type RemoteConversationListSourceKind,
|
||||||
NetworkCursorConversationListPagedSource,
|
NetworkRemoteConversationListPagedSource,
|
||||||
NetworkOffsetConversationListPagedSource,
|
|
||||||
SqliteConversationListPagedSource,
|
SqliteConversationListPagedSource,
|
||||||
createRemoteConversationListSource,
|
createRemoteConversationListSource,
|
||||||
} from './conversationListSources';
|
} from './conversationListSources';
|
||||||
|
|||||||
@@ -353,6 +353,18 @@ class MessageManager {
|
|||||||
this.state.conversationList = list;
|
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 已读保护)
|
* 将游标接口返回的单条会话写入 Map(统一 string id、尊重 pendingReadMap 已读保护)
|
||||||
*/
|
*/
|
||||||
@@ -368,6 +380,7 @@ class MessageManager {
|
|||||||
} else {
|
} else {
|
||||||
next = { ...conv, id };
|
next = { ...conv, id };
|
||||||
}
|
}
|
||||||
|
next = this.normalizeConversationUnreadFromReadCursor(next);
|
||||||
this.state.conversations.set(id, next);
|
this.state.conversations.set(id, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,14 +789,6 @@ class MessageManager {
|
|||||||
const normalizedConversationId = this.normalizeConversationId(conversation_id);
|
const normalizedConversationId = this.normalizeConversationId(conversation_id);
|
||||||
const currentUserId = this.getCurrentUserId();
|
const currentUserId = this.getCurrentUserId();
|
||||||
|
|
||||||
// 【调试日志】追踪消息来源和重复问题
|
|
||||||
|
|
||||||
// 0. 如果是ACK消息,直接跳过不增加未读数(ACK是发送确认,不是新消息)
|
|
||||||
if (_isAck) {
|
|
||||||
// 但仍然需要更新消息列表(因为ACK包含完整消息内容)
|
|
||||||
// 继续处理但不增加未读数
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 消息去重检查 - 防止ACK消息和正常消息重复处理
|
// 1. 消息去重检查 - 防止ACK消息和正常消息重复处理
|
||||||
if (this.isMessageProcessed(id)) {
|
if (this.isMessageProcessed(id)) {
|
||||||
return;
|
return;
|
||||||
@@ -813,7 +818,6 @@ class MessageManager {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('[MessageManager][ERROR] 获取发送者信息失败:', { userId: sender_id, error });
|
console.error('[MessageManager][ERROR] 获取发送者信息失败:', { userId: sender_id, error });
|
||||||
@@ -858,16 +862,13 @@ class MessageManager {
|
|||||||
const isCurrentUserMessage = sender_id === currentUserId;
|
const isCurrentUserMessage = sender_id === currentUserId;
|
||||||
const isActiveConversation = normalizedConversationId === this.state.currentConversationId;
|
const isActiveConversation = normalizedConversationId === this.state.currentConversationId;
|
||||||
|
|
||||||
// 【调试日志】追踪未读数增加逻辑
|
// 确保当前用户发送的消息不会增加未读数
|
||||||
|
|
||||||
// 修复:确保当前用户发送的消息不会增加未读数
|
|
||||||
// 同时确保currentUserId有效(避免undefined === undefined的情况)
|
// 同时确保currentUserId有效(避免undefined === undefined的情况)
|
||||||
// ACK消息(发送确认)也不应该增加未读数
|
// ACK消息(发送确认)也不应该增加未读数
|
||||||
const shouldIncrementUnread = !isCurrentUserMessage && !isActiveConversation && !!currentUserId && !_isAck;
|
const shouldIncrementUnread = !isCurrentUserMessage && !isActiveConversation && !!currentUserId && !_isAck;
|
||||||
|
|
||||||
if (shouldIncrementUnread) {
|
if (shouldIncrementUnread) {
|
||||||
this.incrementUnreadCount(normalizedConversationId);
|
this.incrementUnreadCount(normalizedConversationId);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 如果是当前活动会话,自动标记已读
|
// 6. 如果是当前活动会话,自动标记已读
|
||||||
@@ -1315,9 +1316,11 @@ class MessageManager {
|
|||||||
const isPending = !!readRecord;
|
const isPending = !!readRecord;
|
||||||
// 使用智能合并:如果服务器返回 unread_count > 0 但本地有更晚的已读记录,保留本地状态
|
// 使用智能合并:如果服务器返回 unread_count > 0 但本地有更晚的已读记录,保留本地状态
|
||||||
const shouldPreserveLocalRead = isPending && (detail.unread_count || 0) > 0;
|
const shouldPreserveLocalRead = isPending && (detail.unread_count || 0) > 0;
|
||||||
const safeDetail = shouldPreserveLocalRead
|
const safeDetail = this.normalizeConversationUnreadFromReadCursor(
|
||||||
|
shouldPreserveLocalRead
|
||||||
? { ...(detail as ConversationResponse), id: normalizedId, unread_count: 0 }
|
? { ...(detail as ConversationResponse), id: normalizedId, unread_count: 0 }
|
||||||
: { ...(detail as ConversationResponse), id: normalizedId };
|
: { ...(detail as ConversationResponse), id: normalizedId }
|
||||||
|
);
|
||||||
this.state.conversations.set(normalizedId, safeDetail);
|
this.state.conversations.set(normalizedId, safeDetail);
|
||||||
this.updateConversationList();
|
this.updateConversationList();
|
||||||
|
|
||||||
@@ -1681,6 +1684,25 @@ class MessageManager {
|
|||||||
this.state.totalUnreadCount = unreadData?.total_unread_count ?? 0;
|
this.state.totalUnreadCount = unreadData?.total_unread_count ?? 0;
|
||||||
this.state.systemUnreadCount = systemUnreadData?.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({
|
this.notifySubscribers({
|
||||||
type: 'unread_count_updated',
|
type: 'unread_count_updated',
|
||||||
@@ -1824,10 +1846,11 @@ class MessageManager {
|
|||||||
lastReadSeq: seq,
|
lastReadSeq: seq,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 乐观更新本地状态(立即反映到UI)
|
// 2. 乐观更新本地状态(立即反映到UI;同步已读游标避免与 last_seq 不一致)
|
||||||
const updatedConv: ConversationResponse = {
|
const updatedConv: ConversationResponse = {
|
||||||
...conversation,
|
...conversation,
|
||||||
unread_count: 0,
|
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.conversations.set(normalizedId, updatedConv);
|
||||||
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - prevUnreadCount);
|
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - prevUnreadCount);
|
||||||
|
|||||||
Reference in New Issue
Block a user