2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* MessageManager React Hooks
|
|
|
|
|
|
*
|
|
|
|
|
|
* 提供React组件与MessageManager的集成
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 所有hooks都基于Zustand selector机制
|
2026-03-09 21:29:03 +08:00
|
|
|
|
* 确保组件能实时获取状态更新
|
2026-03-31 19:15:13 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 重构说明:
|
|
|
|
|
|
* - 移除了手动订阅机制,改用 Zustand selector
|
|
|
|
|
|
* - Zustand 会自动处理依赖追踪和组件重渲染
|
|
|
|
|
|
* - 不需要手动管理订阅/取消订阅
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
2026-03-31 19:15:13 +08:00
|
|
|
|
import { useShallow } from 'zustand/react/shallow';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { ConversationResponse, MessageResponse, MessageSegment } from '../types/dto';
|
2026-03-31 19:15:13 +08:00
|
|
|
|
import { messageManager } from './messageManager';
|
|
|
|
|
|
import { useMessageStore } from './message';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== useConversations - 获取会话列表 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseConversationsReturn {
|
|
|
|
|
|
conversations: ConversationResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
2026-03-23 13:40:48 +08:00
|
|
|
|
loadMore: () => Promise<void>;
|
|
|
|
|
|
hasMore: boolean;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 获取会话列表
|
|
|
|
|
|
* 使用 Zustand selector 自动订阅状态变化
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useConversations(): UseConversationsReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 Zustand selector 直接订阅状态
|
|
|
|
|
|
const conversations = useMessageStore(state => state.conversationList);
|
|
|
|
|
|
const isLoading = useMessageStore(state => state.isLoadingConversations);
|
2026-03-23 13:40:48 +08:00
|
|
|
|
const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 初始化时确保 MessageManager 已初始化
|
|
|
|
|
|
messageManager.initialize().catch(error => {
|
|
|
|
|
|
console.error('[useConversations] 初始化失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
|
|
|
|
|
|
const coldStartSyncTimer = setTimeout(() => {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 1200);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
clearTimeout(coldStartSyncTimer);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 监听 hasMore 变化
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setHasMore(messageManager.canLoadMoreConversations());
|
|
|
|
|
|
}, [conversations]);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const refresh = useCallback(async () => {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
await messageManager.refreshConversations(true, 'hooks-manual-refresh');
|
2026-03-23 13:40:48 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const loadMore = useCallback(async () => {
|
|
|
|
|
|
await messageManager.loadMoreConversations();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
2026-03-23 13:40:48 +08:00
|
|
|
|
loadMore,
|
|
|
|
|
|
hasMore,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useMessages - 获取指定会话的消息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseMessagesReturn {
|
|
|
|
|
|
messages: MessageResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
loadMore: () => Promise<void>;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取指定会话的消息
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 自动订阅消息变化
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useMessages(conversationId: string | null): UseMessagesReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
const normalizedConversationId = conversationId ? String(conversationId) : null;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
|
|
|
|
|
const messages = useMessageStore(
|
|
|
|
|
|
useShallow(state =>
|
|
|
|
|
|
normalizedConversationId ? (state.messagesMap.get(normalizedConversationId) || []) : []
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
const isLoadingMessages = useMessageStore(state =>
|
|
|
|
|
|
normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false
|
|
|
|
|
|
);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [hasMore, setHasMore] = useState(true);
|
2026-03-24 01:19:09 +08:00
|
|
|
|
const loadMoreInFlightRef = useRef(false);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
if (!normalizedConversationId) {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 架构入口:激活会话并完成初始化/同步
|
|
|
|
|
|
messageManager.activateConversation(normalizedConversationId).catch(error => {
|
|
|
|
|
|
console.error('[useMessages] 激活会话失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 清理活动会话
|
|
|
|
|
|
if (messageManager.getActiveConversation() === normalizedConversationId) {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
messageManager.setActiveConversation(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-03-31 19:15:13 +08:00
|
|
|
|
}, [normalizedConversationId]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
const loadMore = useCallback(async () => {
|
2026-03-24 01:19:09 +08:00
|
|
|
|
if (!conversationId || !hasMore || loadMoreInFlightRef.current) return;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
const currentMessages = messageManager.getMessages(conversationId);
|
|
|
|
|
|
if (currentMessages.length === 0) return;
|
|
|
|
|
|
|
2026-03-24 01:19:09 +08:00
|
|
|
|
// 消息数组在 MessageManager 内保持 seq 升序,首项即最早消息
|
|
|
|
|
|
const minSeq = currentMessages[0]?.seq ?? Math.min(...currentMessages.map(m => m.seq));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-24 01:19:09 +08:00
|
|
|
|
loadMoreInFlightRef.current = true;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
|
2026-03-24 01:19:09 +08:00
|
|
|
|
loadMoreInFlightRef.current = false;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果没有加载到新消息,说明没有更多了
|
|
|
|
|
|
if (loadedMessages.length === 0) {
|
|
|
|
|
|
setHasMore(false);
|
|
|
|
|
|
}
|
2026-03-24 01:19:09 +08:00
|
|
|
|
}, [conversationId, hasMore]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
if (!conversationId) return;
|
|
|
|
|
|
|
|
|
|
|
|
await messageManager.fetchMessages(conversationId);
|
|
|
|
|
|
setHasMore(true);
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
messages,
|
2026-03-31 19:15:13 +08:00
|
|
|
|
isLoading: isLoadingMessages,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
hasMore,
|
|
|
|
|
|
loadMore,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useSendMessage - 发送消息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseSendMessageReturn {
|
|
|
|
|
|
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
|
|
|
|
|
|
isSending: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useSendMessage(conversationId: string | null): UseSendMessageReturn {
|
|
|
|
|
|
const [isSending, setIsSending] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const sendMessage = useCallback(
|
|
|
|
|
|
async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
|
|
|
|
|
|
if (!conversationId) return null;
|
|
|
|
|
|
|
|
|
|
|
|
setIsSending(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const message = await messageManager.sendMessage(conversationId, segments, options);
|
|
|
|
|
|
return message;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSending(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[conversationId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
sendMessage,
|
|
|
|
|
|
isSending,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useMarkAsRead - 标记已读 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseMarkAsReadReturn {
|
|
|
|
|
|
markAsRead: (seq: number) => Promise<void>;
|
|
|
|
|
|
markAllAsRead: () => Promise<void>;
|
|
|
|
|
|
isMarking: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记已读
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn {
|
|
|
|
|
|
const [isMarking, setIsMarking] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const markAsRead = useCallback(
|
|
|
|
|
|
async (seq: number) => {
|
|
|
|
|
|
if (!conversationId) return;
|
|
|
|
|
|
|
|
|
|
|
|
setIsMarking(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await messageManager.markAsRead(conversationId, seq);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsMarking(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[conversationId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const markAllAsRead = useCallback(async () => {
|
|
|
|
|
|
setIsMarking(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await messageManager.markAllAsRead();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsMarking(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
markAsRead,
|
|
|
|
|
|
markAllAsRead,
|
|
|
|
|
|
isMarking,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useUnreadCount - 获取未读数 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseUnreadCountReturn {
|
|
|
|
|
|
totalUnreadCount: number;
|
|
|
|
|
|
systemUnreadCount: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取未读消息数
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 直接订阅未读数变化
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useUnreadCount(): UseUnreadCountReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 Zustand selector 直接订阅未读数
|
|
|
|
|
|
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
|
|
|
|
|
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 初始化时获取最新未读数
|
|
|
|
|
|
messageManager.fetchUnreadCount();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
totalUnreadCount,
|
|
|
|
|
|
systemUnreadCount,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取总未读消息数(会话未读 + 系统消息未读)
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 直接订阅
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useTotalUnreadCount(): number {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
|
|
|
|
|
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 初始化时获取最新未读数
|
|
|
|
|
|
messageManager.fetchUnreadCount();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-31 19:15:13 +08:00
|
|
|
|
return totalUnreadCount + systemUnreadCount;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useConversation - 获取单个会话信息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseConversationReturn {
|
|
|
|
|
|
conversation: ConversationResponse | null;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取单个会话信息
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 直接订阅会话变化
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useConversation(conversationId: string | null): UseConversationReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 useShallow 避免每次返回新对象引用导致的无限循环
|
|
|
|
|
|
const conversation = useMessageStore(
|
|
|
|
|
|
useShallow(state =>
|
|
|
|
|
|
conversationId ? (state.conversations.get(conversationId) || null) : null
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
if (!conversationId) return;
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
await messageManager.fetchConversationDetail(conversationId);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversation,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// ==================== useMessageManager - 通用状态 ====================
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
interface UseMessageManagerReturn {
|
|
|
|
|
|
isConnected: boolean;
|
|
|
|
|
|
isInitialized: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 通用MessageManager状态
|
|
|
|
|
|
* 使用 Zustand selector 直接订阅连接状态
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useMessageManager(): UseMessageManagerReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 Zustand selector 直接订阅状态
|
|
|
|
|
|
const isConnected = useMessageStore(state => state.isWSConnected);
|
|
|
|
|
|
const isInitialized = useMessageStore(state => state.isInitialized);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 初始化
|
2026-03-31 19:15:13 +08:00
|
|
|
|
messageManager.initialize().catch(error => {
|
|
|
|
|
|
console.error('[useMessageManager] 初始化失败:', error);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
isConnected,
|
|
|
|
|
|
isInitialized,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useCreateConversation - 创建会话 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseCreateConversationReturn {
|
|
|
|
|
|
createConversation: (userId: string) => Promise<ConversationResponse | null>;
|
|
|
|
|
|
isCreating: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建私聊会话
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useCreateConversation(): UseCreateConversationReturn {
|
|
|
|
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const createConversation = useCallback(async (userId: string): Promise<ConversationResponse | null> => {
|
|
|
|
|
|
setIsCreating(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const conversation = await messageManager.createConversation(userId);
|
|
|
|
|
|
return conversation;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsCreating(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
createConversation,
|
|
|
|
|
|
isCreating,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useUpdateConversation - 更新会话 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseUpdateConversationReturn {
|
|
|
|
|
|
updateConversation: (conversationId: string, updates: Partial<ConversationResponse>) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 本地更新会话信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useUpdateConversation(): UseUpdateConversationReturn {
|
|
|
|
|
|
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
|
|
|
|
|
|
messageManager.updateConversation(conversationId, updates);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
updateConversation,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useSystemUnreadCount - 系统消息未读数操作 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseSystemUnreadCountReturn {
|
|
|
|
|
|
setSystemUnreadCount: (count: number) => void;
|
|
|
|
|
|
incrementSystemUnreadCount: () => void;
|
|
|
|
|
|
decrementSystemUnreadCount: (count?: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 系统消息未读数操作
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useSystemUnreadCount(): UseSystemUnreadCountReturn {
|
|
|
|
|
|
const setSystemUnreadCount = useCallback((count: number) => {
|
|
|
|
|
|
messageManager.setSystemUnreadCount(count);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const incrementSystemUnreadCount = useCallback(() => {
|
|
|
|
|
|
messageManager.incrementSystemUnreadCount();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const decrementSystemUnreadCount = useCallback((count?: number) => {
|
|
|
|
|
|
messageManager.decrementSystemUnreadCount(count);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
setSystemUnreadCount,
|
|
|
|
|
|
incrementSystemUnreadCount,
|
|
|
|
|
|
decrementSystemUnreadCount,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useMessageListRefresh - MessageListScreen焦点刷新 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseMessageListRefreshReturn {
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
isRefreshing: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* MessageListScreen焦点刷新Hook
|
|
|
|
|
|
*/
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
export function useMessageListRefresh(): UseMessageListRefreshReturn {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
|
|
const lastRefreshTimeRef = useRef<number>(0);
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
// 防止重复刷新(最小间隔500ms)
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (now - lastRefreshTimeRef.current < 500) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
lastRefreshTimeRef.current = now;
|
|
|
|
|
|
|
|
|
|
|
|
setIsRefreshing(true);
|
|
|
|
|
|
try {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
await messageManager.refreshConversations(true, 'hooks-load-more-fallback');
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[useMessageListRefresh] 刷新失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsRefreshing(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
isRefreshing,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 便捷hooks组合 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ChatScreen专用Hook组合
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface UseChatReturn {
|
|
|
|
|
|
// 消息相关
|
|
|
|
|
|
messages: MessageResponse[];
|
|
|
|
|
|
isLoadingMessages: boolean;
|
|
|
|
|
|
hasMoreMessages: boolean;
|
|
|
|
|
|
loadMoreMessages: () => Promise<void>;
|
|
|
|
|
|
refreshMessages: () => Promise<void>;
|
|
|
|
|
|
|
|
|
|
|
|
// 发送消息
|
|
|
|
|
|
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
|
|
|
|
|
|
isSending: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 已读相关
|
|
|
|
|
|
markAsRead: (seq: number) => Promise<void>;
|
|
|
|
|
|
|
|
|
|
|
|
// 会话信息
|
|
|
|
|
|
conversation: ConversationResponse | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useChat(conversationId: string | null): UseChatReturn {
|
|
|
|
|
|
const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId);
|
|
|
|
|
|
const { sendMessage, isSending } = useSendMessage(conversationId);
|
|
|
|
|
|
const { markAsRead } = useMarkAsRead(conversationId);
|
|
|
|
|
|
const { conversation } = useConversation(conversationId);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
messages,
|
|
|
|
|
|
isLoadingMessages,
|
|
|
|
|
|
hasMoreMessages,
|
|
|
|
|
|
loadMoreMessages: loadMore,
|
|
|
|
|
|
refreshMessages: refresh,
|
|
|
|
|
|
sendMessage,
|
|
|
|
|
|
isSending,
|
|
|
|
|
|
markAsRead,
|
|
|
|
|
|
conversation,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useGroupTyping - 群聊输入状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseGroupTypingReturn {
|
|
|
|
|
|
typingUsers: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取群聊输入状态
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 直接订阅
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useGroupTyping(groupId: string | null): UseGroupTypingReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
|
|
|
|
|
const typingUsers = useMessageStore(
|
|
|
|
|
|
useShallow(state =>
|
|
|
|
|
|
groupId ? (state.typingUsersMap.get(groupId) || []) : []
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
return { typingUsers };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useGroupMuted - 群聊禁言状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseGroupMutedReturn {
|
|
|
|
|
|
isMuted: boolean;
|
|
|
|
|
|
setMuted: (muted: boolean) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取群聊禁言状态
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 使用 Zustand selector 直接订阅
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 使用 Zustand selector 直接订阅禁言状态
|
|
|
|
|
|
const isMuted = useMessageStore(state =>
|
|
|
|
|
|
groupId ? (state.mutedStatusMap.get(groupId) || false) : false
|
|
|
|
|
|
);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
const setMuted = useCallback((muted: boolean) => {
|
|
|
|
|
|
if (groupId) {
|
|
|
|
|
|
messageManager.setMutedStatus(groupId, muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [groupId]);
|
|
|
|
|
|
|
|
|
|
|
|
return { isMuted, setMuted };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* MessageListScreen专用Hook组合
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface UseMessageListReturn {
|
|
|
|
|
|
conversations: ConversationResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
2026-03-23 13:40:48 +08:00
|
|
|
|
loadMore: () => Promise<void>;
|
|
|
|
|
|
hasMore: boolean;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
totalUnreadCount: number;
|
|
|
|
|
|
systemUnreadCount: number;
|
|
|
|
|
|
markAllAsRead: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useMessageList(): UseMessageListReturn {
|
2026-03-23 13:40:48 +08:00
|
|
|
|
const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
|
2026-03-31 19:15:13 +08:00
|
|
|
|
const { markAllAsRead } = useMarkAsRead(null);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
2026-03-23 13:40:48 +08:00
|
|
|
|
loadMore,
|
|
|
|
|
|
hasMore,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
totalUnreadCount,
|
|
|
|
|
|
systemUnreadCount,
|
|
|
|
|
|
markAllAsRead,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|