refactor(message): replace SubscriptionManager with Zustand selectors
All checks were successful
Frontend CI / ota-android (push) Successful in 11m17s
Frontend CI / build-and-push-web (push) Successful in 25m8s
Frontend CI / build-android-apk (push) Successful in 59m7s

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:
lafay
2026-03-31 19:15:13 +08:00
parent 94c11062f0
commit 259de04f3e
14 changed files with 198 additions and 993 deletions

View File

@@ -2,13 +2,20 @@
* MessageManager React Hooks
*
* 提供React组件与MessageManager的集成
* 所有hooks都基于MessageManager的订阅机制
* 所有hooks都基于Zustand selector机制
* 确保组件能实时获取状态更新
*
* 重构说明:
* - 移除了手动订阅机制,改用 Zustand selector
* - Zustand 会自动处理依赖追踪和组件重渲染
* - 不需要手动管理订阅/取消订阅
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { ConversationResponse, MessageResponse, MessageSegment } from '../types/dto';
import { messageManager, MessageEvent } from './messageManager';
import { messageManager } from './messageManager';
import { useMessageStore } from './message';
// ==================== useConversations - 获取会话列表 ====================
@@ -21,33 +28,16 @@ interface UseConversationsReturn {
}
/**
* 获取会话列表(仅消费 MessageManager网络游标与 SQLite 回退均在 Manager 内完成,调用方无感)
* 用于 MessageListScreen 等需要显示会话列表的组件
* 获取会话列表
* 使用 Zustand selector 自动订阅状态变化
*/
export function useConversations(): UseConversationsReturn {
const [conversations, setConversations] = useState<ConversationResponse[]>(
() => messageManager.getConversations()
);
const [isLoading, setIsLoading] = useState(() => messageManager.isLoading());
// 使用 Zustand selector 直接订阅状态
const conversations = useMessageStore(state => state.conversationList);
const isLoading = useMessageStore(state => state.isLoadingConversations);
const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations());
useEffect(() => {
// 订阅MessageManager的事件
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
switch (event.type) {
case 'conversations_updated':
setConversations(messageManager.getConversations());
setHasMore(messageManager.canLoadMoreConversations());
break;
case 'conversations_loading':
setIsLoading(!!event.payload?.loading);
break;
case 'connection_changed':
// 连接状态变化时可能需要刷新
break;
}
});
// 初始化时确保 MessageManager 已初始化
messageManager.initialize().catch(error => {
console.error('[useConversations] 初始化失败:', error);
@@ -55,25 +45,23 @@ export function useConversations(): UseConversationsReturn {
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
const coldStartSyncTimer = setTimeout(() => {
messageManager.requestConversationListRefresh('hooks-initial-refresh', {
force: true,
allowDefer: true,
}).catch(error => {
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
console.error('[useConversations] 冷启动强制刷新失败:', error);
});
}, 1200);
return () => {
clearTimeout(coldStartSyncTimer);
unsubscribe();
};
}, []);
// 监听 hasMore 变化
useEffect(() => {
setHasMore(messageManager.canLoadMoreConversations());
}, [conversations]);
const refresh = useCallback(async () => {
await messageManager.requestConversationListRefresh('hooks-manual-refresh', {
force: true,
allowDefer: false,
});
await messageManager.refreshConversations(true, 'hooks-manual-refresh');
}, []);
const loadMore = useCallback(async () => {
@@ -101,66 +89,40 @@ interface UseMessagesReturn {
/**
* 获取指定会话的消息
* 用于ChatScreen等需要显示消息的组件
*
* @param conversationId 会话ID
* 使用 Zustand selector 自动订阅消息变化
*/
export function useMessages(conversationId: string | null): UseMessagesReturn {
const [messages, setMessages] = useState<MessageResponse[]>([]);
const [isLoading, setIsLoading] = useState(false);
const normalizedConversationId = conversationId ? String(conversationId) : null;
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
const messages = useMessageStore(
useShallow(state =>
normalizedConversationId ? (state.messagesMap.get(normalizedConversationId) || []) : []
)
);
const isLoadingMessages = useMessageStore(state =>
normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false
);
const [hasMore, setHasMore] = useState(true);
const loadMoreInFlightRef = useRef(false);
useEffect(() => {
if (!conversationId) {
setMessages([]);
if (!normalizedConversationId) {
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) {
// 清理活动会话
if (messageManager.getActiveConversation() === normalizedConversationId) {
messageManager.setActiveConversation(null);
}
};
}, [conversationId]);
}, [normalizedConversationId]);
const loadMore = useCallback(async () => {
if (!conversationId || !hasMore || loadMoreInFlightRef.current) return;
@@ -184,16 +146,13 @@ export function useMessages(conversationId: string | null): UseMessagesReturn {
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,
isLoading: isLoadingMessages,
hasMore,
loadMore,
refresh,
@@ -209,7 +168,6 @@ interface UseSendMessageReturn {
/**
* 发送消息
* 返回发送函数和发送状态
*/
export function useSendMessage(conversationId: string | null): UseSendMessageReturn {
const [isSending, setIsSending] = useState(false);
@@ -245,7 +203,6 @@ interface UseMarkAsReadReturn {
/**
* 标记已读
* 用于ChatScreen标记当前会话已读
*/
export function useMarkAsRead(conversationId: string | null): UseMarkAsReadReturn {
const [isMarking, setIsMarking] = useState(false);
@@ -289,27 +246,21 @@ interface UseUnreadCountReturn {
/**
* 获取未读消息数
* 用于TabBar角标等
* 使用 Zustand selector 直接订阅未读数变化
*/
export function useUnreadCount(): UseUnreadCountReturn {
const [counts, setCounts] = useState(() => messageManager.getUnreadCount());
// 使用 Zustand selector 直接订阅未读数
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
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,
totalUnreadCount,
systemUnreadCount,
};
}
@@ -317,29 +268,18 @@ export function useUnreadCount(): UseUnreadCountReturn {
/**
* 获取总未读消息数(会话未读 + 系统消息未读)
* 专用于TabBar徽章显示
* 使用 Zustand selector 直接订阅
*/
export function useTotalUnreadCount(): number {
const [totalCount, setTotalCount] = useState(() => {
const counts = messageManager.getUnreadCount();
return counts.total + counts.system;
});
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
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;
return totalUnreadCount + systemUnreadCount;
}
// ==================== useConversation - 获取单个会话信息 ====================
@@ -352,37 +292,22 @@ interface UseConversationReturn {
/**
* 获取单个会话信息
* 使用 Zustand selector 直接订阅会话变化
*/
export function useConversation(conversationId: string | null): UseConversationReturn {
const [conversation, setConversation] = useState<ConversationResponse | null>(null);
// 使用 useShallow 避免每次返回新对象引用导致的无限循环
const conversation = useMessageStore(
useShallow(state =>
conversationId ? (state.conversations.get(conversationId) || 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]);
@@ -393,7 +318,7 @@ export function useConversation(conversationId: string | null): UseConversationR
};
}
// ==================== useMessageManager - 通用订阅 ====================
// ==================== useMessageManager - 通用状态 ====================
interface UseMessageManagerReturn {
isConnected: boolean;
@@ -401,26 +326,19 @@ interface UseMessageManagerReturn {
}
/**
* 通用MessageManager订阅
* 用于获取连接状态等全局信息
* 通用MessageManager状态
* 使用 Zustand selector 直接订阅连接状态
*/
export function useMessageManager(): UseMessageManagerReturn {
const [isConnected, setIsConnected] = useState(() => messageManager.isConnected());
const [isInitialized, setIsInitialized] = useState(false);
// 使用 Zustand selector 直接订阅状态
const isConnected = useMessageStore(state => state.isWSConnected);
const isInitialized = useMessageStore(state => state.isInitialized);
useEffect(() => {
const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
if (event.type === 'connection_changed') {
setIsConnected(event.payload.connected);
}
});
// 初始化
messageManager.initialize().then(() => {
setIsInitialized(true);
messageManager.initialize().catch(error => {
console.error('[useMessageManager] 初始化失败:', error);
});
return unsubscribe;
}, []);
return {
@@ -517,10 +435,6 @@ interface UseMessageListRefreshReturn {
/**
* MessageListScreen焦点刷新Hook
* 从 ChatScreen 返回时MessageManager 内存状态已经是最新的markAsRead 乐观更新),
* 无需再请求服务器(避免时序竞争覆盖已读状态)。
* 仅在用户主动下拉刷新时才触发服务器请求。
* 注意isFocused 参数已移除,不再触发焦点刷新
*/
export function useMessageListRefresh(): UseMessageListRefreshReturn {
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -536,10 +450,7 @@ export function useMessageListRefresh(): UseMessageListRefreshReturn {
setIsRefreshing(true);
try {
await messageManager.requestConversationListRefresh('hooks-load-more-fallback', {
force: true,
allowDefer: false,
});
await messageManager.refreshConversations(true, 'hooks-load-more-fallback');
} catch (error) {
console.error('[useMessageListRefresh] 刷新失败:', error);
} finally {
@@ -547,9 +458,6 @@ export function useMessageListRefresh(): UseMessageListRefreshReturn {
}
}, []);
// 获得焦点时不主动拉服务器——内存状态已是最新,避免时序竞争
// refresh 仅供下拉刷新手动调用
return {
refresh,
isRefreshing,
@@ -560,7 +468,6 @@ export function useMessageListRefresh(): UseMessageListRefreshReturn {
/**
* ChatScreen专用Hook组合
* 整合所有ChatScreen需要的功能
*/
interface UseChatReturn {
// 消息相关
@@ -608,29 +515,15 @@ interface UseGroupTypingReturn {
/**
* 获取群聊输入状态
* @param groupId 群组ID
* 使用 Zustand selector 直接订阅
*/
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]);
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
const typingUsers = useMessageStore(
useShallow(state =>
groupId ? (state.typingUsersMap.get(groupId) || []) : []
)
);
return { typingUsers };
}
@@ -644,42 +537,17 @@ interface UseGroupMutedReturn {
/**
* 获取群聊禁言状态
* @param groupId 群组ID
* @param currentUserId 当前用户ID用于判断禁言通知是否针对当前用户
* 使用 Zustand selector 直接订阅
*/
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]);
// 使用 Zustand selector 直接订阅禁言状态
const isMuted = useMessageStore(state =>
groupId ? (state.mutedStatusMap.get(groupId) || false) : false
);
const setMuted = useCallback((muted: boolean) => {
if (groupId) {
messageManager.setMutedStatus(groupId, muted);
setIsMuted(muted);
}
}, [groupId]);
@@ -698,13 +566,12 @@ interface UseMessageListReturn {
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
isMarking: boolean;
}
export function useMessageList(): UseMessageListReturn {
const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations();
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
const { markAllAsRead, isMarking } = useMarkAsRead(null);
const { markAllAsRead } = useMarkAsRead(null);
return {
conversations,
@@ -715,6 +582,5 @@ export function useMessageList(): UseMessageListReturn {
totalUnreadCount,
systemUnreadCount,
markAllAsRead,
isMarking,
};
}