Remove custom SubscriptionManager class and adopt Zustand's built-in selector mechanism for reactive state updates. This simplifies the architecture by eliminating manual subscription management and relying on Zustand's automatic dependency tracking for component re-renders. Key changes: - Remove SubscriptionManager class from store.ts - Replace all notifySubscribers calls with direct store updates - Update hooks to use Zustand selectors with useShallow for complex data - Remove subscribe/unsubscribe patterns from MessageManager - Simplify EmbeddedChat to use selector instead of local state - Rename requestConversationListRefresh to refreshConversations
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
/**
|
||
* 消息模块 React Hooks
|
||
*
|
||
* 提供React组件与消息状态管理的集成
|
||
* 所有hooks都基于zustand store的selector机制
|
||
* 确保组件能实时获取状态更新
|
||
*
|
||
* 重构说明:
|
||
* - 移除了 SubscriptionManager,改用 Zustand selector
|
||
* - Zustand 会自动处理依赖追踪和组件重渲染
|
||
* - 不需要手动管理订阅/取消订阅
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||
import { useShallow } from 'zustand/react/shallow';
|
||
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
|
||
import { useMessageStore, normalizeConversationId } from './store';
|
||
|
||
// ==================== useConversations - 获取会话列表 ====================
|
||
|
||
interface UseConversationsReturn {
|
||
conversations: ConversationResponse[];
|
||
isLoading: boolean;
|
||
refresh: () => Promise<void>;
|
||
loadMore: () => Promise<void>;
|
||
hasMore: boolean;
|
||
}
|
||
|
||
/**
|
||
* 获取会话列表
|
||
* 用于 MessageListScreen 等需要显示会话列表的组件
|
||
* 使用 Zustand selector 自动订阅状态变化
|
||
*/
|
||
export function useConversations(
|
||
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>,
|
||
loadMoreConversations: () => Promise<void>,
|
||
canLoadMore: () => boolean,
|
||
initialize: () => Promise<void>
|
||
): UseConversationsReturn {
|
||
// 使用 Zustand selector 直接订阅状态
|
||
const conversations = useMessageStore(state => state.conversationList);
|
||
const isLoading = useMessageStore(state => state.isLoadingConversations);
|
||
const [hasMore, setHasMore] = useState(() => canLoadMore());
|
||
|
||
useEffect(() => {
|
||
// 初始化
|
||
initialize().catch(error => {
|
||
console.error('[useConversations] 初始化失败:', error);
|
||
});
|
||
|
||
// 冷启动兜底:延迟做一次强制刷新
|
||
const coldStartSyncTimer = setTimeout(() => {
|
||
fetchConversations(true, 'hooks-initial-refresh').catch(error => {
|
||
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
||
});
|
||
}, 1200);
|
||
|
||
return () => {
|
||
clearTimeout(coldStartSyncTimer);
|
||
};
|
||
}, []);
|
||
|
||
// 监听 hasMore 变化
|
||
useEffect(() => {
|
||
setHasMore(canLoadMore());
|
||
}, [conversations, canLoadMore]);
|
||
|
||
const refresh = useCallback(async () => {
|
||
await fetchConversations(true, 'hooks-manual-refresh');
|
||
}, [fetchConversations]);
|
||
|
||
const loadMore = useCallback(async () => {
|
||
await loadMoreConversations();
|
||
}, [loadMoreConversations]);
|
||
|
||
return {
|
||
conversations,
|
||
isLoading,
|
||
refresh,
|
||
loadMore,
|
||
hasMore,
|
||
};
|
||
}
|
||
|
||
// ==================== useMessages - 获取指定会话的消息 ====================
|
||
|
||
interface UseMessagesReturn {
|
||
messages: MessageResponse[];
|
||
isLoading: boolean;
|
||
hasMore: boolean;
|
||
loadMore: () => Promise<void>;
|
||
refresh: () => Promise<void>;
|
||
}
|
||
|
||
/**
|
||
* 获取指定会话的消息
|
||
* 使用 Zustand selector 自动订阅消息变化
|
||
*/
|
||
export function useMessages(
|
||
conversationId: string,
|
||
fetchMessages: (conversationId: string) => Promise<void>,
|
||
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
||
): UseMessagesReturn {
|
||
const normalizedConversationId = normalizeConversationId(conversationId);
|
||
const store = useMessageStore();
|
||
|
||
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
||
const messages = useMessageStore(
|
||
useShallow(state => state.messagesMap.get(normalizedConversationId) || [])
|
||
);
|
||
const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId));
|
||
const [hasMore, setHasMore] = useState(true);
|
||
|
||
useEffect(() => {
|
||
// 激活会话
|
||
fetchMessages(normalizedConversationId).catch(error => {
|
||
console.error('[useMessages] 激活会话失败:', error);
|
||
});
|
||
|
||
return () => {
|
||
// 清理活动会话
|
||
if (store.getActiveConversation() === normalizedConversationId) {
|
||
store.setCurrentConversation(null);
|
||
}
|
||
};
|
||
}, [normalizedConversationId]);
|
||
|
||
const loadMore = useCallback(async () => {
|
||
const currentMessages = messages;
|
||
if (currentMessages.length === 0) return;
|
||
|
||
// 消息数组在 store 内保持 seq 升序,首项即最早消息
|
||
const minSeq = currentMessages[0]?.seq;
|
||
if (minSeq === undefined) return;
|
||
|
||
const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20);
|
||
setHasMore(loadedMessages.length > 0);
|
||
}, [conversationId, messages, loadMoreMessages]);
|
||
|
||
const refresh = useCallback(async () => {
|
||
await fetchMessages(conversationId);
|
||
}, [conversationId, fetchMessages]);
|
||
|
||
return {
|
||
messages,
|
||
isLoading: isLoadingMessages,
|
||
hasMore,
|
||
loadMore,
|
||
refresh,
|
||
};
|
||
}
|
||
|
||
// ==================== useSendMessage - 发送消息 ====================
|
||
|
||
interface UseSendMessageReturn {
|
||
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
|
||
isSending: boolean;
|
||
}
|
||
|
||
/**
|
||
* 发送消息
|
||
*/
|
||
export function useSendMessage(
|
||
conversationId: string,
|
||
sendMessageService: (conversationId: string, segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>
|
||
): UseSendMessageReturn {
|
||
const [isSending, setIsSending] = useState(false);
|
||
|
||
const sendMessage = useCallback(async (segments: MessageSegment[], options?: { replyToId?: string }) => {
|
||
setIsSending(true);
|
||
try {
|
||
const message = await sendMessageService(conversationId, segments, options);
|
||
return message;
|
||
} catch (error) {
|
||
console.error('[useSendMessage] 发送消息失败:', error);
|
||
return null;
|
||
} finally {
|
||
setIsSending(false);
|
||
}
|
||
}, [conversationId, sendMessageService]);
|
||
|
||
return {
|
||
sendMessage,
|
||
isSending,
|
||
};
|
||
}
|
||
|
||
// ==================== useMarkAsRead - 标记已读 ====================
|
||
|
||
interface UseMarkAsReadReturn {
|
||
markAsRead: (seq: number) => Promise<void>;
|
||
markAllAsRead: () => Promise<void>;
|
||
}
|
||
|
||
/**
|
||
* 标记已读
|
||
*/
|
||
export function useMarkAsRead(
|
||
conversationId: string,
|
||
markAsReadService: (conversationId: string, seq: number) => Promise<void>,
|
||
markAllAsReadService: () => Promise<void>
|
||
): UseMarkAsReadReturn {
|
||
const markAsRead = useCallback(async (seq: number) => {
|
||
await markAsReadService(conversationId, seq);
|
||
}, [conversationId, markAsReadService]);
|
||
|
||
const markAllAsRead = useCallback(async () => {
|
||
await markAllAsReadService();
|
||
}, [markAllAsReadService]);
|
||
|
||
return {
|
||
markAsRead,
|
||
markAllAsRead,
|
||
};
|
||
}
|
||
|
||
// ==================== useUnreadCount - 获取未读数 ====================
|
||
|
||
interface UseUnreadCountReturn {
|
||
totalUnreadCount: number;
|
||
systemUnreadCount: number;
|
||
refresh: () => Promise<void>;
|
||
}
|
||
|
||
/**
|
||
* 获取未读数
|
||
* 使用 Zustand selector 直接订阅未读数变化
|
||
*/
|
||
export function useUnreadCount(fetchUnreadCount: () => Promise<void>): UseUnreadCountReturn {
|
||
// 使用 Zustand selector 直接订阅未读数
|
||
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||
|
||
useEffect(() => {
|
||
// 初始获取
|
||
fetchUnreadCount();
|
||
}, []);
|
||
|
||
const refresh = useCallback(async () => {
|
||
await fetchUnreadCount();
|
||
}, [fetchUnreadCount]);
|
||
|
||
return {
|
||
totalUnreadCount,
|
||
systemUnreadCount,
|
||
refresh,
|
||
};
|
||
}
|
||
|
||
// ==================== useSystemUnreadCount - 获取系统消息未读数 ====================
|
||
|
||
interface UseSystemUnreadCountReturn {
|
||
systemUnreadCount: number;
|
||
setSystemUnreadCount: (count: number) => void;
|
||
incrementSystemUnreadCount: () => void;
|
||
decrementSystemUnreadCount: (count?: number) => void;
|
||
}
|
||
|
||
/**
|
||
* 获取系统消息未读数
|
||
* 使用 Zustand selector 直接订阅
|
||
*/
|
||
export function useSystemUnreadCount(
|
||
setSystemUnreadCountService: (count: number) => void,
|
||
incrementSystemUnreadCountService: () => void,
|
||
decrementSystemUnreadCountService: (count?: number) => void
|
||
): UseSystemUnreadCountReturn {
|
||
// 使用 Zustand selector 直接订阅
|
||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||
|
||
const setSystemUnreadCount = useCallback((count: number) => {
|
||
setSystemUnreadCountService(count);
|
||
}, [setSystemUnreadCountService]);
|
||
|
||
const incrementSystemUnreadCount = useCallback(() => {
|
||
incrementSystemUnreadCountService();
|
||
}, [incrementSystemUnreadCountService]);
|
||
|
||
const decrementSystemUnreadCount = useCallback((count = 1) => {
|
||
decrementSystemUnreadCountService(count);
|
||
}, [decrementSystemUnreadCountService]);
|
||
|
||
return {
|
||
systemUnreadCount,
|
||
setSystemUnreadCount,
|
||
incrementSystemUnreadCount,
|
||
decrementSystemUnreadCount,
|
||
};
|
||
}
|
||
|
||
// ==================== useConversation - 获取单个会话 ====================
|
||
|
||
interface UseConversationReturn {
|
||
conversation: ConversationResponse | null;
|
||
refresh: () => Promise<void>;
|
||
}
|
||
|
||
/**
|
||
* 获取单个会话
|
||
* 使用 Zustand selector 直接订阅会话变化
|
||
*/
|
||
export function useConversation(
|
||
conversationId: string,
|
||
fetchConversationDetail: (conversationId: string) => Promise<ConversationResponse | null>
|
||
): UseConversationReturn {
|
||
// 使用 useShallow 避免每次返回新对象引用导致的无限循环
|
||
const conversation = useMessageStore(
|
||
useShallow(state => state.conversations.get(conversationId) || null)
|
||
);
|
||
|
||
const refresh = useCallback(async () => {
|
||
await fetchConversationDetail(conversationId);
|
||
}, [conversationId, fetchConversationDetail]);
|
||
|
||
return {
|
||
conversation,
|
||
refresh,
|
||
};
|
||
}
|
||
|
||
// ==================== useMessageManager - 通用状态 ====================
|
||
|
||
interface UseMessageManagerReturn {
|
||
isConnected: boolean;
|
||
}
|
||
|
||
/**
|
||
* 通用消息管理状态
|
||
* 使用 Zustand selector 直接订阅连接状态
|
||
*/
|
||
export function useMessageManager(initialize: () => Promise<void>): UseMessageManagerReturn {
|
||
// 使用 Zustand selector 直接订阅连接状态
|
||
const isConnected = useMessageStore(state => state.isWSConnected);
|
||
|
||
useEffect(() => {
|
||
initialize().catch(error => {
|
||
console.error('[useMessageManager] 初始化失败:', error);
|
||
});
|
||
}, []);
|
||
|
||
return {
|
||
isConnected,
|
||
};
|
||
}
|
||
|
||
// ==================== useCreateConversation - 创建会话 ====================
|
||
|
||
interface UseCreateConversationReturn {
|
||
createConversation: (userId: string) => Promise<ConversationResponse | null>;
|
||
isCreating: boolean;
|
||
}
|
||
|
||
/**
|
||
* 创建会话
|
||
*/
|
||
export function useCreateConversation(
|
||
createConversationService: (userId: string) => Promise<ConversationResponse | null>
|
||
): UseCreateConversationReturn {
|
||
const [isCreating, setIsCreating] = useState(false);
|
||
|
||
const createConversation = useCallback(async (userId: string) => {
|
||
setIsCreating(true);
|
||
try {
|
||
const conversation = await createConversationService(userId);
|
||
return conversation;
|
||
} catch (error) {
|
||
console.error('[useCreateConversation] 创建会话失败:', error);
|
||
return null;
|
||
} finally {
|
||
setIsCreating(false);
|
||
}
|
||
}, [createConversationService]);
|
||
|
||
return {
|
||
createConversation,
|
||
isCreating,
|
||
};
|
||
}
|
||
|
||
// ==================== useUpdateConversation - 更新会话 ====================
|
||
|
||
interface UseUpdateConversationReturn {
|
||
updateConversation: (conversationId: string, updates: Partial<ConversationResponse>) => void;
|
||
}
|
||
|
||
/**
|
||
* 更新会话
|
||
*/
|
||
export function useUpdateConversation(
|
||
updateConversationService: (conversationId: string, updates: Partial<ConversationResponse>) => void
|
||
): UseUpdateConversationReturn {
|
||
const updateConversation = useCallback((conversationId: string, updates: Partial<ConversationResponse>) => {
|
||
updateConversationService(conversationId, updates);
|
||
}, [updateConversationService]);
|
||
|
||
return {
|
||
updateConversation,
|
||
};
|
||
}
|
||
|
||
// ==================== useGroupTyping - 群聊输入状态 ====================
|
||
|
||
interface UseGroupTypingReturn {
|
||
typingUsers: string[];
|
||
}
|
||
|
||
/**
|
||
* 获取群聊输入状态
|
||
* 使用 Zustand selector 直接订阅
|
||
*/
|
||
export function useGroupTyping(groupId: string): UseGroupTypingReturn {
|
||
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
||
const typingUsers = useMessageStore(
|
||
useShallow(state => state.typingUsersMap.get(groupId) || [])
|
||
);
|
||
|
||
return {
|
||
typingUsers,
|
||
};
|
||
}
|
||
|
||
// ==================== useGroupMuted - 群聊禁言状态 ====================
|
||
|
||
interface UseGroupMutedReturn {
|
||
isMuted: boolean;
|
||
setMutedStatus: (muted: boolean) => void;
|
||
}
|
||
|
||
/**
|
||
* 获取群聊禁言状态
|
||
* 使用 Zustand selector 直接订阅
|
||
*/
|
||
export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
||
// 使用 Zustand selector 直接订阅禁言状态
|
||
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false);
|
||
const store = useMessageStore();
|
||
|
||
const setMutedStatus = useCallback((muted: boolean) => {
|
||
store.setMutedStatus(groupId, muted);
|
||
}, [groupId, store]);
|
||
|
||
return {
|
||
isMuted,
|
||
setMutedStatus,
|
||
};
|
||
}
|
||
|
||
// ==================== useConnectionStatus - 连接状态 ====================
|
||
|
||
interface UseConnectionStatusReturn {
|
||
isConnected: boolean;
|
||
}
|
||
|
||
/**
|
||
* 获取连接状态
|
||
* 使用 Zustand selector 直接订阅
|
||
*/
|
||
export function useConnectionStatus(): UseConnectionStatusReturn {
|
||
const isConnected = useMessageStore(state => state.isWSConnected);
|
||
|
||
return {
|
||
isConnected,
|
||
};
|
||
}
|
||
|
||
// ==================== useIsInitialized - 初始化状态 ====================
|
||
|
||
interface UseIsInitializedReturn {
|
||
isInitialized: boolean;
|
||
}
|
||
|
||
/**
|
||
* 获取初始化状态
|
||
* 使用 Zustand selector 直接订阅
|
||
*/
|
||
export function useIsInitialized(): UseIsInitializedReturn {
|
||
const isInitialized = useMessageStore(state => state.isInitialized);
|
||
|
||
return {
|
||
isInitialized,
|
||
};
|
||
}
|