Files
frontend/src/stores/messageManagerHooks.ts

696 lines
18 KiB
TypeScript
Raw Normal View History

/**
* 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
*
*
* isFocused
*/
export function useMessageListRefresh(): 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,
};
}