refactor(message): replace SubscriptionManager with Zustand selectors
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
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
* 消息模块 React Hooks
|
||||
*
|
||||
* 提供React组件与消息状态管理的集成
|
||||
* 所有hooks都基于zustand store和订阅机制
|
||||
* 所有hooks都基于zustand store的selector机制
|
||||
* 确保组件能实时获取状态更新
|
||||
*
|
||||
* 重构说明:直接使用 zustand store,移除了对 messageManager 的直接依赖
|
||||
* 重构说明:
|
||||
* - 移除了 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, subscriptionManager, normalizeConversationId } from './store';
|
||||
import type { MessageEvent, MessageSubscriber } from './types';
|
||||
import { useMessageStore, normalizeConversationId } from './store';
|
||||
|
||||
// ==================== useConversations - 获取会话列表 ====================
|
||||
|
||||
@@ -26,6 +29,7 @@ interface UseConversationsReturn {
|
||||
/**
|
||||
* 获取会话列表
|
||||
* 用于 MessageListScreen 等需要显示会话列表的组件
|
||||
* 使用 Zustand selector 自动订阅状态变化
|
||||
*/
|
||||
export function useConversations(
|
||||
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>,
|
||||
@@ -33,28 +37,12 @@ export function useConversations(
|
||||
canLoadMore: () => boolean,
|
||||
initialize: () => Promise<void>
|
||||
): UseConversationsReturn {
|
||||
const store = useMessageStore();
|
||||
const [conversations, setConversations] = useState<ConversationResponse[]>(() => store.getConversations());
|
||||
const [isLoading, setIsLoading] = useState(() => store.isLoading());
|
||||
// 使用 Zustand selector 直接订阅状态
|
||||
const conversations = useMessageStore(state => state.conversationList);
|
||||
const isLoading = useMessageStore(state => state.isLoadingConversations);
|
||||
const [hasMore, setHasMore] = useState(() => canLoadMore());
|
||||
|
||||
useEffect(() => {
|
||||
// 订阅事件
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
switch (event.type) {
|
||||
case 'conversations_updated':
|
||||
setConversations(store.getConversations());
|
||||
setHasMore(canLoadMore());
|
||||
break;
|
||||
case 'conversations_loading':
|
||||
setIsLoading(!!event.payload?.loading);
|
||||
break;
|
||||
case 'connection_changed':
|
||||
// 连接状态变化时可能需要刷新
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
initialize().catch(error => {
|
||||
console.error('[useConversations] 初始化失败:', error);
|
||||
@@ -69,10 +57,14 @@ export function useConversations(
|
||||
|
||||
return () => {
|
||||
clearTimeout(coldStartSyncTimer);
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听 hasMore 变化
|
||||
useEffect(() => {
|
||||
setHasMore(canLoadMore());
|
||||
}, [conversations, canLoadMore]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchConversations(true, 'hooks-manual-refresh');
|
||||
}, [fetchConversations]);
|
||||
@@ -102,30 +94,24 @@ interface UseMessagesReturn {
|
||||
|
||||
/**
|
||||
* 获取指定会话的消息
|
||||
* 使用 Zustand selector 自动订阅消息变化
|
||||
*/
|
||||
export function useMessages(
|
||||
conversationId: string,
|
||||
fetchMessages: (conversationId: string) => Promise<void>,
|
||||
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
||||
): UseMessagesReturn {
|
||||
const store = useMessageStore();
|
||||
const normalizedConversationId = normalizeConversationId(conversationId);
|
||||
const store = useMessageStore();
|
||||
|
||||
const [messages, setMessages] = useState<MessageResponse[]>(() =>
|
||||
store.getMessages(normalizedConversationId)
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(() =>
|
||||
store.isLoadingMessages(normalizedConversationId)
|
||||
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
||||
const messages = useMessageStore(
|
||||
useShallow(state => state.messagesMap.get(normalizedConversationId) || [])
|
||||
);
|
||||
const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId));
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'messages_updated' && event.payload.conversationId === normalizedConversationId) {
|
||||
setMessages(event.payload.messages);
|
||||
}
|
||||
});
|
||||
|
||||
// 激活会话
|
||||
fetchMessages(normalizedConversationId).catch(error => {
|
||||
console.error('[useMessages] 激活会话失败:', error);
|
||||
@@ -136,12 +122,11 @@ export function useMessages(
|
||||
if (store.getActiveConversation() === normalizedConversationId) {
|
||||
store.setCurrentConversation(null);
|
||||
}
|
||||
unsubscribe();
|
||||
};
|
||||
}, [normalizedConversationId]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const currentMessages = store.getMessages(conversationId);
|
||||
const currentMessages = messages;
|
||||
if (currentMessages.length === 0) return;
|
||||
|
||||
// 消息数组在 store 内保持 seq 升序,首项即最早消息
|
||||
@@ -150,16 +135,15 @@ export function useMessages(
|
||||
|
||||
const loadedMessages = await loadMoreMessages(conversationId, minSeq, 20);
|
||||
setHasMore(loadedMessages.length > 0);
|
||||
}, [conversationId, loadMoreMessages]);
|
||||
}, [conversationId, messages, loadMoreMessages]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchMessages(conversationId);
|
||||
setMessages(store.getMessages(conversationId));
|
||||
}, [conversationId, fetchMessages]);
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
isLoading: isLoadingMessages,
|
||||
hasMore,
|
||||
loadMore,
|
||||
refresh,
|
||||
@@ -240,22 +224,16 @@ interface UseUnreadCountReturn {
|
||||
|
||||
/**
|
||||
* 获取未读数
|
||||
* 使用 Zustand selector 直接订阅未读数变化
|
||||
*/
|
||||
export function useUnreadCount(fetchUnreadCount: () => Promise<void>): UseUnreadCountReturn {
|
||||
const store = useMessageStore();
|
||||
const [counts, setCounts] = useState(() => store.getUnreadCount());
|
||||
// 使用 Zustand selector 直接订阅未读数
|
||||
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
||||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'unread_count_updated') {
|
||||
setCounts(store.getUnreadCount());
|
||||
}
|
||||
});
|
||||
|
||||
// 初始获取
|
||||
fetchUnreadCount();
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
@@ -263,8 +241,8 @@ export function useUnreadCount(fetchUnreadCount: () => Promise<void>): UseUnread
|
||||
}, [fetchUnreadCount]);
|
||||
|
||||
return {
|
||||
totalUnreadCount: counts.total,
|
||||
systemUnreadCount: counts.system,
|
||||
totalUnreadCount,
|
||||
systemUnreadCount,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
@@ -280,26 +258,15 @@ interface UseSystemUnreadCountReturn {
|
||||
|
||||
/**
|
||||
* 获取系统消息未读数
|
||||
* 使用 Zustand selector 直接订阅
|
||||
*/
|
||||
export function useSystemUnreadCount(
|
||||
setSystemUnreadCountService: (count: number) => void,
|
||||
incrementSystemUnreadCountService: () => void,
|
||||
decrementSystemUnreadCountService: (count?: number) => void
|
||||
): UseSystemUnreadCountReturn {
|
||||
const store = useMessageStore();
|
||||
const [systemUnreadCount, setSystemUnreadCountState] = useState(() =>
|
||||
store.getUnreadCount().system
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'unread_count_updated') {
|
||||
setSystemUnreadCountState(store.getUnreadCount().system);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
// 使用 Zustand selector 直接订阅
|
||||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||||
|
||||
const setSystemUnreadCount = useCallback((count: number) => {
|
||||
setSystemUnreadCountService(count);
|
||||
@@ -330,32 +297,19 @@ interface UseConversationReturn {
|
||||
|
||||
/**
|
||||
* 获取单个会话
|
||||
* 使用 Zustand selector 直接订阅会话变化
|
||||
*/
|
||||
export function useConversation(
|
||||
conversationId: string,
|
||||
fetchConversationDetail: (conversationId: string) => Promise<ConversationResponse | null>
|
||||
): UseConversationReturn {
|
||||
const store = useMessageStore();
|
||||
const [conversation, setConversation] = useState<ConversationResponse | null>(() =>
|
||||
store.getConversation(conversationId)
|
||||
// 使用 useShallow 避免每次返回新对象引用导致的无限循环
|
||||
const conversation = useMessageStore(
|
||||
useShallow(state => state.conversations.get(conversationId) || null)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setConversation(store.getConversation(conversationId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'conversations_updated') {
|
||||
const updated = store.getConversation(conversationId);
|
||||
setConversation(updated);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [conversationId]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchConversationDetail(conversationId);
|
||||
setConversation(store.getConversation(conversationId));
|
||||
}, [conversationId, fetchConversationDetail]);
|
||||
|
||||
return {
|
||||
@@ -364,43 +318,28 @@ export function useConversation(
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useMessageManager - 通用订阅 ====================
|
||||
// ==================== useMessageManager - 通用状态 ====================
|
||||
|
||||
interface UseMessageManagerReturn {
|
||||
isConnected: boolean;
|
||||
subscribe: (subscriber: MessageSubscriber) => () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用消息订阅
|
||||
* 通用消息管理状态
|
||||
* 使用 Zustand selector 直接订阅连接状态
|
||||
*/
|
||||
export function useMessageManager(initialize: () => Promise<void>): UseMessageManagerReturn {
|
||||
const store = useMessageStore();
|
||||
const [isConnected, setIsConnected] = useState(() => store.isConnected());
|
||||
// 使用 Zustand selector 直接订阅连接状态
|
||||
const isConnected = useMessageStore(state => state.isWSConnected);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'connection_changed') {
|
||||
setIsConnected(event.payload.connected);
|
||||
}
|
||||
});
|
||||
|
||||
initialize().then(() => {
|
||||
setIsConnected(store.isConnected());
|
||||
}).catch(error => {
|
||||
initialize().catch(error => {
|
||||
console.error('[useMessageManager] 初始化失败:', error);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((subscriber: MessageSubscriber) => {
|
||||
return subscriptionManager.subscribe(subscriber);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -467,25 +406,14 @@ interface UseGroupTypingReturn {
|
||||
|
||||
/**
|
||||
* 获取群聊输入状态
|
||||
* 使用 Zustand selector 直接订阅
|
||||
*/
|
||||
export function useGroupTyping(groupId: string): UseGroupTypingReturn {
|
||||
const store = useMessageStore();
|
||||
const [typingUsers, setTypingUsers] = useState<string[]>(() =>
|
||||
store.getTypingUsers(groupId)
|
||||
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
||||
const typingUsers = useMessageStore(
|
||||
useShallow(state => state.typingUsersMap.get(groupId) || [])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTypingUsers(store.getTypingUsers(groupId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'typing_status' && event.payload.groupId === groupId) {
|
||||
setTypingUsers(event.payload.typingUsers);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [groupId]);
|
||||
|
||||
return {
|
||||
typingUsers,
|
||||
};
|
||||
@@ -500,28 +428,16 @@ interface UseGroupMutedReturn {
|
||||
|
||||
/**
|
||||
* 获取群聊禁言状态
|
||||
* 使用 Zustand selector 直接订阅
|
||||
*/
|
||||
export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
||||
// 使用 Zustand selector 直接订阅禁言状态
|
||||
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false);
|
||||
const store = useMessageStore();
|
||||
const [isMuted, setIsMuted] = useState(() => store.isMuted(groupId));
|
||||
|
||||
useEffect(() => {
|
||||
setIsMuted(store.isMuted(groupId));
|
||||
|
||||
const unsubscribe = subscriptionManager.subscribe((event: MessageEvent) => {
|
||||
if (event.type === 'group_notice' && event.payload.groupId === groupId) {
|
||||
if (event.payload.noticeType === 'muted' || event.payload.noticeType === 'unmuted') {
|
||||
setIsMuted(event.payload.noticeType === 'muted');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [groupId]);
|
||||
|
||||
const setMutedStatus = useCallback((muted: boolean) => {
|
||||
store.setMutedStatus(groupId, muted);
|
||||
}, [groupId]);
|
||||
}, [groupId, store]);
|
||||
|
||||
return {
|
||||
isMuted,
|
||||
@@ -529,29 +445,38 @@ export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== useActiveConversation - 活动会话 ====================
|
||||
// ==================== useConnectionStatus - 连接状态 ====================
|
||||
|
||||
interface UseActiveConversationReturn {
|
||||
activeConversationId: string | null;
|
||||
setActiveConversation: (conversationId: string | null) => void;
|
||||
interface UseConnectionStatusReturn {
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取/设置活动会话
|
||||
* 获取连接状态
|
||||
* 使用 Zustand selector 直接订阅
|
||||
*/
|
||||
export function useActiveConversation(): UseActiveConversationReturn {
|
||||
const store = useMessageStore();
|
||||
const [activeConversationId, setActiveConversationId] = useState<string | null>(() =>
|
||||
store.getActiveConversation()
|
||||
);
|
||||
|
||||
const setActiveConversation = useCallback((conversationId: string | null) => {
|
||||
store.setCurrentConversation(conversationId);
|
||||
setActiveConversationId(conversationId);
|
||||
}, []);
|
||||
export function useConnectionStatus(): UseConnectionStatusReturn {
|
||||
const isConnected = useMessageStore(state => state.isWSConnected);
|
||||
|
||||
return {
|
||||
activeConversationId,
|
||||
setActiveConversation,
|
||||
isConnected,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== useIsInitialized - 初始化状态 ====================
|
||||
|
||||
interface UseIsInitializedReturn {
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始化状态
|
||||
* 使用 Zustand selector 直接订阅
|
||||
*/
|
||||
export function useIsInitialized(): UseIsInitializedReturn {
|
||||
const isInitialized = useMessageStore(state => state.isInitialized);
|
||||
|
||||
return {
|
||||
isInitialized,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user