feat(rn-demo): update chat screens and message bubble components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-04-29 00:37:32 +08:00
父节点 069f3454fe
当前提交 53097c5a5c
共有 4 个文件被更改,包括 83 次插入26 次删除

查看文件

@ -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<number>(0)
const inputRef = useRef<TextInput>(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 (
<View>
{editingMessage && (
<View style={styles.editBanner}>
<Text style={styles.editBannerText}></Text>
<TouchableOpacity onPress={onCancelEdit}>
<Text style={styles.editBannerAction}></Text>
</TouchableOpacity>
</View>
)}
{showEmoji && (
<View style={styles.emojiPanel}>
<View style={styles.emojiGrid}>
@ -140,21 +162,21 @@ export default function ChatInput({ toId, chatType, onSent }: Props) {
)}
<View style={styles.bar}>
<TouchableOpacity style={styles.iconBtn} onPress={toggleRecord}>
<Text style={styles.icon}>{recording ? '⏹' : '🎤'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.iconBtn} onPress={toggleRecord}>
<Text style={styles.icon}>{recording ? '⏹' : '🎤'}</Text>
</TouchableOpacity>
<TextInput
ref={inputRef}
style={styles.input}
placeholder={recording ? '松开停止录音...' : '输入消息'}
value={text}
onChangeText={setText}
multiline
editable={!recording && !sending}
onFocus={() => setShowEmoji(false)}
returnKeyType="default"
/>
<TextInput
ref={inputRef}
style={styles.input}
placeholder={editingMessage ? '编辑消息内容' : recording ? '松开停止录音...' : '输入消息'}
value={text}
onChangeText={setText}
multiline
editable={!recording && !sending}
onFocus={() => setShowEmoji(false)}
returnKeyType="default"
/>
<TouchableOpacity style={styles.iconBtn} onPress={() => setShowEmoji(v => !v)}>
<Text style={styles.icon}>😊</Text>
@ -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' },
})

查看文件

@ -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) {
<Text style={styles.metaLine}>
{senderLabel}
{message.status === 'READ' ? ' · 已读' : message.status === 'DELIVERED' ? ' · 已送达' : ''}
{message.editedAt ? ' · 已编辑' : ''}
</Text>
<MessageContent message={message} />
</Pressable>

查看文件

@ -41,6 +41,7 @@ export default function GroupChatScreen({ route, navigation }: Props) {
const [messages, setMessages] = useState<ImMessage[]>([])
const [page, setPage] = useState(0)
const [loadingMore, setLoadingMore] = useState(false)
const [editingMessage, setEditingMessage] = useState<ImMessage | null>(null)
const listRef = useRef<FlatList<ImMessage>>(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 }}
/>
<ChatInput toId={groupId} chatType="GROUP" onSent={onSent} />
<ChatInput
toId={groupId}
chatType="GROUP"
onSent={onSent}
editingMessage={editingMessage}
onCancelEdit={() => setEditingMessage(null)}
/>
</KeyboardAvoidingView>
</SafeAreaView>
)

查看文件

@ -29,6 +29,7 @@ export default function SingleChatScreen({ route }: Props) {
const [messages, setMessages] = useState<ImMessage[]>([])
const [page, setPage] = useState(0)
const [loadingMore, setLoadingMore] = useState(false)
const [editingMessage, setEditingMessage] = useState<ImMessage | null>(null)
const listRef = useRef<FlatList<ImMessage>>(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 }}
/>
<ChatInput toId={targetId} chatType="SINGLE" onSent={onSent} />
<ChatInput
toId={targetId}
chatType="SINGLE"
onSent={onSent}
editingMessage={editingMessage}
onCancelEdit={() => setEditingMessage(null)}
/>
</KeyboardAvoidingView>
</SafeAreaView>
)