/** * 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 { formatChatTime } from '@/utils/formatTime'; import * as ImagePicker from 'expo-image-picker'; import * as DocumentPicker from 'expo-document-picker'; import { Linking } from 'react-native'; import { GroupMemberResponse, MessageSegment, TextSegmentData, ImageSegmentData, FileSegmentData, AtSegmentData, ReplySegmentData, MessageStatus } from '../../../../types/dto'; import { messageService } from '@/services/message'; import { uploadService } from '@/services/upload'; import { ApiError } from '@/services/core'; // 【新架构】使用 MessageManager import { useChat, useGroupTyping, useGroupMuted, messageManager, callStore } from '../../../../stores'; import { useMessageStore } from '../../../../stores/message'; import { groupService } from '@/services/message'; import { userManager } from '../../../../stores/user'; import { groupManager } from '../../../../stores/group'; import * as hrefs from '../../../../navigation/hrefs'; import { firstRouteParam } from '../../../../navigation/paramUtils'; import { GroupMessage, PanelType, UserRole, SenderInfo, ChatRouteParams, MenuPosition, PendingChatAttachment, ChatScreenProps, } from './types'; import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants'; import { messageRepository } from '@/database'; import { useReplyMessage } from './useReplyMessage'; interface MentionRange { start: number; end: number; userId: string; } function getMentionRanges( text: string, selectedMentions: string[], mentionAll: boolean, groupMembers: GroupMemberResponse[] ): MentionRange[] { const ranges: MentionRange[] = []; if (mentionAll) { const pattern = /@所有人\s*/g; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { ranges.push({ start: match.index, end: match.index + match[0].length, userId: 'all' }); } } for (const userId of selectedMentions) { const member = groupMembers.find(m => m.user_id === userId); const nickname = member?.nickname || member?.user?.nickname; if (!nickname) continue; const escapedName = nickname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp(`@${escapedName}\\s*`, 'g'); let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { ranges.push({ start: match.index, end: match.index + match[0].length, userId }); } } ranges.sort((a, b) => a.start - b.start); return ranges; } export const useChatScreen = (props?: ChatScreenProps) => { 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[]; groupAvatar?: string | string[]; scrollToSeq?: string | string[]; }>(); // 路由参数(动态段 + query);群 ID 勿用 Number(),避免超过 2^53-1 时精度丢失 // 嵌入式模式下优先使用 props,否则从路由参数获取 const routeParams = useMemo(() => { if (props?.embeddedConversationId) { return { conversationId: props.embeddedConversationId, userId: null as string | null, isGroupChat: props.embeddedIsGroupChat ?? false, groupId: props.embeddedGroupId ?? null, groupName: props.embeddedGroupName ?? null, groupAvatar: null as string | null, }; } const isGroupFlag = firstRouteParam(rawParams.isGroupChat); const scrollToSeqParam = firstRouteParam(rawParams.scrollToSeq); return { conversationId: firstRouteParam(rawParams.conversationId) ?? null, userId: firstRouteParam(rawParams.userId) ?? null, isGroupChat: isGroupFlag === '1' || isGroupFlag === 'true', groupId: firstRouteParam(rawParams.groupId), groupName: firstRouteParam(rawParams.groupName), groupAvatar: firstRouteParam(rawParams.groupAvatar), scrollToSeq: scrollToSeqParam ? Number(scrollToSeqParam) : null, }; }, [ props?.embeddedConversationId, props?.embeddedIsGroupChat, props?.embeddedGroupId, props?.embeddedGroupName, rawParams.conversationId, rawParams.userId, rawParams.isGroupChat, rawParams.groupId, rawParams.groupName, rawParams.groupAvatar, rawParams.scrollToSeq, ]); const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName, groupAvatar: routeGroupAvatar, scrollToSeq: routeScrollToSeq } = routeParams; // 当前会话ID(可能在新建私聊会话后动态更新) const [conversationId, setConversationId] = useState(routeConversationId); // 【新架构】使用 MessageManager 的 hook 获取所有数据 const { messages: messageManagerMessages, isLoadingMessages, hasMoreMessages, loadMoreMessages, refreshMessages, sendMessage: sendMessageViaManager, retrySendMessage: retrySendMessageViaManager, isSending: isSendingViaManager, markAsRead, conversation, } = useChat(conversationId); // 统一入口:优先路由参数,其次从 conversation 对象提取(JPUSH 进入时 conversation 异步加载) const effectiveGroupId = routeGroupId || (isGroupChat && conversation?.group?.id) || null; const effectiveGroupName = routeGroupName || (isGroupChat && conversation?.group?.name) || null; const effectiveGroupAvatar = routeGroupAvatar || (isGroupChat && conversation?.group?.avatar) || null; // 本地状态 const [currentUserId, setCurrentUserId] = useState(''); // 【新架构】使用 MessageManager 获取群聊输入状态和禁言状态 const { typingUsers: groupTypingUsers } = useGroupTyping(effectiveGroupId ? String(effectiveGroupId) : null); const { isMuted: isMutedFromManager, setMuted: setMutedStatus } = useGroupMuted( effectiveGroupId ? String(effectiveGroupId) : null, currentUserId ); // 本地状态 const [inputText, setInputText] = useState(''); const inputTextRef = useRef(inputText); useEffect(() => { inputTextRef.current = inputText; }, [inputText]); 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 [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 prevLatestSeqRef = useRef(0); const prevMarkedReadSeqRef = useRef(0); const enterMarkedKeyRef = useRef(''); const suppressAutoFollowRef = useRef(false); const isBrowsingHistoryRef = useRef(false); const hasShownMessageListRef = useRef(false); const historyLoadingLockUntilRef = useRef(0); const scrollToSeqRef = useRef(null); // 回复消息状态 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; avatar?: string | null } | null>(null); const [groupMembers, setGroupMembers] = useState([]); // 群名/头像:优先使用路由参数(JPUSH extras),其次从 conversation 对象获取 useEffect(() => { if (!isGroupChat) return; const name = effectiveGroupName; const avatar = effectiveGroupAvatar; if (name || avatar) { setGroupInfo(prev => prev ? prev : { name: name || undefined, avatar: avatar || null }); } }, [isGroupChat, effectiveGroupName, effectiveGroupAvatar]); const [currentUserRole, setCurrentUserRole] = useState('member'); const [mentionQuery, setMentionQuery] = useState(''); const [selectedMentions, setSelectedMentions] = useState([]); const [mentionAll, setMentionAll] = useState(false); const prevInputTextRef = useRef(''); 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.sender, is_system_notice: m.is_system_notice, notice_content: m.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]); // 加载态语义修正: // 首屏加载:尚未展示过消息列表 且 无消息 → 显示 loading(避免 FlashList 以空 data 挂载后 inverted 布局异常) // 一旦有消息或加载完成(isLoadingMessages 归 false),即解除 loading useEffect(() => { if (messageManagerMessages.length > 0) { hasShownMessageListRef.current = true; } const shouldShowInitialLoading = !hasShownMessageListRef.current && messageManagerMessages.length === 0 && isLoadingMessages; 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.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.is_following_me === 'boolean') { setIsFollowedByOther(detail.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; prevLatestSeqRef.current = 0; prevMarkedReadSeqRef.current = 0; enterMarkedKeyRef.current = ''; suppressAutoFollowRef.current = false; isBrowsingHistoryRef.current = false; hasShownMessageListRef.current = false; historyLoadingLockUntilRef.current = 0; scrollToSeqRef.current = routeScrollToSeq ?? null; 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 flatListRef.current?.scrollToOffset({ offset: 0, animated, }); }, [flatListRef, isHistoryLoadingLocked]); const isNearBottom = useCallback(() => { const scrollY = scrollPositionRef.current.scrollY || 0; // inverted 列表下,越接近 0 越靠近最新消息端 return scrollY <= 100; }, [scrollPositionRef]); // 首屏加载完成后仅锚定一次到最新消息端(有 scrollToSeq 时跳过) // 注意:必须在消息首次出现时立刻把 hasInitialAnchorDoneRef 置为 true, // 不能再依赖 viewportHeight 等通过 ref 同步的值——否则后续首次 loadMoreHistory // 让 messages.length 变化时此 effect 会被再度命中,并以 force=true 调用 // scrollToLatest 强行把列表拉回最底端,造成"上滑首次加载更多就回底"的问题。 useEffect(() => { if ( loading || loadingMore || messages.length === 0 || hasInitialAnchorDoneRef.current ) { return; } hasInitialAnchorDoneRef.current = true; if (scrollToSeqRef.current != null) return; // 由下方 scrollToSeq effect 处理 // inverted FlashList 天然从 offset=0(最新端)开始; // 这里仅做一次保险锚定,即使布局尚未就绪也不会有副作用。 const timer = setTimeout(() => { scrollToLatest(false, true, 'initial-anchor'); }, 0); return () => clearTimeout(timer); }, [loading, loadingMore, messages.length, scrollToLatest]); // 新消息跟随策略(Telegram/QQ 风格): // 仅当"最新端消息 seq 增长"且用户在底部附近时才跟随; // 历史加载只会增加旧消息,不会提升 latest seq,因此不会触发回底。 useEffect(() => { const latestSeq = messages.length > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0; const prevLatestSeq = prevLatestSeqRef.current; prevLatestSeqRef.current = latestSeq; if (loading || loadingMore) return; if (latestSeq <= prevLatestSeq) return; if (suppressAutoFollowRef.current || isBrowsingHistoryRef.current || isHistoryLoadingLocked()) return; if (!isNearBottom()) return; const timer = setTimeout(() => { scrollToLatest(false, false, 'new-message-follow'); }, 0); return () => clearTimeout(timer); }, [messages, loading, loadingMore, isNearBottom, scrollToLatest, isHistoryLoadingLocked]); // 获取当前用户信息 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 || !effectiveGroupId) return; const fetchGroupInfo = async () => { try { const group = await groupManager.getGroup(effectiveGroupId); setGroupInfo(group); const membersResponse = await groupManager.getMembers(effectiveGroupId, 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(effectiveGroupId); 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, effectiveGroupId, 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 + maintainVisibleContentPosition 由列表原生保持位置) const loadMoreHistory = useCallback(async () => { if (!conversationId || !hasMoreHistory || loadingMore) { return; } // 历史加载期间禁止"新消息自动跟随到底" suppressAutoFollowRef.current = true; isBrowsingHistoryRef.current = true; historyLoadingLockUntilRef.current = Date.now() + 3000; setLoadingMore(true); try { await loadMoreMessages(); } catch (error) { console.error('加载历史消息失败:', error); } finally { setLoadingMore(false); // 加载结束后继续保留短暂锁窗,避免布局结算阶段误触自动回底 historyLoadingLockUntilRef.current = Date.now() + 800; } }, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages]); // 从搜索结果跳转:滚动到目标 seq useEffect(() => { const targetSeq = scrollToSeqRef.current; if (targetSeq == null || !hasInitialAnchorDoneRef.current) return; if (loading || loadingMore) return; const found = messages.find(m => m.seq === targetSeq); if (found) { scrollToSeqRef.current = null; isBrowsingHistoryRef.current = true; suppressAutoFollowRef.current = true; const ascendingIndex = messages.findIndex(m => m.seq === targetSeq); const displayIndex = messages.length - 1 - ascendingIndex; setTimeout(() => { try { flatListRef.current?.scrollToIndex({ index: displayIndex, animated: false, viewPosition: 0.5, }); } catch { // FlashList 远距离跳转可能失败,降级到 offset 滚动 flatListRef.current?.scrollToOffset?.({ offset: 0, animated: false }); } }, 50); return; } if (hasMoreHistory) { loadMoreHistory(); } else { scrollToSeqRef.current = null; } }, [loading, loadingMore, messages, hasMoreHistory, loadMoreHistory]); /** * 跳转到指定 seq 的消息(供引用消息点击跳转复用 scrollToSeq 范式)。 * 设置目标 seq 后由上方 effect 接管:内存命中则 scrollToIndex, * 未命中则循环 loadMoreHistory 直到目标进入视窗或历史耗尽。 * @returns 是否已受理跳转(false 表示无更多历史且不在内存,无法定位) */ const jumpToMessageSeq = useCallback((seq: number): boolean => { if (!hasInitialAnchorDoneRef.current) return false; if (!Number.isFinite(seq) || seq <= 0) return false; scrollToSeqRef.current = seq; isBrowsingHistoryRef.current = true; suppressAutoFollowRef.current = true; // 目标已在内存:直接定位(effect 依赖未变不会自动重跑,这里同步处理命中情况) const ascendingIndex = messages.findIndex(m => m.seq === seq); if (ascendingIndex >= 0) { scrollToSeqRef.current = null; const displayIndex = messages.length - 1 - ascendingIndex; setTimeout(() => { try { flatListRef.current?.scrollToIndex({ index: displayIndex, animated: true, viewPosition: 0.5 }); } catch { flatListRef.current?.scrollToOffset?.({ offset: 0, animated: true }); } }, 50); return true; } // 目标不在内存:kickstart 历史加载循环,由 scrollToSeq effect 接管后续 if (hasMoreHistory) { loadMoreHistory(); return true; } // 无更多历史且不在内存,无法定位 scrollToSeqRef.current = null; return false; }, [messages, hasMoreHistory, loadMoreHistory, flatListRef]); // 列表内容尺寸变化:仅同步内容高度,锚点补偿由 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; // FlashList 在 inverted 模式下 scrollToOffset({ offset: 0 }) 可能无法真正滚到最底部, // 因为列表内容高度变化后最小 offset 可能不是 0。先尝试滚到负值确保到底,再补偿回 0。 flatListRef.current?.scrollToOffset({ offset: -99999, animated: false }); requestAnimationFrame(() => { flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); }); }, [flatListRef]); const setBrowsingHistory = useCallback((browsing: boolean) => { isBrowsingHistoryRef.current = browsing; }, []); /** * 无条件清除浏览历史锁与自动跟随抑制(动量滚动结束、layout settle、 * 轻触回到底部等场景使用)。区别于 handleReachLatestEdge,后者受加载锁保护。 */ const clearHistoryLock = useCallback(() => { if (isBrowsingHistoryRef.current || suppressAutoFollowRef.current) { isBrowsingHistoryRef.current = false; suppressAutoFollowRef.current = false; } }, []); // 进入聊天详情后立即清未读(不依赖滚动位置) useEffect(() => { if (!conversationId) return; if (!conversation) return; const unreadCount = Number(conversation.unread_count || 0); if (unreadCount <= 0) return; const latestFromConversation = Number(conversation.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。 // 注意:不能复用 prevLatestSeqRef 做判定——它被新消息跟随 effect 无条件推进, // 会导致这里的去重判断永远成立,标记已读逻辑事实上不会执行。 // 这里只依赖专属的 prevMarkedReadSeqRef 做去重。 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; // 避免对同一 seq 重复标记 if (latestSeq <= prevMarkedReadSeqRef.current) return; prevMarkedReadSeqRef.current = latestSeq; markAsRead(latestSeq).catch(error => { console.error('[ChatScreen] 自动标记已读失败:', error); }); }, [messages, conversationId, markAsRead, loading, loadingMore, isNearBottom]); // 监听键盘事件 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 => { return formatChatTime(dateString); }, []); // 检查是否需要显示时间分隔 const shouldShowTime = useCallback((index: number): boolean => { if (index === 0) return true; const currentMsg = messages[index]; const prevMsg = messages[index - 1]; const currentDate = new Date(currentMsg.created_at); const prevDate = new Date(prevMsg.created_at); if (Number.isNaN(currentDate.getTime()) || Number.isNaN(prevDate.getTime())) return false; return (currentDate.getTime() - prevDate.getTime()) > TIME_SEPARATOR_INTERVAL; }, [messages]); // 处理输入变化,检测@符号及 @mention 整体删除 const handleInputChange = useCallback((text: string) => { const prevText = prevInputTextRef.current; // 检测退格导致 @mention 被部分删除 → 整体补删(setNativeProps 立即生效无卡顿) if (text.length < prevText.length && isGroupChat) { let deleteStart = -1; for (let i = 0; i < prevText.length; i++) { if (prevText[i] !== text[i]) { deleteStart = i; break; } } if (deleteStart === -1) { deleteStart = text.length; } const deleteEnd = deleteStart + (prevText.length - text.length); const mentionRanges = getMentionRanges(prevText, selectedMentions, mentionAll, groupMembers); for (const range of mentionRanges) { if (deleteStart < range.end && deleteEnd > range.start) { const correctedText = prevText.slice(0, range.start) + prevText.slice(range.end); prevInputTextRef.current = correctedText; // 立即同步到原生层,避免二次渲染卡顿 textInputRef.current?.setNativeProps({ text: correctedText }); setInputText(correctedText); setSelectedMentions(prev => prev.filter(userId => { const member = groupMembers.find(m => m.user_id === userId); const nickname = member?.nickname || member?.user?.nickname; return nickname && correctedText.includes(`@${nickname}`); })); if (mentionAll && !correctedText.includes('@所有人')) { setMentionAll(false); } const lastAtIndex = correctedText.lastIndexOf('@'); if (lastAtIndex !== -1) { const textAfterAt = correctedText.slice(lastAtIndex + 1); if (!textAfterAt.includes(' ')) { setMentionQuery(textAfterAt.toLowerCase()); setActivePanel('mention'); } else { setActivePanel('none'); } } else { setActivePanel('none'); } return; } } } prevInputTextRef.current = text; 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'); } if (selectedMentions.length > 0) { const stillPresent = selectedMentions.filter(userId => { const member = groupMembers.find(m => m.user_id === userId); const nickname = member?.nickname || member?.user?.nickname; if (!nickname) return false; const escapedName = nickname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`@${escapedName}(\\s|$)`, 'g').test(text); }); if (stillPresent.length !== selectedMentions.length) { setSelectedMentions(stillPresent); } } if (mentionAll && !text.includes('@所有人')) { setMentionAll(false); } } }, [isGroupChat, selectedMentions, mentionAll, groupMembers]); // 【改造】获取发送者信息(群聊)- 优先从消息的 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 { getCached: getCachedReply, ensureReplyMessage } = useReplyMessage(); // 选择@用户 const handleSelectMention = useCallback((member: { user_id: string; nickname?: string; user?: { nickname?: string } }) => { const currentText = inputTextRef.current; const lastAtIndex = currentText.lastIndexOf('@'); if (lastAtIndex !== -1) { const textBeforeAt = currentText.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); }, [selectedMentions]); // @所有人 const handleMentionAll = useCallback(() => { const currentText = inputTextRef.current; const lastAtIndex = currentText.lastIndexOf('@'); if (lastAtIndex !== -1) { const textBeforeAt = currentText.slice(0, lastAtIndex); const newText = `${textBeforeAt}@所有人 `; setInputText(newText); setMentionAll(true); } setActivePanel('none'); setMentionQuery(''); setTimeout(() => { textInputRef.current?.focus(); }, 100); }, []); /** * 构建文本消息的 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 }); } // 收集所有 @ 提及在文本中的位置 const mentionPositions: { start: number; end: number; userId: string }[] = []; if (mentionAll) { const pattern = /@所有人\s*/g; let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { mentionPositions.push({ start: match.index, end: match.index + match[0].length, userId: 'all' }); } } 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, '\\$&'); const pattern = new RegExp(`@${escapedName}\\s*`, 'g'); let match: RegExpExecArray | null; while ((match = pattern.exec(text)) !== null) { mentionPositions.push({ start: match.index, end: match.index + match[0].length, userId }); } }); } mentionPositions.sort((a, b) => a.start - b.start); const seen = new Set(); const uniquePositions = mentionPositions.filter(p => { if (seen.has(p.start)) return false; seen.add(p.start); return true; }); let cursor = 0; for (const pos of uniquePositions) { if (pos.start >= pos.end) continue; if (pos.start > cursor) { const plainText = text.slice(cursor, pos.start); if (plainText) { segments.push({ type: 'text', data: { text: plainText } as TextSegmentData }); } } segments.push({ type: 'at', data: { user_id: pos.userId } as AtSegmentData }); cursor = Math.max(cursor, pos.end); } if (cursor < text.length) { const remaining = text.slice(cursor); if (remaining) { segments.push({ type: 'text', data: { text: remaining } as TextSegmentData }); } } if (segments.length === 0 || (segments.length === 1 && segments[0].type === 'reply')) { segments.push({ type: 'text', data: { text: '' } 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; }, []); /** * 构建文件消息的 segments 数组 */ const buildFileSegments = useCallback(( fileUrl: string, name: string, size?: number, mimeType?: string, 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: 'file', data: { url: fileUrl, name, size, mime_type: mimeType, } as FileSegmentData }); 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; } } // 图片需要先上传(这个无法乐观,因为需要URL) 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); } } // 构建消息段 const segments: MessageSegment[] = (trimmedText || replyingTo) ? [...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; } // 乐观更新:立即清空输入框并滚动到底部 const savedReplyTo = replyingTo; setInputText(''); setSelectedMentions([]); setMentionAll(false); setReplyingTo(null); setPendingAttachments([]); // 立即滚动到底部显示新消息 setTimeout(() => { scrollToLatest(false, false, hasPending ? 'send-mixed' : 'send-text'); }, 50); // 后台发送消息(私聊与群聊统一走 MessageSendService 的乐观更新): // 1. 立即在 store 中插入 pending 临时消息 // 2. 成功 → 替换为正式消息、更新会话 last_message、写入本地数据库 // 3. 失败 → 标记为 failed,供用户点击重试 try { await sendMessageViaManager(segments, { replyToId: savedReplyTo?.id, detailType: isGroupChat ? 'group' : 'private', }); } catch (error) { console.error('发送消息失败:', error); // MessageSendService 已将消息置为 failed 并在 UI 上显示状态, // 这里仅弹窗提示用户(与原行为保持一致)。 if (isGroupChat) { Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试')); } } }, [ inputText, pendingAttachments, conversationId, isGroupChat, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText, canSendPrivateImage, scrollToLatest, ]); // 重试发送失败的消息(点击「发送失败」气泡时触发) // 仅对当前用户自己发送且状态为 failed 的消息生效; // 重试期间消息会先变为 pending(发送中),再根据结果转为 normal/failed。 const handleRetrySend = useCallback(async (message: GroupMessage) => { if (!conversationId) return; if (message.sender_id !== currentUserId) return; if (message.status !== 'failed') return; try { await retrySendMessageViaManager(message); } catch (error) { console.error('重试发送消息失败:', error); Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试')); } }, [conversationId, currentUserId, retrySendMessageViaManager, getSendErrorMessage]); // 选择图片(多选进入输入框待发送,点发送时与文字一并发出) 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 handlePickFile = useCallback(async () => { if (!conversationId) { setActivePanel('none'); return; } if (isGroupChat && isMuted) { Alert.alert('无法发送', muteAll ? '当前群组已开启全员禁言' : '你已被管理员禁言'); setActivePanel('none'); return; } setActivePanel('none'); try { const result = await DocumentPicker.getDocumentAsync({ multiple: false, copyToCacheDirectory: true, }); if (result.canceled || !result.assets || result.assets.length === 0) { return; } const asset = result.assets[0]; setUploadingAttachments(true); const uploaded = await uploadService.uploadFile( { uri: asset.uri, name: asset.name, type: asset.mimeType || undefined, }, 'chat' ); if (!uploaded?.url) { Alert.alert('上传失败', getSendErrorMessage(null, '文件上传失败,请重试')); return; } const segments = buildFileSegments( uploaded.url, // asset.name 是 DocumentPicker 返回的用户原始文件名(真实名), // 优先使用它;uploaded.name 为后端回传的展示名(已与 asset.name 一致)。 asset.name || uploaded.name || 'file', uploaded.size ?? asset.size, uploaded.mime_type ?? asset.mimeType, replyingTo, ); setSending(true); try { if (isGroupChat && effectiveGroupId) { await messageService.sendMessageByAction('group', conversationId, segments); } else { await sendMessageViaManager(segments); } setReplyingTo(null); setTimeout(() => scrollToLatest(false, false, 'send-file'), 100); } catch (error) { console.error('发送文件消息失败:', error); Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试')); } finally { setSending(false); } } catch (error) { console.error('选择文件失败:', error); Alert.alert('错误', getSendErrorMessage(error, '选择文件失败')); } finally { setUploadingAttachments(false); } }, [ conversationId, isGroupChat, isMuted, muteAll, effectiveGroupId, replyingTo, buildFileSegments, sendMessageViaManager, scrollToLatest, getSendErrorMessage, ]); // 处理更多功能 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': handlePickFile(); break; default: setActivePanel('none'); } }, [handlePickImage, handleTakePhoto, handlePickFile, 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 && effectiveGroupId) { 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, effectiveGroupId, 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 处理(撤回事件由 MessageManager 同步状态) const handleRecall = useCallback(async (messageId: string) => { try { await messageService.recallMessage(messageId); } catch (error) { console.error('撤回消息失败:', error); Alert.alert('撤回失败', '无法撤回消息'); } }, []); // 长按消息显示操作菜单 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); await messageRepository.delete(messageId); if (conversationId) { useMessageStore.getState().removeMessage(conversationId, messageId); } } catch (error) { console.error('删除消息失败:', error); Alert.alert('删除失败', '无法删除消息'); } }, [conversationId]); // 清空会话的所有聊天记录 const handleClearConversation = useCallback(async () => { if (!conversationId) return; try { setHasMoreHistory(true); await messageRepository.clearConversation(conversationId); // 同步清空内存消息 + hydrated 标志,保证内存/DB/hydrated 三者一致。 // 否则 hydrated 残留会让 refreshMessages 走轻量增量同步,清空后仍显示旧消息。 useMessageStore.getState().clearMessages(conversationId); // 刷新消息列表(hydrated 已清,会重新走完整 hydration 管线) 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 currentText = inputTextRef.current; setInputText(currentText + `@${nickname} `); setTimeout(() => { textInputRef.current?.focus(); }, 100); }, [isGroupChat, currentUserId, 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 handleDismiss = useCallback(() => { Keyboard.dismiss(); if (activePanel !== 'none') { setActivePanel('none'); } }, [activePanel]); // 导航到群组信息或用户资料 const navigateToInfo = useCallback(() => { if (isGroupChat && effectiveGroupId) { router.push(hrefs.hrefGroupInfo(effectiveGroupId, conversationId || undefined)); } else if (otherUser?.id) { router.push(hrefs.hrefUserProfile(String(otherUser.id))); } }, [isGroupChat, effectiveGroupId, otherUser, conversationId]); // 导航到聊天管理页面 const navigateToChatSettings = useCallback(() => { if (isGroupChat && effectiveGroupId) { router.push(hrefs.hrefGroupInfo(effectiveGroupId, conversationId || undefined)); } else if (otherUser?.id && conversationId) { router.push( hrefs.hrefPrivateChatInfo({ conversationId, userId: String(otherUser.id), userName: otherUser.nickname, userAvatar: otherUser.avatar, }) ); } }, [isGroupChat, effectiveGroupId, otherUser, conversationId]); return { // 状态 messages, conversationId, inputText, otherUser, currentUser, currentUserId, keyboardHeight, loading, activePanel, sendingImage, uploadingAttachments, pendingAttachments, isComposerBusy, replyingTo, longPressMenuVisible, selectedMessage, selectedMessageId, menuPosition, isGroupChat, groupInfo, groupMembers, currentUserRole, mentionQuery, isMuted, muteAll, followRestrictionHint, canSendPrivateImage, effectiveGroupId, effectiveGroupName, otherUserLastReadSeq, messageMap, // 引用消息回填(会话级缓存:内存 → 本地 SQLite → 服务端) getCachedReply, ensureReplyMessage, jumpToMessageSeq, loadingMore, hasMoreHistory, // Refs flatListRef, textInputRef, scrollPositionRef, // 方法 formatTime, shouldShowTime, handleInputChange, handleSend, handleRetrySend, removePendingAttachment, handleMoreAction, handleInsertEmoji, handleSendSticker, toggleEmojiPanel, toggleMorePanel, closePanel, handleRecall, handleLongPressMessage, hideLongPressMenu, handleDeleteMessage, handleReplyMessage, handleCancelReply, handleAvatarPress, handleAvatarLongPress, handleSelectMention, handleMentionAll, getSenderInfo, getTypingHint, handleDismiss, navigateToInfo, navigateToChatSettings, loadMoreHistory, handleClearConversation, handleMessageListContentSizeChange, handleReachLatestEdge, jumpToLatestMessages, setBrowsingHistory, clearHistoryLock, }; };