Files
frontend/src/stores/message/hooks.ts
lan 313820ed20
Some checks failed
Frontend CI / ota-android (push) Successful in 1m40s
Frontend CI / ota-ios (push) Successful in 1m38s
Frontend CI / build-and-push-web (push) Successful in 4m32s
Frontend CI / build-android-apk (push) Failing after 8m53s
feat(message): enhance message store management and UI stability
Implement `removeMessage` in `useMessageStore` to allow for atomic message deletion from the state. Improve `useChatScreen` loading logic to prevent layout jumps and ensure consistent UI state during initial message loading. Additionally, add type safety for WebSocket sequence numbers and optimize message selector stability.

- Add `removeMessage` action to `useMessageStore` for state-consistent deletions
- Refine `useChatScreen` loading state logic to prevent FlashList layout issues
- Ensure `useMessages` hook returns a stable `EMPTY_MESSAGES` constant
- Cast WebSocket `seq` to integer to ensure consistent numeric comparison
- Update `useChatScreen` to use `useMessageStore` for immediate local deletion feedback
2026-05-25 02:30:59 +08:00

587 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MessageManager React Hooks
*
* 提供React组件与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 } from './MessageManager';
import { useMessageStore } from './store';
import { useUnreadCountQuery } from '../../hooks/useUnreadCount';
const EMPTY_MESSAGES: MessageResponse[] = [];
// ==================== useConversations - 获取会话列表 ====================
interface UseConversationsReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
}
/**
* 获取会话列表
* 使用 Zustand selector 自动订阅状态变化
*/
export function useConversations(): UseConversationsReturn {
// 使用 Zustand selector 直接订阅状态
const conversations = useMessageStore(state => state.conversationList);
const isLoading = useMessageStore(state => state.isLoadingConversations);
const [hasMore, setHasMore] = useState(() => messageManager.canLoadMoreConversations());
useEffect(() => {
// 初始化时确保 MessageManager 已初始化
messageManager.initialize().catch(error => {
console.error('[useConversations] 初始化失败:', error);
});
// 仅在首次初始化时做一次冷启动兜底刷新,避免每次挂载都触发
const store = useMessageStore.getState();
if (!store.isInitialized) {
const coldStartSyncTimer = setTimeout(() => {
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
console.error('[useConversations] 冷启动强制刷新失败:', error);
});
}, 1200);
return () => {
clearTimeout(coldStartSyncTimer);
};
}
}, []);
// 监听 hasMore 变化
useEffect(() => {
setHasMore(messageManager.canLoadMoreConversations());
}, [conversations]);
const refresh = useCallback(async () => {
await messageManager.refreshConversations(true, 'hooks-manual-refresh');
}, []);
const loadMore = useCallback(async () => {
await messageManager.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 | null): UseMessagesReturn {
const normalizedConversationId = conversationId ? String(conversationId) : null;
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
const messages = useMessageStore(
useShallow(state => {
if (!normalizedConversationId) return EMPTY_MESSAGES;
return state.messagesMap.get(normalizedConversationId) ?? EMPTY_MESSAGES;
})
);
const isLoadingMessages = useMessageStore(state =>
normalizedConversationId ? state.loadingMessagesSet.has(normalizedConversationId) : false
);
const [hasMore, setHasMore] = useState(true);
const loadMoreInFlightRef = useRef(false);
useEffect(() => {
if (!normalizedConversationId) {
return;
}
// 架构入口:激活会话并完成初始化/同步
messageManager.activateConversation(normalizedConversationId).catch(error => {
console.error('[useMessages] 激活会话失败:', error);
});
return () => {
// 清理活动会话
if (messageManager.getActiveConversation() === normalizedConversationId) {
messageManager.setActiveConversation(null);
}
};
}, [normalizedConversationId]);
const loadMore = useCallback(async () => {
if (!conversationId || !hasMore || loadMoreInFlightRef.current) return;
const currentMessages = messageManager.getMessages(conversationId);
if (currentMessages.length === 0) return;
// 消息数组在 MessageManager 内保持 seq 升序,首项即最早消息
const minSeq = currentMessages[0]?.seq ?? Math.min(...currentMessages.map(m => m.seq));
loadMoreInFlightRef.current = true;
const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
loadMoreInFlightRef.current = false;
// 如果没有加载到新消息,说明没有更多了
if (loadedMessages.length === 0) {
setHasMore(false);
}
}, [conversationId, hasMore]);
const refresh = useCallback(async () => {
if (!conversationId) return;
await messageManager.fetchMessages(conversationId);
setHasMore(true);
}, [conversationId]);
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 | 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;
}
/**
* 标记已读
*/
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;
}
/**
* 获取未读消息数
* 使用 Zustand selector 直接订阅未读数变化
*/
export function useUnreadCount(): UseUnreadCountReturn {
// Use React Query for fetching (caching + auto-refetch), Zustand selectors for reading
useUnreadCountQuery();
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
return {
totalUnreadCount,
systemUnreadCount,
};
}
// ==================== useTotalUnreadCount - 获取总未读数(包含系统消息) ====================
/**
* 获取总未读消息数(会话未读 + 系统消息未读)
* 使用 Zustand selector 直接订阅
*/
export function useTotalUnreadCount(): number {
// Use React Query for fetching, Zustand selectors for reading
useUnreadCountQuery();
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
return totalUnreadCount + systemUnreadCount;
}
// ==================== useConversation - 获取单个会话信息 ====================
interface UseConversationReturn {
conversation: ConversationResponse | null;
isLoading: boolean;
refresh: () => Promise<void>;
}
/**
* 获取单个会话信息
* 使用 Zustand selector 直接订阅会话变化
*/
export function useConversation(conversationId: string | null): UseConversationReturn {
// 使用 useShallow 避免每次返回新对象引用导致的无限循环
const conversation = useMessageStore(
useShallow(state =>
conversationId ? (state.conversations.get(conversationId) || null) : null
)
);
const [isLoading, setIsLoading] = useState(false);
const refresh = useCallback(async () => {
if (!conversationId) return;
setIsLoading(true);
await messageManager.fetchConversationDetail(conversationId);
setIsLoading(false);
}, [conversationId]);
return {
conversation,
isLoading,
refresh,
};
}
// ==================== useMessageManager - 通用状态 ====================
interface UseMessageManagerReturn {
isConnected: boolean;
isInitialized: boolean;
}
/**
* 通用MessageManager状态
* 使用 Zustand selector 直接订阅连接状态
*/
export function useMessageManager(): UseMessageManagerReturn {
// 使用 Zustand selector 直接订阅状态
const isConnected = useMessageStore(state => state.isWSConnected);
const isInitialized = useMessageStore(state => state.isInitialized);
useEffect(() => {
// 初始化
messageManager.initialize().catch(error => {
console.error('[useMessageManager] 初始化失败:', error);
});
}, []);
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
*/
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.refreshConversations(true, 'hooks-load-more-fallback');
} catch (error) {
console.error('[useMessageListRefresh] 刷新失败:', error);
} finally {
setIsRefreshing(false);
}
}, []);
return {
refresh,
isRefreshing,
};
}
// ==================== 便捷hooks组合 ====================
/**
* ChatScreen专用Hook组合
*/
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[];
}
/**
* 获取群聊输入状态
* 使用 Zustand selector 直接订阅
*/
export function useGroupTyping(groupId: string | null): UseGroupTypingReturn {
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
const typingUsers = useMessageStore(
useShallow(state =>
groupId ? (state.typingUsersMap.get(groupId) || []) : []
)
);
return { typingUsers };
}
// ==================== useGroupMuted - 群聊禁言状态 ====================
interface UseGroupMutedReturn {
isMuted: boolean;
setMuted: (muted: boolean) => void;
}
/**
* 获取群聊禁言状态
* 使用 Zustand selector 直接订阅
*/
export function useGroupMuted(groupId: string | null, currentUserId?: string): UseGroupMutedReturn {
// 使用 Zustand selector 直接订阅禁言状态
const isMuted = useMessageStore(state =>
groupId ? (state.mutedStatusMap.get(groupId) || false) : false
);
const setMuted = useCallback((muted: boolean) => {
if (groupId) {
messageManager.setMutedStatus(groupId, muted);
}
}, [groupId]);
return { isMuted, setMuted };
}
/**
* MessageListScreen专用Hook组合
*/
interface UseMessageListReturn {
conversations: ConversationResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
loadMore: () => Promise<void>;
hasMore: boolean;
totalUnreadCount: number;
systemUnreadCount: number;
markAllAsRead: () => Promise<void>;
}
export function useMessageList(): UseMessageListReturn {
const { conversations, isLoading, refresh, loadMore, hasMore } = useConversations();
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
const { markAllAsRead } = useMarkAsRead(null);
return {
conversations,
isLoading,
refresh,
loadMore,
hasMore,
totalUnreadCount,
systemUnreadCount,
markAllAsRead,
};
}