/** * EmbeddedChat.tsx * 嵌入式聊天组件 - 用于桌面端双栏布局右侧 */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator, KeyboardAvoidingView, Platform, TextInput, Dimensions, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing, fontSizes, shadows } from '../../../theme'; import { ConversationResponse, MessageResponse, MessageSegment } from '../../../types/dto'; import { Avatar, Text } from '../../../components/common'; import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores'; import { extractTextFromSegments } from '../../../types/dto'; interface EmbeddedChatProps { conversation: ConversationResponse; onBack: () => void; } const { width: screenWidth } = Dimensions.get('window'); export const EmbeddedChat: React.FC = ({ conversation, onBack }) => { const navigation = useNavigation(); const currentUser = useAuthStore(state => state.currentUser); // 状态 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 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]); // 发送消息 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 renderMessage = ({ item }: { item: MessageResponse }) => { const isMe = String(item.sender_id) === String(currentUser?.id); return ( {!isMe && ( )} {isGroupChat && !isMe && ( {item.sender?.nickname || item.sender?.username} )} {item.status === 'recalled' ? '消息已撤回' : extractTextFromSegments(item.segments)} {isMe && ( )} ); }; return ( {/* 头部 */} {chatTitle} {/* 消息列表 */} {loading ? ( ) : messages.length === 0 ? ( 暂无消息 发送第一条消息开始聊天 ) : ( String(item.id)} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={true} onLayout={() => { if (messages.length > 0) { flatListRef.current?.scrollToEnd({ animated: false }); } }} /> )} {/* 输入框区域 */} {sending ? ( ) : ( )} ); }; 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', flexDirection: 'row-reverse', }, 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', }, 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', }, });