Files
frontend/src/screens/message/components/EmbeddedChat.tsx

410 lines
12 KiB
TypeScript
Raw Normal View History

/**
* 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<EmbeddedChatProps> = ({ conversation, onBack }) => {
const navigation = useNavigation();
const currentUser = useAuthStore(state => state.currentUser);
// 状态
const [messages, setMessages] = useState<MessageResponse[]>([]);
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [inputText, setInputText] = useState('');
const flatListRef = useRef<FlatList>(null);
const inputRef = useRef<TextInput>(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 (
<View style={[styles.messageRow, isMe ? styles.messageRowRight : styles.messageRowLeft]}>
{!isMe && (
<Avatar
source={item.sender?.avatar}
size={36}
name={item.sender?.nickname || item.sender?.username}
/>
)}
<View style={[styles.messageBubble, isMe ? styles.messageBubbleMe : styles.messageBubbleOther]}>
{isGroupChat && !isMe && (
<Text style={styles.senderName}>{item.sender?.nickname || item.sender?.username}</Text>
)}
<Text style={[styles.messageText, isMe ? styles.messageTextMe : styles.messageTextOther]}>
{extractTextFromSegments(item.segments)}
</Text>
</View>
{isMe && (
<Avatar
source={currentUser?.avatar}
size={36}
name={currentUser?.nickname || currentUser?.username}
/>
)}
</View>
);
};
return (
<View style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.headerButton}>
<MaterialCommunityIcons name="arrow-left" size={24} color="#666" />
</TouchableOpacity>
<View style={styles.headerCenter}>
<Avatar source={chatAvatar} size={36} name={chatTitle} />
<Text style={styles.headerTitle} numberOfLines={1}>
{chatTitle}
</Text>
</View>
<TouchableOpacity onPress={handleNavigateToFullChat} style={styles.headerButton}>
<MaterialCommunityIcons name="arrow-expand" size={22} color="#666" />
</TouchableOpacity>
</View>
{/* 消息列表 */}
<View style={styles.messageList}>
{loading ? (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : messages.length === 0 ? (
<View style={styles.centerContainer}>
<MaterialCommunityIcons name="message-text-outline" size={48} color="#DDD" />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubtext}></Text>
</View>
) : (
<FlatList
ref={flatListRef}
data={messages}
renderItem={renderMessage}
keyExtractor={(item) => String(item.id)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={true}
onLayout={() => {
if (messages.length > 0) {
flatListRef.current?.scrollToEnd({ animated: false });
}
}}
/>
)}
</View>
{/* 输入框区域 */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
<View style={styles.inputArea}>
<View style={styles.inputRow}>
<TouchableOpacity style={styles.iconButton}>
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
</TouchableOpacity>
<View style={styles.inputContainer}>
<TextInput
ref={inputRef}
style={styles.textInput}
placeholder="发送消息..."
placeholderTextColor="#999"
value={inputText}
onChangeText={setInputText}
multiline={false}
returnKeyType="send"
onSubmitEditing={handleSend}
blurOnSubmit={false}
/>
</View>
<TouchableOpacity style={styles.iconButton}>
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
</TouchableOpacity>
<TouchableOpacity
style={[styles.sendButton, (!inputText.trim() || sending) && styles.sendButtonDisabled]}
onPress={handleSend}
disabled={!inputText.trim() || sending}
>
{sending ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
<MaterialCommunityIcons name="send" size={20} color="#FFF" />
)}
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</View>
);
};
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',
},
});