This keeps chat, notification, and post interactions cleaner in production while preserving error-level visibility.
697 lines
18 KiB
TypeScript
697 lines
18 KiB
TypeScript
/**
|
||
* 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 乐观更新),
|
||
* 无需再请求服务器(避免时序竞争覆盖已读状态)。
|
||
* 仅在用户主动下拉刷新时才触发服务器请求。
|
||
* @deprecated isFocused 参数保留仅为向后兼容,不再触发焦点刷新
|
||
*/
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
export function useMessageListRefresh(_isFocused?: boolean): UseMessageListRefreshReturn {
|
||
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,
|
||
};
|
||
}
|