/** * EmbeddedChat.tsx * 嵌入式聊天组件 - 用于桌面端双栏布局右侧 */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator, KeyboardAvoidingView, Platform, TextInput, Dimensions, Animated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Image as ExpoImage } from 'expo-image'; import { colors, spacing, fontSizes, shadows } from '../../../theme'; import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto'; import { Avatar, Text, ImageGallery } from '../../../components/common'; import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores'; import { extractTextFromSegments } from '../../../types/dto'; import { useBreakpointGTE } from '../../../hooks/useResponsive'; import { useMarkAsRead } from '../../../stores/messageManagerHooks'; import { messageService } from '../../../services'; import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel'; import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen'; const { width: screenWidth } = Dimensions.get('window'); const PANEL_WIDTH = 360; interface EmbeddedChatProps { conversation: ConversationResponse; onBack: () => void; } export const EmbeddedChat: React.FC = ({ conversation, onBack }) => { const navigation = useNavigation(); const currentUser = useAuthStore(state => state.currentUser); // 响应式布局 - 使用 768px 作为大屏幕和小屏幕的分界线 const isWideScreen = useBreakpointGTE('lg'); // 使用 markAsRead hook const { markAsRead } = useMarkAsRead(String(conversation.id)); // 状态 const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [inputText, setInputText] = useState(''); const flatListRef = useRef(null); const inputRef = useRef(null); // 图片预览状态 const [imageGalleryVisible, setImageGalleryVisible] = useState(false); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [galleryImages, setGalleryImages] = useState<{ url: string }[]>([]); // 右键菜单状态 const [contextMenuVisible, setContextMenuVisible] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState(); const [selectedMessageForMenu, setSelectedMessageForMenu] = useState(null); // 回复状态 const [replyingToMessage, setReplyingToMessage] = useState(null); // 群信息面板动画 const slideAnim = useRef(new Animated.Value(PANEL_WIDTH)).current; const fadeAnim = useRef(new Animated.Value(0)).current; const [showGroupInfoPanel, setShowGroupInfoPanel] = useState(false); // 群成员列表 const [groupMembers, setGroupMembers] = useState([]); // 打开/关闭群信息面板 const toggleGroupInfoPanel = useCallback(() => { if (showGroupInfoPanel) { Animated.parallel([ Animated.timing(slideAnim, { toValue: PANEL_WIDTH, duration: 300, useNativeDriver: true, }), Animated.timing(fadeAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), ]).start(() => { setShowGroupInfoPanel(false); }); } else { setShowGroupInfoPanel(true); Animated.parallel([ Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true, }), ]).start(); } }, [showGroupInfoPanel, slideAnim, fadeAnim]); // 获取会话信息 const isGroupChat = conversation.type === 'group'; const chatTitle = isGroupChat ? conversation.group?.name || '群聊' : conversation.participants?.[0]?.nickname || conversation.participants?.[0]?.username || '聊天'; const chatAvatar = isGroupChat ? conversation.group?.avatar : conversation.participants?.[0]?.avatar; // 加载消息 const loadMessages = useCallback(async () => { try { setLoading(true); await messageManager.fetchMessages(String(conversation.id)); setMessages(messageManager.getMessages(String(conversation.id))); } catch (error) { console.error('[EmbeddedChat] 加载消息失败:', error); } finally { setLoading(false); } }, [conversation.id]); // 初始加载 useEffect(() => { loadMessages(); // 订阅消息事件 const subscriber: MessageSubscriber = (event: MessageEvent) => { if ( event.type === 'messages_updated' && String(event.payload?.conversationId) === String(conversation.id) ) { setMessages(event.payload.messages || []); setTimeout(() => { flatListRef.current?.scrollToEnd({ animated: true }); }, 100); } }; const unsubscribe = messageManager.subscribe(subscriber); return () => { unsubscribe(); }; }, [conversation.id, loadMessages]); // 自动标记已读 - 当有新消息时 useEffect(() => { if (messages.length === 0) return; const maxSeq = Math.max(...messages.map(m => m.seq), 0); if (maxSeq > 0) { markAsRead(maxSeq).catch(error => { console.error('[EmbeddedChat] 自动标记已读失败:', error); }); } }, [messages, markAsRead]); // 发送消息 const handleSend = useCallback(async () => { if (!inputText.trim() || sending) return; const text = inputText.trim(); setInputText(''); try { setSending(true); const segments: MessageSegment[] = [{ type: 'text', data: { text } }]; await messageManager.sendMessage(String(conversation.id), segments); } catch (error) { console.error('[EmbeddedChat] 发送消息失败:', error); } finally { setSending(false); } }, [inputText, sending, conversation.id]); // 导航到完整聊天页面 const handleNavigateToFullChat = () => { if (isGroupChat && conversation.group) { (navigation as any).navigate('Chat', { conversationId: String(conversation.id), groupId: String(conversation.group.id), groupName: conversation.group.name, isGroupChat: true, }); } else { const currentUserId = currentUser?.id; const otherUser = conversation.participants?.find(p => String(p.id) !== String(currentUserId)); (navigation as any).navigate('Chat', { conversationId: String(conversation.id), userId: otherUser ? String(otherUser.id) : undefined, userName: otherUser?.nickname || otherUser?.username, isGroupChat: false, }); } }; // 处理图片点击预览 const handleImagePress = (images: { url: string }[], index: number) => { setGalleryImages(images); setCurrentImageIndex(index); setImageGalleryVisible(true); }; // 处理右键点击消息 const handleMessageRightPress = (message: MessageResponse, event: any) => { // 获取鼠标右键位置 const pageX = event?.nativeEvent?.pageX || 0; const pageY = event?.nativeEvent?.pageY || 0; setSelectedMessageForMenu(message as GroupMessage); setContextMenuPosition({ x: pageX, y: pageY, width: 0, height: 0, pressX: pageX, pressY: pageY, }); setContextMenuVisible(true); }; // 关闭右键菜单 const handleContextMenuClose = () => { setContextMenuVisible(false); setSelectedMessageForMenu(null); setContextMenuPosition(undefined); }; // 处理回复消息 const handleReply = (message: GroupMessage) => { setReplyingToMessage(message); handleContextMenuClose(); }; // 处理撤回消息 const handleRecall = async (messageId: string) => { try { await messageService.recallMessage(messageId); handleContextMenuClose(); } catch (error) { console.error('[EmbeddedChat] 撤回消息失败:', error); } }; // 处理删除消息 const handleDeleteMessage = async (messageId: string) => { try { await messageService.deleteMessage(messageId); handleContextMenuClose(); } catch (error) { console.error('[EmbeddedChat] 删除消息失败:', error); } }; // 渲染消息内容 const renderMessageContent = (item: MessageResponse) => { // 撤回消息 if (item.status === 'recalled') { return ( 消息已撤回 ); } // 获取消息 segments const segments = item.segments || []; // 检查是否有图片消息 const imageSegments = segments .filter(s => s.type === 'image') .map(s => s.data as ImageSegmentData); // 提取文本内容 const textContent = extractTextFromSegments(segments); // 渲染多个内容(文本 + 图片) return ( {/* 文本内容 */} {textContent && textContent !== '[图片]' && ( {textContent} )} {/* 图片内容 */} {imageSegments.length > 0 && ( {imageSegments.map((imgData, idx) => { const imageUrl = imgData.thumbnail_url || imgData.url; const hasValidUrl = !!imageUrl && imageUrl.trim() !== ''; if (!hasValidUrl) { // 无效URL显示占位符 return ( 图片 ); } // 计算图片尺寸 let width = 180; let height = 180; if (imgData.width && imgData.height) { const aspectRatio = imgData.width / imgData.height; if (aspectRatio > 1) { height = width / aspectRatio; } else { width = height * aspectRatio; } } return ( handleImagePress(imageSegments.map(s => ({ url: s.thumbnail_url || s.url || '' })), idx)} activeOpacity={0.8} > ); })} )} {/* 如果没有文本也没有图片,显示空消息提示 */} {!textContent && imageSegments.length === 0 && ( [消息格式错误] )} ); }; // 渲染消息 const renderMessage = ({ item }: { item: MessageResponse }) => { const isMe = String(item.sender_id) === String(currentUser?.id); // 处理指针事件(鼠标右键)- 使用 onPointerDown 检测鼠标右键 const handlePointerDown = (e: any) => { // e.nativeEvent.button: 0=左键, 1=中键, 2=右键 const isRightClick = e.nativeEvent?.button === 2; if (isRightClick) { // 阻止默认右键菜单 e.preventDefault?.(); e.stopPropagation?.(); handleMessageRightPress(item, e); } }; return ( {!isMe && ( )} {isGroupChat && !isMe && ( {item.sender?.nickname || item.sender?.username} )} {renderMessageContent(item)} {isMe && ( )} ); }; return ( {/* 头部 */} {/* 大屏幕(>= 768px)时隐藏返回按钮 */} {!isWideScreen ? ( ) : ( )} {chatTitle} {/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */} {isWideScreen && isGroupChat ? ( ) : ( )} {/* 消息列表 */} {loading ? ( ) : messages.length === 0 ? ( 暂无消息 发送第一条消息开始聊天 ) : ( String(item.id)} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={true} onLayout={() => { if (messages.length > 0) { flatListRef.current?.scrollToEnd({ animated: false }); } }} /> )} {/* 输入框区域 */} {sending ? ( ) : ( )} {/* 群信息侧边栏面板 - 仅大屏幕显示 */} {isWideScreen && isGroupChat && ( <> {/* 遮罩层 */} {showGroupInfoPanel && ( )} {/* 侧边栏面板 */} { // TODO: 实现置顶功能 console.log('Toggle pin:', pinned); }} onLeaveGroup={() => { // TODO: 实现退出群聊功能 console.log('Leave group'); }} onInviteMembers={() => { // TODO: 实现邀请新成员功能 console.log('Invite members'); }} onViewAllMembers={() => { // TODO: 实现查看全部成员功能 console.log('View all members'); }} /> )} {/* 右键菜单 */} {/* 图片查看器 */} ({ id: String(idx), url: img.url || '' }))} initialIndex={currentImageIndex} onClose={() => setImageGalleryVisible(false)} enableSave /> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F5F7FA', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, backgroundColor: '#FFF', borderBottomWidth: 1, borderBottomColor: '#E8E8E8', ...shadows.sm, height: 56, }, headerButton: { padding: spacing.xs, width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerCenter: { flexDirection: 'row', alignItems: 'center', flex: 1, justifyContent: 'center', }, headerTitle: { fontSize: fontSizes.lg, fontWeight: '600', color: '#333', marginLeft: spacing.sm, maxWidth: 200, }, messageList: { flex: 1, backgroundColor: '#F5F7FA', }, centerContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: spacing.xl, }, emptyText: { marginTop: spacing.md, fontSize: 16, color: '#999', }, emptySubtext: { marginTop: spacing.xs, fontSize: 14, color: '#BBB', }, listContent: { padding: spacing.md, paddingBottom: spacing.xl, }, messageRow: { flexDirection: 'row', alignItems: 'flex-end', marginBottom: spacing.md, maxWidth: screenWidth * 0.7, }, messageRowLeft: { alignSelf: 'flex-start', }, messageRowRight: { alignSelf: 'flex-end', justifyContent: 'flex-end', }, messageBubble: { maxWidth: screenWidth * 0.5, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: 18, marginHorizontal: spacing.sm, }, messageBubbleMe: { backgroundColor: colors.primary.main, borderBottomRightRadius: 4, }, messageBubbleOther: { backgroundColor: '#FFF', borderBottomLeftRadius: 4, ...shadows.sm, }, senderName: { fontSize: 12, color: '#999', marginBottom: 2, }, messageText: { fontSize: 15, lineHeight: 20, }, messageTextMe: { color: '#FFF', }, messageTextOther: { color: '#333', }, inputArea: { backgroundColor: '#FFF', borderTopWidth: 1, borderTopColor: '#E8E8E8', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm, }, inputRow: { flexDirection: 'row', alignItems: 'center', }, iconButton: { padding: spacing.xs, width: 44, height: 44, alignItems: 'center', justifyContent: 'center', }, inputContainer: { flex: 1, backgroundColor: '#F5F5F5', borderRadius: 20, paddingHorizontal: spacing.md, minHeight: 40, justifyContent: 'center', // 移除阴影 shadowColor: 'transparent', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0, shadowRadius: 0, elevation: 0, }, textInput: { fontSize: 15, color: '#333', padding: 0, maxHeight: 100, }, sendButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginLeft: spacing.xs, }, sendButtonDisabled: { backgroundColor: '#E0E0E0', }, messageTextRecalled: { fontStyle: 'italic', color: '#999', }, imageGrid: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 4, }, messageImage: { borderRadius: 8, marginRight: 4, marginBottom: 4, }, imagePlaceholder: { width: 120, height: 120, borderRadius: 8, backgroundColor: 'rgba(0,0,0,0.1)', justifyContent: 'center', alignItems: 'center', marginRight: 4, marginBottom: 4, }, imagePlaceholderText: { color: '#999', fontSize: 12, marginTop: 4, }, });