refactor(MessageManager): enhance conversation list handling and integrate local source fallback
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 4m34s
Frontend CI / ota-android (push) Successful in 10m57s
Frontend CI / build-android-apk (push) Has been cancelled

- Update MessageManager to support both remote and local conversation list sources, improving data retrieval and offline capabilities.
- Refactor hooks and components to utilize the new MessageManager structure, ensuring seamless integration of cursor pagination and local caching.
- Introduce new types and methods for better management of conversation data and loading states.
- Modify MessageListScreen to leverage the updated hooks for fetching conversations, enhancing user experience with improved loading and pagination handling.
This commit is contained in:
lafay
2026-03-23 13:40:48 +08:00
parent 26ae288470
commit d97df0b2d4
5 changed files with 407 additions and 89 deletions

View File

@@ -31,13 +31,19 @@ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme';
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments, extractTextFromSegmentsAsync, MessageSegment } from '../../types/dto';
import { authService, messageService } from '../../services';
import { authService } from '../../services';
import { useUserStore, useAuthStore } from '../../stores';
// 【新架构】使用MessageManager hooks
import { messageManager, useMessageListRefresh, useCreateConversation, useUnreadCount, useMarkAsRead } from '../../stores';
// 【新架构】使用MessageManager hooks(会话列表数据源自 MessageManager 游标同步)
import {
messageManager,
useMessageListRefresh,
useCreateConversation,
useUnreadCount,
useMarkAsRead,
useMessageManagerConversations,
} from '../../stores';
import { Avatar, Text, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { RootStackParamList, MessageStackParamList } from '../../navigation/types';
import { getUserCache } from '../../services/database';
// 导入 EmbeddedChat 组件用于桌面端双栏布局
@@ -162,24 +168,14 @@ export const MessageListScreen: React.FC = () => {
const { isDesktop, isTablet, width } = useResponsive();
const isWideScreen = useBreakpointGTE('lg');
// 【游标分页】使用 useCursorPagination hook 获取会话列表
// 会话列表MessageManager 统一游标拉取与合并,与已读/角标同源
const {
list: conversationList,
conversations: conversationList,
isLoading,
isRefreshing,
hasMore,
loadMore,
refresh,
error: paginationError,
} = useCursorPagination(
async ({ cursor, pageSize }) => {
return await messageService.getConversationsCursor({
cursor,
page_size: pageSize,
});
},
{ pageSize: 20 }
);
} = useMessageManagerConversations();
// 使用 MessageManager 获取未读数和系统通知数
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();

View File

@@ -0,0 +1,133 @@
/**
* 会话列表数据源抽象游标、常规页码、SQLite 等实现同一契约MessageManager 只依赖接口。
*/
import { ConversationResponse } from '../types/dto';
import { messageService } from '../services/messageService';
import { getConversationListCache } from '../services/database';
export const CONVERSATION_LIST_PAGE_SIZE = 20;
/** 单次拉取结果(一页或一批) */
export interface ConversationListPage {
items: ConversationResponse[];
/** 在当前源上是否还能再 loadNext 一页 */
hasMore: boolean;
}
/**
* 分页式会话列表源restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。
*/
export interface IConversationListPagedSource {
restart(): void;
loadNext(): Promise<ConversationListPage>;
/** 至少成功 loadNext 过一次且仍有下一页时为 true */
readonly hasMore: boolean;
}
/** 远端常规分页GET /conversations?page=&page_size=(始终请求网络,避免走 getConversations 的本地短路) */
export class NetworkOffsetConversationListPagedSource implements IConversationListPagedSource {
private nextPage = 1;
private hasMoreAfterLastLoad = false;
private loadedOnce = false;
private readonly pageSize: number;
constructor(pageSize: number = CONVERSATION_LIST_PAGE_SIZE) {
this.pageSize = pageSize;
}
restart(): void {
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.hasMoreAfterLastLoad = false;
this.loadedOnce = false;
}
get hasMore(): boolean {
return this.loadedOnce && this.hasMoreAfterLastLoad;
}
async loadNext(): Promise<ConversationListPage> {
const response = await messageService.getConversationsCursor(
this.nextCursor == null
? { page_size: this.pageSize }
: { cursor: this.nextCursor, page_size: this.pageSize }
);
const items = response.list || [];
this.nextCursor = response.next_cursor ?? null;
this.hasMoreAfterLastLoad = Boolean(response.has_more && response.next_cursor != null);
this.loadedOnce = true;
return { items, hasMore: this.hasMoreAfterLastLoad };
}
}
/** SQLite 会话列表缓存(单批,无后续页) */
export class SqliteConversationListPagedSource implements IConversationListPagedSource {
private consumed = false;
restart(): void {
this.consumed = false;
}
get hasMore(): boolean {
return false;
}
async loadNext(): Promise<ConversationListPage> {
if (this.consumed) {
return { items: [], hasMore: false };
}
this.consumed = true;
try {
const items = await getConversationListCache();
return { items, hasMore: false };
} catch (e) {
console.warn('[SqliteConversationListPagedSource] 读取会话列表缓存失败:', e);
return { items: [], hasMore: false };
}
}
}
/** 未注入 remote 时,按类型创建默认远端源 */
export type RemoteConversationListSourceKind = 'cursor' | 'offset';
export function createRemoteConversationListSource(
kind: RemoteConversationListSourceKind,
pageSize: number = CONVERSATION_LIST_PAGE_SIZE
): IConversationListPagedSource {
return kind === 'offset'
? new NetworkOffsetConversationListPagedSource(pageSize)
: new NetworkCursorConversationListPagedSource(pageSize);
}

View File

@@ -14,7 +14,22 @@ export {
// MessageManager 新架构导出(已替换 conversationStore
export { messageManager } from './messageManager';
export type { MessageEvent, MessageEventType, MessageSubscriber } from './messageManager';
export type {
MessageEvent,
MessageEventType,
MessageSubscriber,
MessageManagerConversationListDeps,
} from './messageManager';
export {
CONVERSATION_LIST_PAGE_SIZE,
type ConversationListPage,
type IConversationListPagedSource,
type RemoteConversationListSourceKind,
NetworkCursorConversationListPagedSource,
NetworkOffsetConversationListPagedSource,
SqliteConversationListPagedSource,
createRemoteConversationListSource,
} from './conversationListSources';
export { postManager } from './postManager';
export { groupManager } from './groupManager';
export { userManager } from './userManager';

View File

@@ -11,6 +11,8 @@
* - 统一处理SSE消息
* - 统一处理本地数据库读写
* - 提供订阅机制供React组件使用
*
* UI 只应订阅 MessageManager / hooks列表数据可能来自网络游标或 SQLite 缓存回退,对界面透明。
*/
import { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../types/dto';
@@ -41,14 +43,24 @@ import {
saveUserCache,
updateMessageStatus,
deleteConversation as deleteConversationFromDb,
saveConversationsCache,
saveUsersCache,
saveGroupsCache,
} from '../services/database';
import { api } from '../services/api';
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'
@@ -113,6 +125,19 @@ interface MessageManagerState {
*/
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;
}
/**
* 已读状态记录接口
*/
@@ -138,6 +163,14 @@ class MessageManager {
private initializePromise: Promise<void> | null = null;
private activatingConversationTasks: Map<string, Promise<void>> = new Map();
/** 正在加载会话列表下一页 */
private loadingMoreConversations = false;
/** 远端会话列表(游标或页码,由注入/remoteListKind 决定) */
private readonly remoteConversationListSource: IConversationListPagedSource;
/** 本地会话列表缓存SQLite单批 */
private readonly localConversationListSource: IConversationListPagedSource;
/**
* 正在进行中的已读 API 请求集合
* 用于防止 fetchConversations 的服务器数据覆盖乐观已读状态
@@ -151,7 +184,17 @@ class MessageManager {
*/
private readStateVersion: number = 0;
constructor() {
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: [],
@@ -259,6 +302,85 @@ class MessageManager {
this.state.conversationList = list;
}
/**
* 将游标接口返回的单条会话写入 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 };
}
this.state.conversations.set(id, next);
}
/** 与会话列表同步写入本地缓存(参与者 / 群 / 列表) */
private persistConversationListCache(): void {
const list = Array.from(this.state.conversations.values());
saveConversationsCache(list).catch(error => {
console.error('[MessageManager] 持久化会话列表失败:', error);
});
saveUsersCache(
list.flatMap(conv => [
...(conv.participants || []),
...(conv.last_message?.sender ? [conv.last_message.sender] : []),
])
).catch(error => {
console.error('[MessageManager] 持久化会话相关用户失败:', error);
});
saveGroupsCache(
list
.map(conv => conv.group)
.filter((group): group is NonNullable<ConversationResponse['group']> => Boolean(group))
).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> {
@@ -328,6 +450,9 @@ class MessageManager {
this.state.isInitialized = false;
this.initializePromise = null;
this.activatingConversationTasks.clear();
this.remoteConversationListSource.restart();
this.localConversationListSource.restart();
this.loadingMoreConversations = false;
}
// ==================== SSE 处理 ====================
@@ -996,49 +1121,42 @@ class MessageManager {
// ==================== 数据获取方法 ====================
/**
* 获取会话列表
* 获取会话列表(首屏/下拉刷新:优先网络游标;可回退 SQLite对 UI 透明)
*/
async fetchConversations(forceRefresh = false): Promise<void> {
if (this.state.isLoadingConversations && !forceRefresh) {
return;
}
// 非强制刷新且内存为空:先用本地源暖机,便于离线/弱网先展示列表
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 {
const response = await messageService.getConversations(1, 20, forceRefresh);
const conversations = response.list || [];
this.remoteConversationListSource.restart();
const page = await this.remoteConversationListSource.loadNext();
// 更新会话Map精确保护 pendingReadMap 中的会话不被服务器旧数据覆盖
// 场景markAsRead API 已发出但尚未完成,服务器返回的 unread_count 仍为旧值
this.state.conversations.clear();
conversations.forEach(conv => {
const readRecord = this.pendingReadMap.get(conv.id);
if (readRecord) {
// 此会话有进行中的已读请求或保护期内,使用智能合并逻辑
// 如果服务器返回的 unread_count > 0但本地有更晚的已读记录则保留本地状态
const shouldPreserveLocalRead = conv.unread_count > 0;
if (shouldPreserveLocalRead) {
this.state.conversations.set(conv.id, { ...conv, unread_count: 0 });
} else {
this.state.conversations.set(conv.id, conv);
}
} else {
this.state.conversations.set(conv.id, conv);
}
});
page.items.forEach(conv => this.applyConversationFromFetch(conv));
// 更新排序后的列表
this.updateConversationList();
// 计算总未读数(基于保护后的会话数据)
const totalUnread = Array.from(this.state.conversations.values())
.reduce((sum, conv) => sum + (conv.unread_count || 0), 0);
this.state.totalUnreadCount = totalUnread;
this.recomputeConversationTotalUnread();
// 修复本地缓存refreshConversationsFromServer 已将服务器旧数据写入 SQLite
// 对于 pendingReadMap 中的会话,需要重新把 unread_count=0 写回本地缓存,
// 避免下次冷启动时读到旧的未读数
if (this.pendingReadMap.size > 0) {
for (const convId of this.pendingReadMap.keys()) {
updateConversationCacheUnreadCount(convId, 0).catch(error => {
@@ -1047,24 +1165,15 @@ class MessageManager {
}
}
// 通知更新
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(),
});
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' },
@@ -1072,6 +1181,47 @@ class MessageManager {
});
} 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;
}
}
@@ -1079,25 +1229,26 @@ class MessageManager {
* 获取单个会话详情
*/
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
const normalizedId = this.normalizeConversationId(conversationId);
try {
const detail = await messageService.getConversationById(conversationId);
const detail = await messageService.getConversationById(normalizedId);
if (detail) {
// 若此会话有进行中的已读请求或处于保护期内,保留 unread_count=0
const readRecord = this.pendingReadMap.get(conversationId);
const readRecord = this.pendingReadMap.get(normalizedId);
const isPending = !!readRecord;
// 使用智能合并:如果服务器返回 unread_count > 0 但本地有更晚的已读记录,保留本地状态
const shouldPreserveLocalRead = isPending && (detail.unread_count || 0) > 0;
const safeDetail = shouldPreserveLocalRead
? { ...detail as ConversationResponse, unread_count: 0 }
: detail as ConversationResponse;
this.state.conversations.set(conversationId, safeDetail);
? { ...(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(conversationId, 0).catch(error => {
console.error('[MessageManager] 修复本地缓存未读数失败:', conversationId, error);
updateConversationCacheUnreadCount(normalizedId, 0).catch(error => {
console.error('[MessageManager] 修复本地缓存未读数失败:', normalizedId, error);
});
}
@@ -1554,15 +1705,16 @@ class MessageManager {
* 3. fetchConversations 使用版本号判断数据新旧
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
const normalizedId = this.normalizeConversationId(conversationId);
const conversation = this.state.conversations.get(conversationId);
const conversation = this.state.conversations.get(normalizedId);
if (!conversation) {
console.warn('[MessageManager] 会话不存在:', conversationId);
console.warn('[MessageManager] 会话不存在:', normalizedId);
return;
}
const prevUnreadCount = conversation.unread_count || 0;
const existingRecord = this.pendingReadMap.get(conversationId);
const existingRecord = this.pendingReadMap.get(normalizedId);
// 使用 seq 去重:同一会话若已上报到更大/相同 seq则跳过重复上报
if (existingRecord && seq <= existingRecord.lastReadSeq) {
@@ -1580,7 +1732,7 @@ class MessageManager {
// 1. 标记此会话有进行中的已读请求(防止 fetchConversations 覆盖乐观状态)
// 记录时间戳和版本号,用于后续智能合并
this.pendingReadMap.set(conversationId, {
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
@@ -1591,18 +1743,18 @@ class MessageManager {
...conversation,
unread_count: 0,
};
this.state.conversations.set(conversationId, updatedConv);
this.state.conversations.set(normalizedId, updatedConv);
this.state.totalUnreadCount = Math.max(0, this.state.totalUnreadCount - prevUnreadCount);
this.updateConversationList();
// 3. 更新本地数据库
markConversationAsRead(conversationId).catch(console.error);
markConversationAsRead(normalizedId).catch(console.error);
// 4. 立即通知所有订阅者
this.notifySubscribers({
type: 'message_read',
payload: {
conversationId,
conversationId: normalizedId,
unreadCount: 0,
totalUnreadCount: this.state.totalUnreadCount,
},
@@ -1626,40 +1778,40 @@ class MessageManager {
// 5. 调用 API完成后设置延迟清除保护
try {
await messageService.markAsRead(conversationId, seq);
await messageService.markAsRead(normalizedId, seq);
} catch (error) {
console.error('[MessageManager] 标记已读API失败:', error);
// 失败时回滚状态
this.state.conversations.set(conversationId, conversation);
this.state.conversations.set(normalizedId, conversation);
this.state.totalUnreadCount += prevUnreadCount;
this.updateConversationList();
this.notifySubscribers({
type: 'message_read',
payload: {
conversationId,
conversationId: normalizedId,
unreadCount: prevUnreadCount,
totalUnreadCount: this.state.totalUnreadCount,
},
timestamp: Date.now(),
});
// API 失败时立即清除保护,允许后续 fetch 恢复状态
this.pendingReadMap.delete(conversationId);
this.pendingReadMap.delete(normalizedId);
return;
}
// API 成功后,设置延迟清除保护
// 这确保在 API 完成后的一段时间内fetchConversations 不会用服务器旧数据覆盖已读状态
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(conversationId);
const record = this.pendingReadMap.get(normalizedId);
// 只有版本号匹配时才清除(防止清除新的已读请求的保护)
if (record && record.version === currentVersion) {
this.pendingReadMap.delete(conversationId);
this.pendingReadMap.delete(normalizedId);
}
}, READ_STATE_PROTECTION_DELAY);
// 更新记录保存定时器ID
this.pendingReadMap.set(conversationId, {
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
@@ -1980,6 +2132,13 @@ class MessageManager {
// ==================== 状态查询方法 ====================
/**
* 会话列表是否还能加载更多(由数据源响应推导,不暴露游标本身)
*/
canLoadMoreConversations(): boolean {
return this.remoteConversationListSource.hasMore;
}
/**
* 获取会话列表
*/

View File

@@ -16,17 +16,20 @@ interface UseConversationsReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
}
/**
* 获取会话列表
* 用于MessageListScreen等需要显示会话列表的组件
* 获取会话列表(仅消费 MessageManager网络游标与 SQLite 回退均在 Manager 内完成,调用方无感)
* 用于 MessageListScreen 等需要显示会话列表的组件
*/
export function useConversations(): UseConversationsReturn {
const [conversations, setConversations] = useState<ConversationResponse[]>(
() => messageManager.getConversations()
);
const [isLoading, setIsLoading] = useState(() => messageManager.isLoading());
const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations());
useEffect(() => {
// 订阅MessageManager的事件
@@ -34,6 +37,10 @@ export function useConversations(): UseConversationsReturn {
switch (event.type) {
case 'conversations_updated':
setConversations(messageManager.getConversations());
setHasMore(messageManager.canLoadMoreConversations());
break;
case 'conversations_loading':
setIsLoading(!!event.payload?.loading);
break;
case 'connection_changed':
// 连接状态变化时可能需要刷新
@@ -60,15 +67,19 @@ export function useConversations(): UseConversationsReturn {
}, []);
const refresh = useCallback(async () => {
setIsLoading(true);
await messageManager.fetchConversations(true);
setIsLoading(false);
}, []);
const loadMore = useCallback(async () => {
await messageManager.loadMoreConversations();
}, []);
return {
conversations,
isLoading,
refresh,
loadMore,
hasMore,
};
}
@@ -672,6 +683,8 @@ interface UseMessageListReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
@@ -679,7 +692,7 @@ interface UseMessageListReturn {
}
export function useMessageList(): UseMessageListReturn {
const { conversations, isLoading, refresh } = useConversations();
const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations();
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
const { markAllAsRead, isMarking } = useMarkAsRead(null);
@@ -687,6 +700,8 @@ export function useMessageList(): UseMessageListReturn {
conversations,
isLoading,
refresh,
loadMore,
hasMore,
totalUnreadCount,
systemUnreadCount,
markAllAsRead,