/** * ChatScreen 自定义 Hook * 管理聊天的所有状态和逻辑 * * 【重构后】完全依赖 MessageManager 架构 * - 消息数据通过 MessageManager 管理 * - 会话详情通过 useChat 返回的 conversation 获取 * - 发送者信息通过 MessageManager 维护 * - 输入状态、禁言状态通过 MessageManager 管理 * - 解决竞态条件和状态同步问题 */ import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { Keyboard, Platform, KeyboardEvent, Alert, } from 'react-native'; import { useLocalSearchParams, router } from 'expo-router'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import * as ImagePicker from 'expo-image-picker'; import { GroupMemberResponse, MessageSegment, TextSegmentData, ImageSegmentData, AtSegmentData, ReplySegmentData, MessageStatus } from '../../../../types/dto'; import { messageService } from '../../../../services/messageService'; import { uploadService } from '../../../../services/uploadService'; import { ApiError } from '../../../../services/api'; // 【新架构】使用 MessageManager import { useChat, useGroupTyping, useGroupMuted, messageManager, callStore } from '../../../../stores'; import { groupService } from '../../../../services/groupService'; import { userManager } from '../../../../stores/userManager'; import { groupManager } from '../../../../stores/groupManager'; import * as hrefs from '../../../../navigation/hrefs'; import { firstRouteParam } from '../../../../navigation/paramUtils'; import { GroupMessage, PanelType, UserRole, SenderInfo, ChatRouteParams, MenuPosition, PendingChatAttachment, } from './types'; import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants'; import { deleteMessage as deleteMessageFromDb, clearConversationMessages, } from '../../../../services/database'; export const useChatScreen = () => { const getSendErrorMessage = useCallback((error: unknown, fallback: string) => { if (error instanceof ApiError && error.message) { return error.message; } return fallback; }, []); const rawParams = useLocalSearchParams<{ conversationId?: string | string[]; userId?: string | string[]; isGroupChat?: string | string[]; groupId?: string | string[]; groupName?: string | string[]; }>(); // 路由参数(动态段 + query);群 ID 勿用 Number(),避免超过 2^53-1 时精度丢失 const routeParams = useMemo(() => { const isGroupFlag = firstRouteParam(rawParams.isGroupChat); return { conversationId: firstRouteParam(rawParams.conversationId) ?? null, userId: firstRouteParam(rawParams.userId) ?? null, isGroupChat: isGroupFlag === '1' || isGroupFlag === 'true', groupId: firstRouteParam(rawParams.groupId), groupName: firstRouteParam(rawParams.groupName), }; }, [ rawParams.conversationId, rawParams.userId, rawParams.isGroupChat, rawParams.groupId, rawParams.groupName, ]); const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName } = routeParams; // 当前会话ID(可能在新建私聊会话后动态更新) const [conversationId, setConversationId] = useState(routeConversationId); // 【新架构】使用 MessageManager 的 hook 获取所有数据 const { messages: messageManagerMessages, isLoadingMessages, hasMoreMessages, loadMoreMessages, refreshMessages, sendMessage: sendMessageViaManager, isSending: isSendingViaManager, markAsRead, conversation, } = useChat(conversationId); // 本地状态 const [currentUserId, setCurrentUserId] = useState(''); // 【新架构】使用 MessageManager 获取群聊输入状态和禁言状态 const { typingUsers: groupTypingUsers } = useGroupTyping(routeGroupId ? String(routeGroupId) : null); const { isMuted: isMutedFromManager, setMuted: setMutedStatus } = useGroupMuted( routeGroupId ? String(routeGroupId) : null, currentUserId ); // 本地状态 const [inputText, setInputText] = useState(''); const [otherUser, setOtherUser] = useState<{ id?: string; nickname?: string; avatar?: string | null } | null>(null); const [isFollowedByOther, setIsFollowedByOther] = useState(null); const [currentUser, setCurrentUser] = useState<{ id?: string; nickname?: string; avatar?: string | null } | null>(null); const [keyboardHeight, setKeyboardHeight] = useState(0); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [lastSeq, setLastSeq] = useState(0); const [firstSeq, setFirstSeq] = useState(0); const [otherUserLastReadSeq, setOtherUserLastReadSeq] = useState(0); const [activePanel, setActivePanel] = useState('none'); const [sendingImage, setSendingImage] = useState(false); const [uploadingAttachments, setUploadingAttachments] = useState(false); const [pendingAttachments, setPendingAttachments] = useState([]); const [loadingMore, setLoadingMore] = useState(false); const [hasMoreHistory, setHasMoreHistory] = useState(true); const flatListRef = useRef(null); const textInputRef = useRef(null); // 滚动状态机 refs(Telegram/Element 风格:底部粘附 + 阅读锚点) const scrollPositionRef = useRef({ contentHeight: 0, scrollY: 0, viewportHeight: 0 }); const hasInitialAnchorDoneRef = useRef(false); const prevMessageCountRef = useRef(0); const prevLatestSeqRef = useRef(0); const prevMarkedReadSeqRef = useRef(0); const enterMarkedKeyRef = useRef(''); const isProgrammaticScrollRef = useRef(false); const suppressAutoFollowRef = useRef(false); const isBrowsingHistoryRef = useRef(false); const hasShownMessageListRef = useRef(false); const historyLoadingLockUntilRef = useRef(0); // 回复消息状态 const [replyingTo, setReplyingTo] = useState(null); // 长按菜单状态 const [longPressMenuVisible, setLongPressMenuVisible] = useState(false); const [selectedMessage, setSelectedMessage] = useState(null); const [menuPosition, setMenuPosition] = useState(undefined); const [selectedMessageId, setSelectedMessageId] = useState(null); // 群聊相关状态 const [groupInfo, setGroupInfo] = useState<{ name?: string; member_count?: number } | null>(null); const [groupMembers, setGroupMembers] = useState([]); const [currentUserRole, setCurrentUserRole] = useState('member'); const [mentionQuery, setMentionQuery] = useState(''); const [selectedMentions, setSelectedMentions] = useState([]); const [mentionAll, setMentionAll] = useState(false); const [isMuted, setIsMuted] = useState(false); const [muteAll, setMuteAll] = useState(false); // 路由会话ID变化时同步到本地会话状态,确保订阅会随会话切换 useEffect(() => { setConversationId(routeConversationId); }, [routeConversationId]); // 【改造】直接使用 MessageManager 返回的消息 const messages = useMemo(() => { return messageManagerMessages.map(m => ({ id: m.id, conversation_id: m.conversation_id, sender_id: m.sender_id, seq: m.seq, segments: m.segments || [], status: (m.status || 'normal') as MessageStatus, category: m.category, created_at: m.created_at, sender: (m as any).sender, is_system_notice: (m as any).is_system_notice, notice_content: (m as any).notice_content, })); }, [messageManagerMessages]); // 对方用户ID(稳定来源):优先路由参数,其次会话参与者,最后本地 otherUser const otherUserId = useMemo(() => { if (isGroupChat) return null; if (routeUserId) return String(routeUserId); const participants = conversation?.participants || []; if (participants.length > 0) { if (currentUserId) { const target = participants.find((p: any) => String(p.id) !== String(currentUserId)); if (target?.id) return String(target.id); } if (participants.length === 1 && participants[0]?.id) return String(participants[0].id); } return otherUser?.id ? String(otherUser.id) : null; }, [isGroupChat, routeUserId, conversation?.participants, currentUserId, otherUser?.id]); // 私聊陌生人限制:未被对方关注时仅可发送1条文本消息,且禁止图片 const followRestricted = useMemo(() => { if (isGroupChat) return false; // 仅使用独立关系状态源,避免 participants 覆盖造成误判 return isFollowedByOther === false; }, [isGroupChat, isFollowedByOther]); const myPrivateSentCount = useMemo(() => { if (!followRestricted || !currentUserId) return 0; return messages.filter(m => String(m.sender_id) === String(currentUserId)).length; }, [messages, followRestricted, currentUserId]); const canSendFirstPrivateText = !followRestricted || myPrivateSentCount < 1; const canSendPrivateImage = !followRestricted; const followRestrictionHint = useMemo(() => { if (!followRestricted) return null; if (myPrivateSentCount >= 1) { return '对方未关注你前:仅可发送1条文字消息(已达上限)'; } return '对方未关注你前:仅可发送1条文字消息,且不能发送图片'; }, [followRestricted, myPrivateSentCount]); // 加载态语义修正: // isLoadingMessages 在分页加载历史时也会短暂为 true。 // 若直接驱动 ChatScreen 的 loading,会导致消息列表被卸载重挂载,触发“回到底部”。 // 这里只在“首屏且尚未展示过消息列表”时展示 loading 占位。 useEffect(() => { if (messageManagerMessages.length > 0) { hasShownMessageListRef.current = true; } const shouldShowInitialLoading = !hasShownMessageListRef.current && isLoadingMessages && messageManagerMessages.length === 0; setLoading(shouldShowInitialLoading); }, [isLoadingMessages, messageManagerMessages.length]); // 【改造】同步 hasMore 状态 useEffect(() => { setHasMoreHistory(hasMoreMessages); }, [hasMoreMessages]); // 【改造】同步发送状态 useEffect(() => { setSending(isSendingViaManager); }, [isSendingViaManager]); // 【改造】同步禁言状态 useEffect(() => { setIsMuted(isMutedFromManager); }, [isMutedFromManager]); // 【改造】从 conversation 对象获取会话详情 useEffect(() => { if (!conversation) return; // 私聊模式:从 conversation.participants 获取对方用户信息 if (!isGroupChat && conversation.participants) { if (!currentUserId) return; const currentUserIdStr = String(currentUserId); const other = conversation.participants.find((p: any) => p.id !== currentUserIdStr); if (other) { setOtherUser(prev => ({ ...(prev || {}), ...other })); } // 获取对方最后阅读位置 setOtherUserLastReadSeq((conversation as any).other_last_read_seq || 0); } }, [conversation, isGroupChat, currentUserId]); // 独立同步私聊对象互关状态,避免 participants 缓存字段不完整或被刷新覆盖 useEffect(() => { if (isGroupChat) return; if (!otherUserId) return; let cancelled = false; const syncRelationState = async () => { try { const detail = await userManager.getUserById(String(otherUserId), true); if (!detail || cancelled) return; setOtherUser(prev => ({ ...(prev || {}), ...detail })); if (typeof (detail as any).is_following_me === 'boolean') { setIsFollowedByOther((detail as any).is_following_me); } else { setIsFollowedByOther(null); } } catch (error) { console.error('同步私聊对象互关状态失败:', error); if (!cancelled) { setIsFollowedByOther(null); } } }; syncRelationState(); return () => { cancelled = true; }; }, [isGroupChat, otherUserId]); // 进入新会话时重置滚动状态 useEffect(() => { hasInitialAnchorDoneRef.current = false; prevMessageCountRef.current = 0; prevLatestSeqRef.current = 0; prevMarkedReadSeqRef.current = 0; enterMarkedKeyRef.current = ''; suppressAutoFollowRef.current = false; isBrowsingHistoryRef.current = false; hasShownMessageListRef.current = false; historyLoadingLockUntilRef.current = 0; }, [conversationId]); useEffect(() => { setPendingAttachments([]); }, [conversationId]); const removePendingAttachment = useCallback((id: string) => { setPendingAttachments(prev => prev.filter(p => p.id !== id)); }, []); const isComposerBusy = useMemo( () => sending || sendingImage || uploadingAttachments, [sending, sendingImage, uploadingAttachments] ); const isHistoryLoadingLocked = useCallback(() => { return Date.now() < historyLoadingLockUntilRef.current; }, []); const scrollToLatest = useCallback((animated: boolean, force: boolean = false, reason: string = 'unknown') => { if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current || isHistoryLoadingLocked())) return; // inverted 列表下,最新消息端对应 offset=0 isProgrammaticScrollRef.current = true; flatListRef.current?.scrollToOffset({ offset: 0, animated, }); setTimeout(() => { isProgrammaticScrollRef.current = false; }, animated ? 220 : 32); }, [flatListRef, isHistoryLoadingLocked]); const isNearBottom = useCallback(() => { const scrollY = scrollPositionRef.current.scrollY || 0; // inverted 列表下,越接近 0 越靠近最新消息端 return scrollY <= 100; }, [scrollPositionRef]); // 首屏加载完成后仅锚定一次到最新消息端 useEffect(() => { if ( loading || loadingMore || messages.length === 0 || hasInitialAnchorDoneRef.current || scrollPositionRef.current.viewportHeight <= 0 ) { return; } hasInitialAnchorDoneRef.current = true; const timer = setTimeout(() => { scrollToLatest(false, true, 'initial-anchor'); }, 0); return () => clearTimeout(timer); }, [loading, loadingMore, messages.length, scrollToLatest]); // 新消息跟随策略(Telegram/QQ 风格): // 仅当“最新端消息 seq 增长”且用户在底部附近时才跟随; // 历史加载只会增加旧消息,不会提升 latest seq,因此不会触发回底。 useEffect(() => { const currentCount = messages.length; const latestSeq = currentCount > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0; const prevLatestSeq = prevLatestSeqRef.current; prevMessageCountRef.current = currentCount; prevLatestSeqRef.current = latestSeq; if (loading || loadingMore) return; if (latestSeq <= prevLatestSeq) return; if (suppressAutoFollowRef.current) return; if (!isNearBottom()) return; const timer = setTimeout(() => { scrollToLatest(false, false, 'new-message-follow'); }, 0); return () => clearTimeout(timer); }, [messages, loading, loadingMore, isNearBottom, scrollToLatest]); // 获取当前用户信息 useEffect(() => { const fetchCurrentUser = async () => { try { const currentUserData = await userManager.getCurrentUser(); if (currentUserData) { setCurrentUserId(String(currentUserData.id)); setCurrentUser(currentUserData); } } catch (error) { console.error('获取当前用户失败:', error); } }; fetchCurrentUser(); }, []); // 群聊模式:获取群组信息和成员列表 useEffect(() => { if (!isGroupChat || !routeGroupId) return; const fetchGroupInfo = async () => { try { const group = await groupManager.getGroup(routeGroupId); setGroupInfo(group); const membersResponse = await groupManager.getMembers(routeGroupId, 1, MEMBERS_PAGE_SIZE); const members = membersResponse.list || []; setGroupMembers(members); const myMember = members.find((m: any) => m.user_id === currentUserId); if (myMember) { setCurrentUserRole(myMember.role); } try { const myInfo = await groupService.getMyMemberInfo(routeGroupId); setIsMuted(myInfo.is_muted); setMutedStatus(myInfo.is_muted); setMuteAll(myInfo.mute_all); } catch (error) { console.error('获取成员信息失败:', error); } } catch (error) { const isGroupNotFound = error instanceof ApiError && (error.code === 404 || error.message === '群组不存在' || error.message.includes('群组不存在')); if (isGroupNotFound) { Alert.alert('提示', '该群组不存在或已解散', [ { text: '确定', onPress: () => { if (router.canGoBack()) { router.back(); } else { router.replace(hrefs.hrefMessages()); } }, }, ]); return; } console.error('获取群组信息失败:', error); Alert.alert('错误', getSendErrorMessage(error, '无法获取群组信息')); } }; if (currentUserId) { fetchGroupInfo(); } }, [isGroupChat, routeGroupId, currentUserId, setMutedStatus]); // 私聊模式:创建或获取会话 useEffect(() => { if (isGroupChat) return; let isCancelled = false; const createOrGetConversation = async () => { if (conversationId) return; if (!routeUserId) return; try { setLoading(true); const response = await messageService.createConversation(routeUserId); if (isCancelled) return; setConversationId(response.id); if (response.participants && response.participants.length > 0) { const other = response.participants.find((p: any) => p.id !== currentUserId); if (other) { setOtherUser(other); } } } catch (error) { console.error('创建会话失败:', error); Alert.alert('错误', '无法创建会话'); } finally { if (!isCancelled) { setLoading(false); } } }; createOrGetConversation(); return () => { isCancelled = true; }; }, [routeUserId, conversationId, currentUserId, isGroupChat]); // 加载更多历史消息(inverted 下保持阅读锚点) const loadMoreHistory = useCallback(async () => { if (!conversationId || !hasMoreHistory || loadingMore) { return; } // 历史加载期间禁止“新消息自动跟随到底” suppressAutoFollowRef.current = true; isBrowsingHistoryRef.current = true; historyLoadingLockUntilRef.current = Date.now() + 3000; // 保存加载前的滚动位置和内容高度 const scrollYBefore = scrollPositionRef.current.scrollY; const contentHeightBefore = scrollPositionRef.current.contentHeight; setLoadingMore(true); try { await loadMoreMessages(); // 更新 firstSeq if (messages.length > 0) { const minSeq = Math.min(...messages.map(m => m.seq)); setFirstSeq(minSeq); } // inverted + maintainVisibleContentPosition 下由列表原生保持位置 // 不做手动 scrollToOffset,避免与原生锚点冲突导致回到底部 } catch (error) { console.error('加载历史消息失败:', error); } finally { setLoadingMore(false); // 加载结束后继续保留短暂锁窗,避免布局结算阶段误触自动回底 historyLoadingLockUntilRef.current = Date.now() + 800; } }, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]); // 列表内容尺寸变化:仅同步内容高度,锚点补偿由 loadMoreHistory 统一处理 const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => { scrollPositionRef.current.contentHeight = contentHeight; }, []); const handleReachLatestEdge = useCallback(() => { if (isHistoryLoadingLocked()) return; suppressAutoFollowRef.current = false; isBrowsingHistoryRef.current = false; }, [isHistoryLoadingLocked]); /** 用户点击「回到底部」:解除浏览历史锁并滚到最新消息端(inverted 下 offset=0) */ const jumpToLatestMessages = useCallback(() => { suppressAutoFollowRef.current = false; isBrowsingHistoryRef.current = false; scrollToLatest(true, true, 'jump-latest-button'); }, [scrollToLatest]); const setBrowsingHistory = useCallback((browsing: boolean) => { isBrowsingHistoryRef.current = browsing; }, []); // 进入聊天详情后立即清未读(不依赖滚动位置) useEffect(() => { if (!conversationId) return; if (!conversation) return; const unreadCount = Number((conversation as any).unread_count || 0); if (unreadCount <= 0) return; const latestFromConversation = Number((conversation as any).last_seq || 0); const latestFromMessages = messages.length > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0; const targetSeq = Math.max(latestFromConversation, latestFromMessages); if (targetSeq <= 0) return; const markKey = `${conversationId}:${targetSeq}`; if (enterMarkedKeyRef.current === markKey) return; enterMarkedKeyRef.current = markKey; prevMarkedReadSeqRef.current = Math.max(prevMarkedReadSeqRef.current, targetSeq); markAsRead(targetSeq).catch(error => { console.error('[ChatScreen] 进入会话清未读失败:', error); }); }, [conversationId, conversation, messages, markAsRead]); // 自动标记已读(QQ/Telegram 风格): // 仅当出现更大的 latest seq,且用户在最新端附近时才上报。 // 历史加载/浏览历史不会触发 read。 useEffect(() => { if (!conversationId || messages.length === 0) return; if (loading || loadingMore) return; if (suppressAutoFollowRef.current || isBrowsingHistoryRef.current) return; if (!isNearBottom()) return; const latestSeq = Math.max(...messages.map(m => m.seq), 0); if (latestSeq <= 0) return; // 没有新增最新消息,不重复上报 if (latestSeq <= prevLatestSeqRef.current) return; prevLatestSeqRef.current = latestSeq; // 避免对同一 seq 重复标记 if (latestSeq <= prevMarkedReadSeqRef.current) return; prevMarkedReadSeqRef.current = latestSeq; markAsRead(latestSeq).catch(error => { console.error('[ChatScreen] 自动标记已读失败:', error); }); }, [messages, conversationId, markAsRead, loading, loadingMore, isNearBottom]); // 使用 ref 存储 groupMembers const groupMembersRef = useRef(groupMembers); useEffect(() => { groupMembersRef.current = groupMembers; }, [groupMembers]); // 使用 ref 存储 currentUserId const currentUserIdRef = useRef(currentUserId); useEffect(() => { currentUserIdRef.current = currentUserId; }, [currentUserId]); // 监听键盘事件 useEffect(() => { const keyboardWillShow = (e: KeyboardEvent) => { setKeyboardHeight(e.endCoordinates.height); setActivePanel(prev => prev === 'mention' ? 'mention' : 'none'); }; const keyboardWillHide = () => { setKeyboardHeight(0); }; const showSubscription = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', keyboardWillShow ); const hideSubscription = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', keyboardWillHide ); return () => { showSubscription.remove(); hideSubscription.remove(); }; }, []); // 格式化时间 const formatTime = useCallback((dateString: string): string => { try { const date = new Date(dateString); const now = new Date(); const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); if (diffInHours < 24 && date.getDate() === now.getDate()) { return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } return formatDistanceToNow(date, { addSuffix: true, locale: zhCN, }); } catch { return ''; } }, []); // 检查是否需要显示时间分隔 const shouldShowTime = useCallback((index: number): boolean => { if (index === 0) return true; const currentMsg = messages[index]; const prevMsg = messages[index - 1]; const currentTime = new Date(currentMsg.created_at).getTime(); const prevTime = new Date(prevMsg.created_at).getTime(); return (currentTime - prevTime) > TIME_SEPARATOR_INTERVAL; }, [messages]); // 处理输入变化,检测@符号 const handleInputChange = useCallback((text: string) => { setInputText(text); if (isGroupChat) { const lastAtIndex = text.lastIndexOf('@'); if (lastAtIndex !== -1) { const textAfterAt = text.slice(lastAtIndex + 1); if (!textAfterAt.includes(' ')) { setMentionQuery(textAfterAt.toLowerCase()); setActivePanel('mention'); } else { setActivePanel('none'); } } else { setActivePanel('none'); } } }, [isGroupChat]); // 【改造】获取发送者信息(群聊)- 优先从消息的 sender 字段获取 const getSenderInfo = useCallback((senderId: string): SenderInfo => { if (senderId === currentUserId) { return { nickname: currentUser?.nickname || '我', avatar: currentUser?.avatar || null, userId: currentUserId, }; } // 优先从消息列表中查找 sender 信息 const messageWithSender = messages.find(m => m.sender_id === senderId && m.sender); if (messageWithSender?.sender) { return { nickname: messageWithSender.sender.nickname || messageWithSender.sender.username || '用户', avatar: messageWithSender.sender.avatar || null, userId: senderId, }; } // 兜底:从群成员列表查找 const member = groupMembers.find(m => m.user_id === senderId); if (member) { return { nickname: member.nickname || member.user?.nickname || '用户', avatar: member.user?.avatar || null, userId: member.user_id, }; } return { nickname: '用户', avatar: null, userId: senderId, }; }, [currentUserId, currentUser, messages, groupMembers]); // 创建消息映射(用于查找被回复的消息) const messageMap = useMemo(() => { const map = new Map(); messages.forEach(msg => { map.set(String(msg.id), msg); }); return map; }, [messages]); // 选择@用户 const handleSelectMention = useCallback((member: { user_id: string; nickname?: string; user?: { nickname?: string } }) => { const lastAtIndex = inputText.lastIndexOf('@'); if (lastAtIndex !== -1) { const textBeforeAt = inputText.slice(0, lastAtIndex); const displayName = member.nickname || member.user?.nickname || '用户'; const newText = `${textBeforeAt}@${displayName} `; setInputText(newText); if (!selectedMentions.includes(member.user_id)) { setSelectedMentions(prev => [...prev, member.user_id]); } } setActivePanel('none'); setMentionQuery(''); setTimeout(() => { textInputRef.current?.focus(); }, 100); }, [inputText, selectedMentions]); // @所有人 const handleMentionAll = useCallback(() => { const lastAtIndex = inputText.lastIndexOf('@'); if (lastAtIndex !== -1) { const textBeforeAt = inputText.slice(0, lastAtIndex); const newText = `${textBeforeAt}@所有人 `; setInputText(newText); setMentionAll(true); } setActivePanel('none'); setMentionQuery(''); setTimeout(() => { textInputRef.current?.focus(); }, 100); }, [inputText]); /** * 构建文本消息的 segments 数组 */ const buildTextSegments = useCallback((text: string, replyToMessage?: GroupMessage | null): MessageSegment[] => { const segments: MessageSegment[] = []; if (replyToMessage) { segments.push({ type: 'reply', data: { id: replyToMessage.id, seq: replyToMessage.seq } as ReplySegmentData }); } if (mentionAll) { segments.push({ type: 'at', data: { user_id: 'all' } as AtSegmentData }); } if (selectedMentions.length > 0) { selectedMentions.forEach(userId => { segments.push({ type: 'at', data: { user_id: userId } as AtSegmentData }); }); } let strippedText = text; if (mentionAll) { strippedText = strippedText.replace(/@所有人\s*/g, ''); } if (selectedMentions.length > 0) { selectedMentions.forEach(userId => { const member = groupMembers.find(m => m.user_id === userId); const nickname = member?.nickname || member?.user?.nickname || '用户'; const escapedName = nickname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); strippedText = strippedText.replace(new RegExp(`@${escapedName}\s*`, 'g'), ''); }); } strippedText = strippedText.trim(); if (strippedText) { segments.push({ type: 'text', data: { text: strippedText } as TextSegmentData }); } return segments; }, [mentionAll, selectedMentions, groupMembers]); /** * 构建图片消息的 segments 数组 */ const buildImageSegments = useCallback((imageUrl: string, thumbnailUrl?: string, width?: number, height?: number, replyToMessage?: GroupMessage | null): MessageSegment[] => { const segments: MessageSegment[] = []; if (replyToMessage) { segments.push({ type: 'reply', data: { id: replyToMessage.id, seq: replyToMessage.seq } as ReplySegmentData }); } segments.push({ type: 'image', data: { url: imageUrl, thumbnail_url: thumbnailUrl || imageUrl, width: width, height: height, } as ImageSegmentData }); return segments; }, []); // 【新架构】发送消息(支持纯文字、纯多图、图文同条) const handleSend = useCallback(async () => { const trimmedText = inputText.trim(); const hasPending = pendingAttachments.length > 0; if ((!trimmedText && !hasPending) || !conversationId) return; if (isGroupChat && isMuted) { if (muteAll) { Alert.alert('无法发送', '当前群组已开启全员禁言'); } else { Alert.alert('无法发送', '你已被管理员禁言'); } return; } if (!isGroupChat) { if (trimmedText && !canSendFirstPrivateText) { Alert.alert('无法发送', '对方未关注你前,仅允许发送一条消息'); return; } if (hasPending && !canSendPrivateImage) { Alert.alert('无法发送', '对方未关注你前,暂不支持发送图片'); return; } } const uploadedUrls: string[] = []; if (hasPending) { setUploadingAttachments(true); try { const results = await Promise.all( pendingAttachments.map(p => uploadService.uploadChatImage({ uri: p.uri, type: p.mimeType }) ) ); for (let i = 0; i < results.length; i++) { const r = results[i]; if (!r?.url) { Alert.alert('上传失败', `第 ${i + 1} 张图片上传失败,请重试`); return; } uploadedUrls.push(r.url); } } catch (error) { console.error('上传图片失败:', error); Alert.alert('上传失败', getSendErrorMessage(error, '图片上传失败,请重试')); return; } finally { setUploadingAttachments(false); } } setSending(true); try { const segments: MessageSegment[] = [...buildTextSegments(trimmedText, replyingTo)]; for (const url of uploadedUrls) { segments.push({ type: 'image', data: { url, thumbnail_url: url, } as ImageSegmentData, }); } if (segments.length === 0) { Alert.alert('无法发送', '消息内容为空'); return; } if (isGroupChat && routeGroupId) { await messageService.sendMessageByAction('group', conversationId, segments); setInputText(''); setSelectedMentions([]); setMentionAll(false); setReplyingTo(null); setPendingAttachments([]); } else { await sendMessageViaManager(segments); setInputText(''); setSelectedMentions([]); setMentionAll(false); setReplyingTo(null); setPendingAttachments([]); } setTimeout(() => { scrollToLatest(false, false, hasPending ? 'send-mixed' : 'send-text'); }, 100); } catch (error) { console.error('发送消息失败:', error); Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试')); } finally { setSending(false); } }, [ inputText, pendingAttachments, conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText, canSendPrivateImage, scrollToLatest, ]); // 选择图片(多选进入输入框待发送,点发送时与文字一并发出) const handlePickImage = useCallback(async () => { if (pendingAttachments.length >= MAX_PENDING_CHAT_IMAGES) { Alert.alert('提示', `最多添加 ${MAX_PENDING_CHAT_IMAGES} 张图片`); setActivePanel('none'); return; } try { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('权限不足', '需要访问相册权限才能选择图片'); return; } const remaining = MAX_PENDING_CHAT_IMAGES - pendingAttachments.length; const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: 'images', allowsEditing: false, quality: 1, allowsMultipleSelection: true, selectionLimit: remaining, }); if (!result.canceled && result.assets && result.assets.length > 0) { const newItems: PendingChatAttachment[] = result.assets.map((asset, i) => ({ id: `${Date.now()}-${i}-${Math.random().toString(36).slice(2, 9)}`, uri: asset.uri, mimeType: asset.mimeType ?? undefined, })); setPendingAttachments(prev => [...prev, ...newItems].slice(0, MAX_PENDING_CHAT_IMAGES)); } } catch (error) { console.error('选择图片失败:', error); Alert.alert('错误', '选择图片失败'); } setActivePanel('none'); }, [pendingAttachments.length]); // 拍摄照片(加入待发送队列) const handleTakePhoto = useCallback(async () => { if (pendingAttachments.length >= MAX_PENDING_CHAT_IMAGES) { Alert.alert('提示', `最多添加 ${MAX_PENDING_CHAT_IMAGES} 张图片`); setActivePanel('none'); return; } try { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== 'granted') { Alert.alert('权限不足', '需要相机权限才能拍摄照片'); return; } const result = await ImagePicker.launchCameraAsync({ mediaTypes: 'images', allowsEditing: false, quality: 0.8, }); if (!result.canceled && result.assets && result.assets.length > 0) { const asset = result.assets[0]; setPendingAttachments(prev => prev.length >= MAX_PENDING_CHAT_IMAGES ? prev : [ ...prev, { id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, uri: asset.uri, mimeType: asset.mimeType ?? undefined, }, ] ); } } catch (error) { console.error('拍摄照片失败:', error); Alert.alert('错误', '拍摄照片失败'); } setActivePanel('none'); }, [pendingAttachments.length]); // 处理更多功能 const handleMoreAction = useCallback((actionId: string) => { switch (actionId) { case 'voice_call': if (!isGroupChat && otherUser?.id && conversationId) { callStore.getState().startCall(conversationId, otherUser.id, { nickname: otherUser.nickname, avatar: otherUser.avatar, }, 'voice'); } setActivePanel('none'); break; case 'video_call': if (!isGroupChat && otherUser?.id && conversationId) { callStore.getState().startCall(conversationId, otherUser.id, { nickname: otherUser.nickname, avatar: otherUser.avatar, }, 'video'); } setActivePanel('none'); break; case 'image': handlePickImage(); break; case 'camera': handleTakePhoto(); break; case 'file': Alert.alert('提示', '文件功能即将上线'); setActivePanel('none'); break; case 'location': Alert.alert('提示', '位置功能即将上线'); setActivePanel('none'); break; default: setActivePanel('none'); } }, [handlePickImage, handleTakePhoto, isGroupChat, otherUser, conversationId]); // 插入表情 const handleInsertEmoji = useCallback((emoji: string) => { setInputText(prev => prev + emoji); }, []); // 发送自定义表情 const handleSendSticker = useCallback(async (stickerUrl: string) => { if (!conversationId) return; if (isGroupChat && isMuted) { if (muteAll) { Alert.alert('无法发送', '当前群组已开启全员禁言'); } else { Alert.alert('无法发送', '你已被管理员禁言'); } return; } if (!isGroupChat && !canSendPrivateImage) { Alert.alert('无法发送', '对方未关注你前,暂不支持发送图片'); return; } setSendingImage(true); try { const segments = buildImageSegments(stickerUrl, stickerUrl, undefined, undefined, replyingTo); if (isGroupChat && routeGroupId) { await messageService.sendMessageByAction('group', conversationId, segments); setReplyingTo(null); } else { // 【新架构】私聊表情通过 MessageManager 发送 await sendMessageViaManager(segments); setReplyingTo(null); } setTimeout(() => { scrollToLatest(false, false, 'send-sticker'); }, 100); } catch (error) { console.error('发送自定义表情失败:', error); Alert.alert('发送失败', '自定义表情发送失败,请重试'); } finally { setSendingImage(false); } }, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, canSendPrivateImage, scrollToLatest]); // 切换表情面板 const toggleEmojiPanel = useCallback(() => { Keyboard.dismiss(); setActivePanel(prev => { const newPanel = prev === 'emoji' ? 'none' : 'emoji'; return newPanel; }); }, []); // 切换更多功能面板 const toggleMorePanel = useCallback(() => { Keyboard.dismiss(); setActivePanel(prev => { const newPanel = prev === 'more' ? 'none' : 'more'; return newPanel; }); }, []); // 关闭面板 const closePanel = useCallback(() => { setActivePanel('none'); }, []); // 撤回消息 - 现在通过 MessageManager 处理 const handleRecall = useCallback(async (messageId: string) => { try { if (isGroupChat && routeGroupId) { await messageService.recallMessage(messageId); } else { await messageService.recallMessage(messageId); } // 不需要手动更新状态,MessageManager 会处理撤回事件 } catch (error) { console.error('撤回消息失败:', error); Alert.alert('撤回失败', '无法撤回消息'); } }, [isGroupChat, routeGroupId, conversationId]); // 长按消息显示操作菜单 const handleLongPressMessage = useCallback((message: GroupMessage, position?: MenuPosition) => { const isSystemNotice = message.is_system_notice || message.category === 'notification'; if (message.status === 'recalled' || isSystemNotice) return; setSelectedMessage(message); setSelectedMessageId(String(message.id)); setMenuPosition(position); setLongPressMenuVisible(true); }, []); // 隐藏长按菜单 const hideLongPressMenu = useCallback(() => { setLongPressMenuVisible(false); setSelectedMessage(null); setSelectedMessageId(null); setMenuPosition(undefined); }, []); // 删除消息 const handleDeleteMessage = useCallback(async (messageId: string) => { try { await messageService.deleteMessage(messageId); // 从本地状态移除(MessageManager 不会自动处理删除) const updatedMessages = messages.filter(m => m.id !== messageId); // 注意:这里我们依赖 MessageManager 的数据,所以需要通过其他方式刷新 // 暂时只删除本地数据库 await deleteMessageFromDb(messageId); } catch (error) { console.error('删除消息失败:', error); Alert.alert('删除失败', '无法删除消息'); } }, [messages]); // 清空会话的所有聊天记录 const handleClearConversation = useCallback(async () => { if (!conversationId) return; try { setLastSeq(0); setFirstSeq(0); setHasMoreHistory(true); await clearConversationMessages(conversationId); // 刷新消息列表 await refreshMessages(); } catch (error) { console.error('清空会话失败:', error); Alert.alert('清空失败', '无法清空聊天记录'); } }, [conversationId, refreshMessages]); // 回复消息 const handleReplyMessage = useCallback((message: GroupMessage) => { setReplyingTo(message); textInputRef.current?.focus(); hideLongPressMenu(); }, [hideLongPressMenu]); // 取消回复 const handleCancelReply = useCallback(() => { setReplyingTo(null); }, []); // 点击头像跳转到用户主页 const handleAvatarPress = useCallback((userId: string) => { if (userId && userId !== currentUserId) { router.push(hrefs.hrefUserProfile(String(userId))); } }, [currentUserId]); // 长按头像@用户 const handleAvatarLongPress = useCallback((senderId: string, nickname: string) => { if (!isGroupChat || senderId === currentUserId) return; if (!selectedMentions.includes(senderId)) { setSelectedMentions(prev => [...prev, senderId]); } const newText = inputText + `@${nickname} `; setInputText(newText); setTimeout(() => { textInputRef.current?.focus(); }, 100); }, [isGroupChat, currentUserId, inputText, selectedMentions]); // 获取正在输入提示文本 - 现在从 MessageManager 获取 const getTypingHint = useCallback((): string | null => { if (groupTypingUsers.length === 0) return null; const typingNames = groupTypingUsers.map(userId => { const member = groupMembers.find(m => m.user_id === userId); return member?.nickname || member?.user?.nickname || '有人'; }); if (typingNames.length === 1) { return `${typingNames[0]} 正在输入...`; } else if (typingNames.length <= 3) { return `${typingNames.join('、')} 正在输入...`; } else { return `${typingNames.slice(0, 3).join('、')}等 ${groupTypingUsers.length} 人正在输入...`; } }, [groupTypingUsers, groupMembers]); // 计算底部面板高度 const getPanelHeight = useCallback(() => { if (activePanel === 'none') return 0; if (activePanel === 'mention') return 250; return 350; }, [activePanel]); // 计算输入框的bottom值 const getInputBottom = useCallback(() => { if (keyboardHeight > 0) return keyboardHeight; if (activePanel !== 'none') return getPanelHeight(); return 0; }, [keyboardHeight, activePanel, getPanelHeight]); // 计算消息列表的底部padding const getListPaddingBottom = useCallback(() => { if (activePanel !== 'none' && keyboardHeight === 0) return getPanelHeight(); return 0; }, [keyboardHeight, activePanel, getPanelHeight]); // 关闭所有面板 const handleDismiss = useCallback(() => { if (activePanel !== 'none') { Keyboard.dismiss(); setActivePanel('none'); } }, [activePanel]); // 导航到群组信息或用户资料 const navigateToInfo = useCallback(() => { if (isGroupChat && routeGroupId) { router.push(hrefs.hrefGroupInfo(routeGroupId, conversationId || undefined)); } else if (otherUser?.id) { router.push(hrefs.hrefUserProfile(String(otherUser.id))); } }, [isGroupChat, routeGroupId, otherUser, conversationId]); // 导航到聊天管理页面 const navigateToChatSettings = useCallback(() => { if (isGroupChat && routeGroupId) { router.push(hrefs.hrefGroupInfo(routeGroupId, conversationId || undefined)); } else if (otherUser?.id && conversationId) { router.push( hrefs.hrefPrivateChatInfo({ conversationId, userId: String(otherUser.id), userName: otherUser.nickname, userAvatar: otherUser.avatar, }) ); } }, [isGroupChat, routeGroupId, otherUser, conversationId]); return { // 状态 messages, conversationId, inputText, otherUser, currentUser, currentUserId, keyboardHeight, loading, sending, activePanel, sendingImage, uploadingAttachments, pendingAttachments, isComposerBusy, replyingTo, longPressMenuVisible, selectedMessage, selectedMessageId, setSelectedMessageId, menuPosition, isGroupChat, groupInfo, groupMembers, // 【改造】使用 MessageManager 的输入状态 typingUsers: groupTypingUsers, currentUserRole, mentionQuery, selectedMentions, isMuted, muteAll, followRestrictionHint, canSendPrivateImage, routeGroupId, routeGroupName, otherUserLastReadSeq, messageMap, loadingMore, hasMoreHistory, // Refs flatListRef, textInputRef, scrollPositionRef, // 方法 formatTime, shouldShowTime, handleInputChange, handleSend, handlePickImage, removePendingAttachment, handleTakePhoto, handleMoreAction, handleInsertEmoji, handleSendSticker, toggleEmojiPanel, toggleMorePanel, closePanel, handleRecall, handleLongPressMessage, hideLongPressMenu, handleDeleteMessage, handleReplyMessage, handleCancelReply, handleAvatarPress, handleAvatarLongPress, handleSelectMention, handleMentionAll, getSenderInfo, getTypingHint, getPanelHeight, getInputBottom, getListPaddingBottom, handleDismiss, navigateToInfo, navigateToChatSettings, loadMoreHistory, handleClearConversation, handleMessageListContentSizeChange, handleReachLatestEdge, jumpToLatestMessages, setBrowsingHistory, }; };