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 toId: string
chatType: ChatType chatType: ChatType
onSent(msg: ImMessage): void onSent(msg: ImMessage): void
editingMessage?: ImMessage | null
onCancelEdit?: () => void
} }
const recorder = AudioRecorderPlayer 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 [text, setText] = useState('')
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const [showEmoji, setShowEmoji] = 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 recordStart = useRef<number>(0)
const inputRef = useRef<TextInput>(null) const inputRef = useRef<TextInput>(null)
React.useEffect(() => {
setText(editingMessage?.content ?? '')
if (editingMessage) {
setTimeout(() => inputRef.current?.focus(), 50)
}
}, [editingMessage])
const send = async () => { const send = async () => {
const t = text.trim() const t = text.trim()
if (!t || sending) return if (!t || sending) return
setSending(true) setSending(true)
try { 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('') setText('')
if (editingMessage) {
onCancelEdit?.()
}
onSent(msg) onSent(msg)
} catch (e: any) { } catch (e: any) {
Alert.alert('发送失败', e?.message ?? '请重试') Alert.alert(editingMessage ? '保存失败' : '发送失败', e?.message ?? '请重试')
} finally { } finally {
setSending(false) setSending(false)
} }
@ -127,6 +141,14 @@ export default function ChatInput({ toId, chatType, onSent }: Props) {
return ( return (
<View> <View>
{editingMessage && (
<View style={styles.editBanner}>
<Text style={styles.editBannerText}></Text>
<TouchableOpacity onPress={onCancelEdit}>
<Text style={styles.editBannerAction}></Text>
</TouchableOpacity>
</View>
)}
{showEmoji && ( {showEmoji && (
<View style={styles.emojiPanel}> <View style={styles.emojiPanel}>
<View style={styles.emojiGrid}> <View style={styles.emojiGrid}>
@ -147,7 +169,7 @@ export default function ChatInput({ toId, chatType, onSent }: Props) {
<TextInput <TextInput
ref={inputRef} ref={inputRef}
style={styles.input} style={styles.input}
placeholder={recording ? '松开停止录音...' : '输入消息'} placeholder={editingMessage ? '编辑消息内容' : recording ? '松开停止录音...' : '输入消息'}
value={text} value={text}
onChangeText={setText} onChangeText={setText}
multiline multiline
@ -198,4 +220,7 @@ const styles = StyleSheet.create({
emojiGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8 }, emojiGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8 },
emojiBtn: { width: '10%', aspectRatio: 1, alignItems: 'center', justifyContent: 'center' }, emojiBtn: { width: '10%', aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },
emojiText: { fontSize: 24 }, 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 React, {useRef, useState} from 'react'
import {Alert, Dimensions, Image, Pressable, StyleSheet, Text, View} from 'react-native' 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 AudioRecorderPlayer from 'react-native-audio-recorder-player'
import type {ImMessage} from '@xuqm/rn-sdk' import type {ImMessage} from '@xuqm/rn-sdk'
@ -8,6 +9,7 @@ interface Props {
isOwn: boolean isOwn: boolean
peerNickname: string peerNickname: string
onRevoke?: (messageId: string) => void onRevoke?: (messageId: string) => void
onEdit?: (message: ImMessage) => void
} }
const SCREEN_W = Dimensions.get('window').width const SCREEN_W = Dimensions.get('window').width
@ -306,15 +308,20 @@ function MessageContent({message}: {message: ImMessage}) {
} }
} }
export function MessageBubble({message, isOwn, peerNickname, onRevoke}: Props) { export function MessageBubble({message, isOwn, peerNickname, onRevoke, onEdit}: Props) {
const canRevoke = isOwn && message.status !== 'REVOKED' && message.msgType !== 'REVOKED' const canRevoke = Boolean(onRevoke) && isOwn && message.status !== 'REVOKED' && message.msgType !== 'REVOKED'
const canEdit = Boolean(onEdit) && isOwn && message.msgType === 'TEXT' && message.status !== 'REVOKED'
const handleLongPress = () => { const handleLongPress = () => {
if (!canRevoke) return if (!canEdit && !canRevoke) return
Alert.alert('操作', '撤回这条消息?', [ const buttons: AlertButton[] = [{text: '取消', style: 'cancel'}]
{text: '取消', style: 'cancel'}, if (canEdit) {
{text: '撤回', style: 'destructive', onPress: () => onRevoke?.(message.id)}, buttons.push({text: '编辑', onPress: () => onEdit?.(message)})
]) }
if (canRevoke) {
buttons.push({text: '撤回', style: 'destructive', onPress: () => onRevoke?.(message.id)})
}
Alert.alert('操作', '请选择要执行的操作', buttons)
} }
const senderLabel = isOwn ? '我' : peerNickname const senderLabel = isOwn ? '我' : peerNickname
@ -333,6 +340,7 @@ export function MessageBubble({message, isOwn, peerNickname, onRevoke}: Props) {
<Text style={styles.metaLine}> <Text style={styles.metaLine}>
{senderLabel} {senderLabel}
{message.status === 'READ' ? ' · 已读' : message.status === 'DELIVERED' ? ' · 已送达' : ''} {message.status === 'READ' ? ' · 已读' : message.status === 'DELIVERED' ? ' · 已送达' : ''}
{message.editedAt ? ' · 已编辑' : ''}
</Text> </Text>
<MessageContent message={message} /> <MessageContent message={message} />
</Pressable> </Pressable>

查看文件

@ -41,6 +41,7 @@ export default function GroupChatScreen({ route, navigation }: Props) {
const [messages, setMessages] = useState<ImMessage[]>([]) const [messages, setMessages] = useState<ImMessage[]>([])
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const [loadingMore, setLoadingMore] = useState(false) const [loadingMore, setLoadingMore] = useState(false)
const [editingMessage, setEditingMessage] = useState<ImMessage | null>(null)
const listRef = useRef<FlatList<ImMessage>>(null) const listRef = useRef<FlatList<ImMessage>>(null)
const loadHistory = useCallback(async (p = 0) => { 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 () => { const loadMore = async () => {
if (loadingMore) return if (loadingMore) return
setLoadingMore(true) setLoadingMore(true)
@ -119,6 +124,7 @@ export default function GroupChatScreen({ route, navigation }: Props) {
isOwn={item.fromUserId === userId} isOwn={item.fromUserId === userId}
peerNickname={item.fromUserId} peerNickname={item.fromUserId}
onRevoke={handleRevoke} onRevoke={handleRevoke}
onEdit={handleEdit}
/> />
)} )}
contentContainerStyle={styles.list} contentContainerStyle={styles.list}
@ -126,7 +132,13 @@ export default function GroupChatScreen({ route, navigation }: Props) {
onEndReached={loadMore} onEndReached={loadMore}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }} maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
/> />
<ChatInput toId={groupId} chatType="GROUP" onSent={onSent} /> <ChatInput
toId={groupId}
chatType="GROUP"
onSent={onSent}
editingMessage={editingMessage}
onCancelEdit={() => setEditingMessage(null)}
/>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
) )

查看文件

@ -29,6 +29,7 @@ export default function SingleChatScreen({ route }: Props) {
const [messages, setMessages] = useState<ImMessage[]>([]) const [messages, setMessages] = useState<ImMessage[]>([])
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const [loadingMore, setLoadingMore] = useState(false) const [loadingMore, setLoadingMore] = useState(false)
const [editingMessage, setEditingMessage] = useState<ImMessage | null>(null)
const listRef = useRef<FlatList<ImMessage>>(null) const listRef = useRef<FlatList<ImMessage>>(null)
const loadHistory = useCallback(async (p = 0) => { 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 () => { const loadMore = async () => {
if (loadingMore) return if (loadingMore) return
setLoadingMore(true) setLoadingMore(true)
@ -102,6 +107,7 @@ export default function SingleChatScreen({ route }: Props) {
isOwn={item.fromUserId === userId} isOwn={item.fromUserId === userId}
peerNickname={targetName} peerNickname={targetName}
onRevoke={handleRevoke} onRevoke={handleRevoke}
onEdit={handleEdit}
/> />
)} )}
contentContainerStyle={styles.list} contentContainerStyle={styles.list}
@ -109,7 +115,13 @@ export default function SingleChatScreen({ route }: Props) {
onEndReached={loadMore} onEndReached={loadMore}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }} maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
/> />
<ChatInput toId={targetId} chatType="SINGLE" onSent={onSent} /> <ChatInput
toId={targetId}
chatType="SINGLE"
onSent={onSent}
editingMessage={editingMessage}
onCancelEdit={() => setEditingMessage(null)}
/>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
) )