2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* MessageManager React Hooks
|
|
|
|
|
|
*
|
|
|
|
|
|
* 提供React组件与MessageManager的集成
|
|
|
|
|
|
* 所有hooks都基于MessageManager的订阅机制
|
|
|
|
|
|
* 确保组件能实时获取状态更新
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
|
|
import { ConversationResponse, MessageResponse, MessageSegment } from '../types/dto';
|
|
|
|
|
|
import { messageManager, MessageEvent } from './messageManager';
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useConversations - 获取会话列表 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseConversationsReturn {
|
|
|
|
|
|
conversations: ConversationResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话列表
|
|
|
|
|
|
* 用于MessageListScreen等需要显示会话列表的组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useConversations(): UseConversationsReturn {
|
|
|
|
|
|
const [conversations, setConversations] = useState<ConversationResponse[]>(
|
|
|
|
|
|
() => messageManager.getConversations()
|
|
|
|
|
|
);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(() => messageManager.isLoading());
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 订阅MessageManager的事件
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
|
case 'conversations_updated':
|
|
|
|
|
|
setConversations(messageManager.getConversations());
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'connection_changed':
|
|
|
|
|
|
// 连接状态变化时可能需要刷新
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时确保 MessageManager 已初始化
|
|
|
|
|
|
messageManager.initialize().catch(error => {
|
|
|
|
|
|
console.error('[useConversations] 初始化失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
|
|
|
|
|
|
const coldStartSyncTimer = setTimeout(() => {
|
|
|
|
|
|
messageManager.fetchConversations(true).catch(error => {
|
|
|
|
|
|
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 1200);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
clearTimeout(coldStartSyncTimer);
|
|
|
|
|
|
unsubscribe();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
await messageManager.fetchConversations(true);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useMessages - 获取指定会话的消息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseMessagesReturn {
|
|
|
|
|
|
messages: MessageResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
loadMore: () => Promise<void>;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取指定会话的消息
|
|
|
|
|
|
* 用于ChatScreen等需要显示消息的组件
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param conversationId 会话ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useMessages(conversationId: string | null): UseMessagesReturn {
|
|
|
|
|
|
const [messages, setMessages] = useState<MessageResponse[]>([]);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
const [hasMore, setHasMore] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!conversationId) {
|
|
|
|
|
|
setMessages([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedConversationId = String(conversationId);
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setHasMore(true);
|
|
|
|
|
|
let isUnmounted = false;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (
|
|
|
|
|
|
event.type === 'messages_updated' &&
|
|
|
|
|
|
String(event.payload.conversationId) === normalizedConversationId
|
|
|
|
|
|
) {
|
|
|
|
|
|
setMessages(event.payload.messages);
|
|
|
|
|
|
if (!isUnmounted) {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 先读取内存快照,确保页面切换时无闪烁
|
|
|
|
|
|
const cachedMessages = messageManager.getMessages(normalizedConversationId);
|
|
|
|
|
|
if (cachedMessages.length > 0) {
|
|
|
|
|
|
setMessages(cachedMessages);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 架构入口:激活会话并完成初始化/同步
|
|
|
|
|
|
messageManager.activateConversation(normalizedConversationId).catch(error => {
|
|
|
|
|
|
console.error('[useMessages] 激活会话失败:', error);
|
|
|
|
|
|
if (!isUnmounted) {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
|
if (!isUnmounted) {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
isUnmounted = true;
|
|
|
|
|
|
unsubscribe();
|
|
|
|
|
|
if (String(messageManager.getActiveConversation()) === normalizedConversationId) {
|
|
|
|
|
|
messageManager.setActiveConversation(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
const loadMore = useCallback(async () => {
|
|
|
|
|
|
if (!conversationId || isLoading || !hasMore) return;
|
|
|
|
|
|
|
|
|
|
|
|
const currentMessages = messageManager.getMessages(conversationId);
|
|
|
|
|
|
if (currentMessages.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取最早的消息seq
|
|
|
|
|
|
const minSeq = Math.min(...currentMessages.map(m => m.seq));
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有加载到新消息,说明没有更多了
|
|
|
|
|
|
if (loadedMessages.length === 0) {
|
|
|
|
|
|
setHasMore(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [conversationId, isLoading, hasMore]);
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
if (!conversationId) return;
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
await messageManager.fetchMessages(conversationId);
|
|
|
|
|
|
setMessages(messageManager.getMessages(conversationId));
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
setHasMore(true);
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
messages,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记已读
|
|
|
|
|
|
* 用于ChatScreen标记当前会话已读
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取未读消息数
|
|
|
|
|
|
* 用于TabBar角标等
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useUnreadCount(): UseUnreadCountReturn {
|
|
|
|
|
|
const [counts, setCounts] = useState(() => messageManager.getUnreadCount());
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'unread_count_updated') {
|
|
|
|
|
|
setCounts(messageManager.getUnreadCount());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时获取最新未读数
|
|
|
|
|
|
messageManager.fetchUnreadCount();
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalUnreadCount: counts.total,
|
|
|
|
|
|
systemUnreadCount: counts.system,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取总未读消息数(会话未读 + 系统消息未读)
|
|
|
|
|
|
* 专用于TabBar徽章显示
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useTotalUnreadCount(): number {
|
|
|
|
|
|
const [totalCount, setTotalCount] = useState(() => {
|
|
|
|
|
|
const counts = messageManager.getUnreadCount();
|
|
|
|
|
|
return counts.total + counts.system;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'unread_count_updated') {
|
|
|
|
|
|
const counts = messageManager.getUnreadCount();
|
|
|
|
|
|
setTotalCount(counts.total + counts.system);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时获取最新未读数
|
|
|
|
|
|
messageManager.fetchUnreadCount();
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return totalCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useConversation - 获取单个会话信息 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseConversationReturn {
|
|
|
|
|
|
conversation: ConversationResponse | null;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取单个会话信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useConversation(conversationId: string | null): UseConversationReturn {
|
|
|
|
|
|
const [conversation, setConversation] = useState<ConversationResponse | null>(null);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!conversationId) {
|
|
|
|
|
|
setConversation(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前会话信息
|
|
|
|
|
|
setConversation(messageManager.getConversation(conversationId));
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅更新
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'conversations_updated') {
|
|
|
|
|
|
const updated = messageManager.getConversation(conversationId);
|
|
|
|
|
|
setConversation(updated);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
if (!conversationId) return;
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
await messageManager.fetchConversationDetail(conversationId);
|
|
|
|
|
|
setConversation(messageManager.getConversation(conversationId));
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}, [conversationId]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversation,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useMessageManager - 通用订阅 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseMessageManagerReturn {
|
|
|
|
|
|
isConnected: boolean;
|
|
|
|
|
|
isInitialized: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通用MessageManager订阅
|
|
|
|
|
|
* 用于获取连接状态等全局信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useMessageManager(): UseMessageManagerReturn {
|
|
|
|
|
|
const [isConnected, setIsConnected] = useState(() => messageManager.isConnected());
|
|
|
|
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'connection_changed') {
|
|
|
|
|
|
setIsConnected(event.payload.connected);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
messageManager.initialize().then(() => {
|
|
|
|
|
|
setIsInitialized(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
* 从 ChatScreen 返回时,MessageManager 内存状态已经是最新的(markAsRead 乐观更新),
|
|
|
|
|
|
* 无需再请求服务器(避免时序竞争覆盖已读状态)。
|
|
|
|
|
|
* 仅在用户主动下拉刷新时才触发服务器请求。
|
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
|
|
|
|
* 注意:isFocused 参数已移除,不再触发焦点刷新
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
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 {
|
|
|
|
|
|
await messageManager.fetchConversations(true);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[useMessageListRefresh] 刷新失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsRefreshing(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 获得焦点时不主动拉服务器——内存状态已是最新,避免时序竞争
|
|
|
|
|
|
// refresh 仅供下拉刷新手动调用
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
isRefreshing,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 便捷hooks组合 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ChatScreen专用Hook组合
|
|
|
|
|
|
* 整合所有ChatScreen需要的功能
|
|
|
|
|
|
*/
|
|
|
|
|
|
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[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取群聊输入状态
|
|
|
|
|
|
* @param groupId 群组ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useGroupTyping(groupId: string | null): UseGroupTypingReturn {
|
|
|
|
|
|
const [typingUsers, setTypingUsers] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
|
setTypingUsers([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始值
|
|
|
|
|
|
setTypingUsers(messageManager.getTypingUsers(groupId));
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅输入状态变化
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'typing_status' && event.payload.groupId === groupId) {
|
|
|
|
|
|
setTypingUsers(event.payload.typingUsers);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, [groupId]);
|
|
|
|
|
|
|
|
|
|
|
|
return { typingUsers };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== useGroupMuted - 群聊禁言状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
interface UseGroupMutedReturn {
|
|
|
|
|
|
isMuted: boolean;
|
|
|
|
|
|
setMuted: (muted: boolean) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取群聊禁言状态
|
|
|
|
|
|
* @param groupId 群组ID
|
|
|
|
|
|
* @param currentUserId 当前用户ID(用于判断禁言通知是否针对当前用户)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn {
|
|
|
|
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
|
setIsMuted(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始值
|
|
|
|
|
|
setIsMuted(messageManager.isMuted(groupId));
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅群通知变化
|
|
|
|
|
|
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
|
|
|
|
|
|
if (event.type === 'group_notice' && event.payload.groupId === groupId) {
|
|
|
|
|
|
const { noticeType, data } = event.payload;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果提供了当前用户ID,只响应当前用户的禁言/解禁通知
|
|
|
|
|
|
if ((noticeType === 'muted' || noticeType === 'unmuted')) {
|
|
|
|
|
|
if (!currentUserId || data?.user_id === currentUserId) {
|
|
|
|
|
|
setIsMuted(noticeType === 'muted');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, [groupId, currentUserId]);
|
|
|
|
|
|
|
|
|
|
|
|
const setMuted = useCallback((muted: boolean) => {
|
|
|
|
|
|
if (groupId) {
|
|
|
|
|
|
messageManager.setMutedStatus(groupId, muted);
|
|
|
|
|
|
setIsMuted(muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [groupId]);
|
|
|
|
|
|
|
|
|
|
|
|
return { isMuted, setMuted };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* MessageListScreen专用Hook组合
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface UseMessageListReturn {
|
|
|
|
|
|
conversations: ConversationResponse[];
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
totalUnreadCount: number;
|
|
|
|
|
|
systemUnreadCount: number;
|
|
|
|
|
|
markAllAsRead: () => Promise<void>;
|
|
|
|
|
|
isMarking: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useMessageList(): UseMessageListReturn {
|
|
|
|
|
|
const { conversations, isLoading, refresh } = useConversations();
|
|
|
|
|
|
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
|
|
|
|
|
|
const { markAllAsRead, isMarking } = useMarkAsRead(null);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
totalUnreadCount,
|
|
|
|
|
|
systemUnreadCount,
|
|
|
|
|
|
markAllAsRead,
|
|
|
|
|
|
isMarking,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|