Files
frontend/src/screens/message/components/ChatScreen/useChatScreen.ts
lan be84c01abd Migrate frontend realtime messaging to SSE.
Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts.

Made-with: Cursor
2026-03-10 12:58:23 +08:00

1217 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 {
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);
// 防止重复加载的 ref
const shouldAutoScrollOnEnterRef = useRef(true);
const autoScrollTimersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
const scrollPositionRef = useRef({ contentHeight: 0, scrollY: 0 });
// 回复消息状态
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]);
// 【改造】同步加载状态
useEffect(() => {
setLoading(isLoadingMessages);
}, [isLoadingMessages]);
// 【改造】同步 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(() => {
shouldAutoScrollOnEnterRef.current = true;
autoScrollTimersRef.current.forEach(clearTimeout);
autoScrollTimersRef.current = [];
}, [conversationId]);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
autoScrollTimersRef.current.forEach(clearTimeout);
autoScrollTimersRef.current = [];
};
}, []);
// 消息加载完成后滚动到底部
useEffect(() => {
if (!loading && !loadingMore && messages.length > 0 && shouldAutoScrollOnEnterRef.current) {
const timer = setTimeout(() => {
if (shouldAutoScrollOnEnterRef.current) {
flatListRef.current?.scrollToEnd({ animated: false });
}
}, 500);
return () => clearTimeout(timer);
}
}, [loading, loadingMore, messages.length]);
// 获取当前用户信息
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]);
// 【改造】加载更多历史消息
const loadMoreHistory = useCallback(async () => {
if (!conversationId || !hasMoreHistory || loadingMore) {
return;
}
setLoadingMore(true);
try {
await loadMoreMessages();
// 更新 firstSeq
if (messages.length > 0) {
const minSeq = Math.min(...messages.map(m => m.seq));
setFirstSeq(minSeq);
}
} catch (error) {
console.error('加载历史消息失败:', error);
} finally {
setLoadingMore(false);
}
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
// 列表内容尺寸变化后触发首次自动滚动
const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
if (!shouldAutoScrollOnEnterRef.current) return;
if (loading || loadingMore) return;
if (messages.length === 0) return;
const prevContentHeight = scrollPositionRef.current.contentHeight;
scrollPositionRef.current.contentHeight = contentHeight;
if (prevContentHeight > 0 && contentHeight > prevContentHeight) {
return;
}
shouldAutoScrollOnEnterRef.current = false;
autoScrollTimersRef.current.forEach(clearTimeout);
autoScrollTimersRef.current = [];
[100, 300, 500, 800, 1200].forEach(delay => {
const timer = setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: false });
}, delay);
autoScrollTimersRef.current.push(timer);
});
}, [loading, loadingMore, messages.length]);
// 【改造】自动标记已读 - 当有新消息且是当前会话时
useEffect(() => {
if (!conversationId || messages.length === 0) return;
const maxSeq = Math.max(...messages.map(m => m.seq), 0);
if (maxSeq > 0) {
markAsRead(maxSeq).catch(error => {
console.error('[ChatScreen] 自动标记已读失败:', error);
});
}
}, [messages, conversationId, markAsRead]);
// 使用 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');
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: false });
}, 150);
};
const keyboardWillHide = () => {
setKeyboardHeight(0);
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: false });
}, 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();
};
}, []);
// 格式化时间
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(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 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]);
// 发送图片消息
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(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
} catch (error) {
console.error('发送图片失败:', error);
Alert.alert('发送失败', getSendErrorMessage(error, '图片发送失败,请重试'));
} finally {
setSendingImage(false);
}
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, getSendErrorMessage, canSendPrivateImage]);
// 选择图片
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(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
} catch (error) {
console.error('发送自定义表情失败:', error);
Alert.alert('发送失败', '自定义表情发送失败,请重试');
} finally {
setSendingImage(false);
}
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, canSendPrivateImage]);
// 切换表情面板
const toggleEmojiPanel = useCallback(() => {
Keyboard.dismiss();
setActivePanel(prev => {
const newPanel = prev === 'emoji' ? 'none' : 'emoji';
if (newPanel !== 'none') {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: false });
}, 200);
}
return newPanel;
});
}, []);
// 切换更多功能面板
const toggleMorePanel = useCallback(() => {
Keyboard.dismiss();
setActivePanel(prev => {
const newPanel = prev === 'more' ? 'none' : 'more';
if (newPanel !== 'none') {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: false });
}, 200);
}
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) {
navigation.navigate('UserProfile' as any, { userId: String(userId) });
}
}, [navigation, 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) {
const rootNavigation = navigation.getParent();
rootNavigation?.navigate('UserProfile' as any, { 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,
};
};