Files
frontend/src/screens/message/components/ChatScreen/useChatScreen.ts

1283 lines
42 KiB
TypeScript
Raw Normal View History

/**
* 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 { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
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 } from '../../../../stores';
import { groupService } from '../../../../services/groupService';
import { userManager } from '../../../../stores/userManager';
import { groupManager } from '../../../../stores/groupManager';
import { RootStackParamList } from '../../../../navigation/types';
import { navigationService } from '../../../../infrastructure/navigation/navigationService';
import {
GroupMessage,
PanelType,
UserRole,
SenderInfo,
ChatRouteParams,
MenuPosition,
} from './types';
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE } from './constants';
import {
deleteMessage as deleteMessageFromDb,
clearConversationMessages,
} from '../../../../services/database';
type ChatRouteProp = RouteProp<RootStackParamList, 'Chat'>;
export const useChatScreen = () => {
const getSendErrorMessage = useCallback((error: unknown, fallback: string) => {
if (error instanceof ApiError && error.message) {
return error.message;
}
return fallback;
}, []);
const route = useRoute<ChatRouteProp>();
const navigation = useNavigation();
// 路由参数
const routeParams = useMemo(() => ({
conversationId: route.params?.conversationId || null,
userId: route.params?.userId || null,
isGroupChat: route.params?.isGroupChat || false,
groupId: route.params?.groupId,
groupName: route.params?.groupName,
}), [route.params?.conversationId, route.params?.userId, route.params?.isGroupChat, route.params?.groupId, route.params?.groupName]);
const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName } = routeParams;
// 当前会话ID可能在新建私聊会话后动态更新
const [conversationId, setConversationId] = useState<string | null>(routeConversationId);
// 【新架构】使用 MessageManager 的 hook 获取所有数据
const {
messages: messageManagerMessages,
isLoadingMessages,
hasMoreMessages,
loadMoreMessages,
refreshMessages,
sendMessage: sendMessageViaManager,
isSending: isSendingViaManager,
markAsRead,
conversation,
} = useChat(conversationId);
// 本地状态
const [currentUserId, setCurrentUserId] = useState<string>('');
// 【新架构】使用 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<boolean | null>(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<number>(0);
const [firstSeq, setFirstSeq] = useState<number>(0);
const [otherUserLastReadSeq, setOtherUserLastReadSeq] = useState<number>(0);
const [activePanel, setActivePanel] = useState<PanelType>('none');
const [sendingImage, setSendingImage] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMoreHistory, setHasMoreHistory] = useState(true);
const flatListRef = useRef<any>(null);
const textInputRef = useRef<any>(null);
// 滚动状态机 refsTelegram/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 isProgrammaticScrollRef = useRef(false);
const suppressAutoFollowRef = useRef(false);
const isBrowsingHistoryRef = useRef(false);
// 回复消息状态
const [replyingTo, setReplyingTo] = useState<GroupMessage | null>(null);
// 长按菜单状态
const [longPressMenuVisible, setLongPressMenuVisible] = useState(false);
const [selectedMessage, setSelectedMessage] = useState<GroupMessage | null>(null);
const [menuPosition, setMenuPosition] = useState<MenuPosition | undefined>(undefined);
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
// 群聊相关状态
const [groupInfo, setGroupInfo] = useState<{ name?: string; member_count?: number } | null>(null);
const [groupMembers, setGroupMembers] = useState<GroupMemberResponse[]>([]);
const [currentUserRole, setCurrentUserRole] = useState<UserRole>('member');
const [mentionQuery, setMentionQuery] = useState<string>('');
const [selectedMentions, setSelectedMentions] = useState<string[]>([]);
const [mentionAll, setMentionAll] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [muteAll, setMuteAll] = useState(false);
// 路由会话ID变化时同步到本地会话状态确保订阅会随会话切换
useEffect(() => {
setConversationId(routeConversationId);
}, [routeConversationId]);
// 【改造】直接使用 MessageManager 返回的消息
const messages = useMemo<GroupMessage[]>(() => {
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(() => {
setLoading(isLoadingMessages && messageManagerMessages.length === 0);
}, [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;
suppressAutoFollowRef.current = false;
isBrowsingHistoryRef.current = false;
}, [conversationId]);
const scrollToLatest = useCallback((animated: boolean, force: boolean = false, reason: string = 'unknown') => {
if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current)) return;
// inverted 列表下,最新消息端对应 offset=0
isProgrammaticScrollRef.current = true;
flatListRef.current?.scrollToOffset({
offset: 0,
animated,
});
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, animated ? 220 : 32);
}, [flatListRef, scrollPositionRef, messages.length]);
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) {
console.error('获取群组信息失败:', error);
Alert.alert('错误', '无法获取群组信息');
}
};
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;
// 保存加载前的滚动位置和内容高度
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);
}
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
// 列表内容尺寸变化:仅同步内容高度,锚点补偿由 loadMoreHistory 统一处理
const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
scrollPositionRef.current.contentHeight = contentHeight;
}, []);
const handleReachLatestEdge = useCallback(() => {
suppressAutoFollowRef.current = false;
isBrowsingHistoryRef.current = false;
}, []);
const setBrowsingHistory = useCallback((browsing: boolean) => {
isBrowsingHistoryRef.current = browsing;
}, []);
// 自动标记已读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');
if (isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'keyboard-show');
}, 150);
}
};
const keyboardWillHide = () => {
setKeyboardHeight(0);
if (isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'keyboard-hide');
}, 100);
}
};
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();
};
}, [scrollToLatest, isNearBottom]);
// 格式化时间
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<string, GroupMessage>();
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();
if (!trimmedText || !conversationId) return;
if (isGroupChat && isMuted) {
if (muteAll) {
Alert.alert('无法发送', '当前群组已开启全员禁言');
} else {
Alert.alert('无法发送', '你已被管理员禁言');
}
return;
}
if (!isGroupChat && !canSendFirstPrivateText) {
Alert.alert('无法发送', '对方未关注你前,仅允许发送一条消息');
return;
}
setSending(true);
try {
const segments = buildTextSegments(trimmedText, replyingTo);
if (isGroupChat && routeGroupId) {
await messageService.sendMessageByAction('group', conversationId, segments);
setInputText('');
setSelectedMentions([]);
setMentionAll(false);
setReplyingTo(null);
} else {
// 【新架构】私聊消息通过 MessageManager 发送
await sendMessageViaManager(segments);
setInputText('');
setReplyingTo(null);
}
setTimeout(() => {
scrollToLatest(false, false, 'send-text');
}, 100);
} catch (error) {
console.error('发送消息失败:', error);
Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试'));
} finally {
setSending(false);
}
}, [inputText, conversationId, isGroupChat, routeGroupId, selectedMentions, mentionAll, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText, scrollToLatest]);
// 发送图片消息
const handleSendImage = useCallback(async (imageUri: string, mimeType?: 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 uploadResult = await uploadService.uploadChatImage({ uri: imageUri, type: mimeType });
if (!uploadResult) {
Alert.alert('上传失败', '图片上传失败,请重试');
return;
}
const segments = buildImageSegments(uploadResult.url, uploadResult.url, 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-image');
}, 100);
} catch (error) {
console.error('发送图片失败:', error);
Alert.alert('发送失败', getSendErrorMessage(error, '图片发送失败,请重试'));
} finally {
setSendingImage(false);
}
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, getSendErrorMessage, canSendPrivateImage, scrollToLatest]);
// 选择图片
const handlePickImage = useCallback(async () => {
try {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('权限不足', '需要访问相册权限才能选择图片');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: false,
quality: 1,
allowsMultipleSelection: false,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const asset = result.assets[0];
await handleSendImage(asset.uri, asset.mimeType ?? undefined);
}
} catch (error) {
console.error('选择图片失败:', error);
Alert.alert('错误', '选择图片失败');
}
setActivePanel('none');
}, [handleSendImage]);
// 拍摄照片
const handleTakePhoto = useCallback(async () => {
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];
await handleSendImage(asset.uri, asset.mimeType ?? undefined);
}
} catch (error) {
console.error('拍摄照片失败:', error);
Alert.alert('错误', '拍摄照片失败');
}
setActivePanel('none');
}, [handleSendImage]);
// 处理更多功能
const handleMoreAction = useCallback((actionId: string) => {
switch (actionId) {
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]);
// 插入表情
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';
if (newPanel !== 'none' && isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'open-emoji-panel');
}, 200);
}
return newPanel;
});
}, [scrollToLatest, isNearBottom]);
// 切换更多功能面板
const toggleMorePanel = useCallback(() => {
Keyboard.dismiss();
setActivePanel(prev => {
const newPanel = prev === 'more' ? 'none' : 'more';
if (newPanel !== 'none' && isNearBottom()) {
setTimeout(() => {
scrollToLatest(false, false, 'open-more-panel');
}, 200);
}
return newPanel;
});
}, [scrollToLatest, isNearBottom]);
// 关闭面板
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) {
navigationService.navigate('UserProfile', { userId: 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) {
navigation.navigate('GroupInfo' as any, {
groupId: routeGroupId,
conversationId: conversationId || undefined,
});
} else if (otherUser?.id) {
navigationService.navigate('UserProfile', { userId: String(otherUser.id) });
}
}, [navigation, isGroupChat, routeGroupId, otherUser]);
// 导航到聊天管理页面
const navigateToChatSettings = useCallback(() => {
if (isGroupChat && routeGroupId) {
navigation.navigate('GroupInfo' as any, {
groupId: routeGroupId,
conversationId: conversationId || undefined,
});
} else if (otherUser?.id && conversationId) {
navigation.navigate('PrivateChatInfo' as any, {
conversationId: conversationId,
userId: String(otherUser.id),
userName: otherUser.nickname,
userAvatar: otherUser.avatar,
});
}
}, [navigation, isGroupChat, routeGroupId, otherUser, conversationId]);
return {
// 状态
messages,
conversationId,
inputText,
otherUser,
currentUser,
currentUserId,
keyboardHeight,
loading,
sending,
activePanel,
sendingImage,
replyingTo,
longPressMenuVisible,
selectedMessage,
selectedMessageId,
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,
handleSendImage,
handlePickImage,
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,
setBrowsingHistory,
};
};