From 53097c5a5cf949d1fdb826df013584694e6bdf5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Wed, 29 Apr 2026 00:37:32 +0800 Subject: [PATCH] feat(rn-demo): update chat screens and message bubble components Co-Authored-By: Claude Sonnet 4.6 --- src/components/ChatInput.tsx | 59 +++++++++++++++++++-------- src/components/MessageBubble.tsx | 22 ++++++---- src/screens/chat/GroupChatScreen.tsx | 14 ++++++- src/screens/chat/SingleChatScreen.tsx | 14 ++++++- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index 2d57f9f..489ba22 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -18,11 +18,13 @@ interface Props { toId: string chatType: ChatType onSent(msg: ImMessage): void + editingMessage?: ImMessage | null + onCancelEdit?: () => void } const recorder = AudioRecorderPlayer -export default function ChatInput({ toId, chatType, onSent }: Props) { +export default function ChatInput({ toId, chatType, onSent, editingMessage, onCancelEdit }: Props) { const [text, setText] = useState('') const [sending, setSending] = useState(false) const [showEmoji, setShowEmoji] = useState(false) @@ -31,16 +33,28 @@ export default function ChatInput({ toId, chatType, onSent }: Props) { const recordStart = useRef(0) const inputRef = useRef(null) + React.useEffect(() => { + setText(editingMessage?.content ?? '') + if (editingMessage) { + setTimeout(() => inputRef.current?.focus(), 50) + } + }, [editingMessage]) + const send = async () => { const t = text.trim() if (!t || sending) return setSending(true) try { - const msg = await ImSDK.sendMessage(toId, chatType, 'TEXT', t) + const msg = editingMessage + ? await ImSDK.editMessage(editingMessage.id, t) + : await ImSDK.sendMessage(toId, chatType, 'TEXT', t) setText('') + if (editingMessage) { + onCancelEdit?.() + } onSent(msg) } catch (e: any) { - Alert.alert('发送失败', e?.message ?? '请重试') + Alert.alert(editingMessage ? '保存失败' : '发送失败', e?.message ?? '请重试') } finally { setSending(false) } @@ -127,6 +141,14 @@ export default function ChatInput({ toId, chatType, onSent }: Props) { return ( + {editingMessage && ( + + 正在编辑消息 + + 取消 + + + )} {showEmoji && ( @@ -140,21 +162,21 @@ export default function ChatInput({ toId, chatType, onSent }: Props) { )} - - {recording ? '⏹' : '🎤'} - + + {recording ? '⏹' : '🎤'} + - setShowEmoji(false)} - returnKeyType="default" - /> + setShowEmoji(false)} + returnKeyType="default" + /> setShowEmoji(v => !v)}> 😊 @@ -198,4 +220,7 @@ const styles = StyleSheet.create({ emojiGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8 }, emojiBtn: { width: '10%', aspectRatio: 1, alignItems: 'center', justifyContent: 'center' }, emojiText: { fontSize: 24 }, + editBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 8, backgroundColor: '#fff7ed', borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#fed7aa' }, + editBannerText: { fontSize: 13, color: '#9a3412', fontWeight: '600' }, + editBannerAction: { fontSize: 13, color: '#ea580c', fontWeight: '700' }, }) diff --git a/src/components/MessageBubble.tsx b/src/components/MessageBubble.tsx index 02e6d40..dd03681 100644 --- a/src/components/MessageBubble.tsx +++ b/src/components/MessageBubble.tsx @@ -1,5 +1,6 @@ import React, {useRef, useState} from 'react' import {Alert, Dimensions, Image, Pressable, StyleSheet, Text, View} from 'react-native' +import type { AlertButton } from 'react-native' import AudioRecorderPlayer from 'react-native-audio-recorder-player' import type {ImMessage} from '@xuqm/rn-sdk' @@ -8,6 +9,7 @@ interface Props { isOwn: boolean peerNickname: string onRevoke?: (messageId: string) => void + onEdit?: (message: ImMessage) => void } const SCREEN_W = Dimensions.get('window').width @@ -306,15 +308,20 @@ function MessageContent({message}: {message: ImMessage}) { } } -export function MessageBubble({message, isOwn, peerNickname, onRevoke}: Props) { - const canRevoke = isOwn && message.status !== 'REVOKED' && message.msgType !== 'REVOKED' +export function MessageBubble({message, isOwn, peerNickname, onRevoke, onEdit}: Props) { + const canRevoke = Boolean(onRevoke) && isOwn && message.status !== 'REVOKED' && message.msgType !== 'REVOKED' + const canEdit = Boolean(onEdit) && isOwn && message.msgType === 'TEXT' && message.status !== 'REVOKED' const handleLongPress = () => { - if (!canRevoke) return - Alert.alert('操作', '撤回这条消息?', [ - {text: '取消', style: 'cancel'}, - {text: '撤回', style: 'destructive', onPress: () => onRevoke?.(message.id)}, - ]) + if (!canEdit && !canRevoke) return + const buttons: AlertButton[] = [{text: '取消', style: 'cancel'}] + if (canEdit) { + buttons.push({text: '编辑', onPress: () => onEdit?.(message)}) + } + if (canRevoke) { + buttons.push({text: '撤回', style: 'destructive', onPress: () => onRevoke?.(message.id)}) + } + Alert.alert('操作', '请选择要执行的操作', buttons) } const senderLabel = isOwn ? '我' : peerNickname @@ -333,6 +340,7 @@ export function MessageBubble({message, isOwn, peerNickname, onRevoke}: Props) { {senderLabel} {message.status === 'READ' ? ' · 已读' : message.status === 'DELIVERED' ? ' · 已送达' : ''} + {message.editedAt ? ' · 已编辑' : ''} diff --git a/src/screens/chat/GroupChatScreen.tsx b/src/screens/chat/GroupChatScreen.tsx index 16ccd5d..7580c72 100644 --- a/src/screens/chat/GroupChatScreen.tsx +++ b/src/screens/chat/GroupChatScreen.tsx @@ -41,6 +41,7 @@ export default function GroupChatScreen({ route, navigation }: Props) { const [messages, setMessages] = useState([]) const [page, setPage] = useState(0) const [loadingMore, setLoadingMore] = useState(false) + const [editingMessage, setEditingMessage] = useState(null) const listRef = useRef>(null) const loadHistory = useCallback(async (p = 0) => { @@ -95,6 +96,10 @@ export default function GroupChatScreen({ route, navigation }: Props) { } } + const handleEdit = (message: ImMessage) => { + setEditingMessage(message) + } + const loadMore = async () => { if (loadingMore) return setLoadingMore(true) @@ -119,6 +124,7 @@ export default function GroupChatScreen({ route, navigation }: Props) { isOwn={item.fromUserId === userId} peerNickname={item.fromUserId} onRevoke={handleRevoke} + onEdit={handleEdit} /> )} contentContainerStyle={styles.list} @@ -126,7 +132,13 @@ export default function GroupChatScreen({ route, navigation }: Props) { onEndReached={loadMore} maintainVisibleContentPosition={{ minIndexForVisible: 0 }} /> - + setEditingMessage(null)} + /> ) diff --git a/src/screens/chat/SingleChatScreen.tsx b/src/screens/chat/SingleChatScreen.tsx index f79bb87..bcb8949 100644 --- a/src/screens/chat/SingleChatScreen.tsx +++ b/src/screens/chat/SingleChatScreen.tsx @@ -29,6 +29,7 @@ export default function SingleChatScreen({ route }: Props) { const [messages, setMessages] = useState([]) const [page, setPage] = useState(0) const [loadingMore, setLoadingMore] = useState(false) + const [editingMessage, setEditingMessage] = useState(null) const listRef = useRef>(null) const loadHistory = useCallback(async (p = 0) => { @@ -78,6 +79,10 @@ export default function SingleChatScreen({ route }: Props) { } } + const handleEdit = (message: ImMessage) => { + setEditingMessage(message) + } + const loadMore = async () => { if (loadingMore) return setLoadingMore(true) @@ -102,6 +107,7 @@ export default function SingleChatScreen({ route }: Props) { isOwn={item.fromUserId === userId} peerNickname={targetName} onRevoke={handleRevoke} + onEdit={handleEdit} /> )} contentContainerStyle={styles.list} @@ -109,7 +115,13 @@ export default function SingleChatScreen({ route }: Props) { onEndReached={loadMore} maintainVisibleContentPosition={{ minIndexForVisible: 0 }} /> - + setEditingMessage(null)} + /> )