- AuthContext: post-login checkAppUpdate + silent RN update - ContactsScreen: ImSDK.listFriends + demoApi profiles, useFocusEffect - UserSearchScreen: addFriend via ImSDK - DisconnectBanner moved to AppStack level (global, not per-screen) - Remove DisconnectBanner from SingleChatScreen and GroupChatScreen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 行
4.0 KiB
TypeScript
121 行
4.0 KiB
TypeScript
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
import {
|
|
View, FlatList, StyleSheet, Alert, SafeAreaView,
|
|
KeyboardAvoidingView, Platform, TouchableOpacity, Text,
|
|
} from 'react-native'
|
|
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
import { ImSDK } from '@xuqm/rn-sdk'
|
|
import type { ImMessage, ImEventListener } from '@xuqm/rn-sdk'
|
|
import { useAuth } from '../../context/AuthContext'
|
|
import type { RootStackParams } from '../../navigation/types'
|
|
import { MessageBubble } from '../../components/MessageBubble'
|
|
import ChatInput from '../../components/ChatInput'
|
|
|
|
type Props = NativeStackScreenProps<RootStackParams, 'SingleChat'>
|
|
|
|
export default function SingleChatScreen({ route, navigation }: Props) {
|
|
const { targetId, targetName, targetAvatar } = route.params
|
|
const { userId, profile } = useAuth()
|
|
const [messages, setMessages] = useState<ImMessage[]>([])
|
|
const [page, setPage] = useState(0)
|
|
const [loadingMore, setLoadingMore] = useState(false)
|
|
const listRef = useRef<FlatList<ImMessage>>(null)
|
|
|
|
const loadHistory = useCallback(async (p = 0) => {
|
|
try {
|
|
const history = await ImSDK.fetchHistory(targetId, p, 30)
|
|
setMessages(prev => p === 0 ? history : [...history, ...prev])
|
|
setPage(p)
|
|
} catch {/* ignore */}
|
|
}, [targetId])
|
|
|
|
useEffect(() => {
|
|
loadHistory(0)
|
|
|
|
const listener: ImEventListener = {
|
|
onMessage(msg) {
|
|
if ((msg.fromUserId === targetId && msg.toId === userId) ||
|
|
(msg.fromUserId === userId && msg.toId === targetId)) {
|
|
setMessages(prev => {
|
|
if (prev.find(m => m.id === msg.id)) return prev
|
|
return [...prev, msg]
|
|
})
|
|
setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100)
|
|
}
|
|
},
|
|
}
|
|
ImSDK.addListener(listener)
|
|
ImSDK.markRead(targetId).catch(() => {})
|
|
|
|
return () => ImSDK.removeListener(listener)
|
|
}, [targetId, userId, loadHistory])
|
|
|
|
const onSent = (msg: ImMessage) => {
|
|
setMessages(prev => {
|
|
if (prev.find(m => m.id === msg.id)) return prev
|
|
return [...prev, msg]
|
|
})
|
|
setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100)
|
|
}
|
|
|
|
const handleRevoke = async (messageId: string) => {
|
|
try {
|
|
const revoked = await ImSDK.revokeMessage(messageId)
|
|
setMessages(prev => prev.map(m => m.id === revoked.id ? revoked : m))
|
|
} catch (e: any) {
|
|
Alert.alert('撤回失败', e?.message ?? '操作失败')
|
|
}
|
|
}
|
|
|
|
const handleLongPress = (msg: ImMessage) => {
|
|
if (msg.fromUserId !== userId) return
|
|
if (msg.status === 'REVOKED' || msg.msgType === 'REVOKED') return
|
|
Alert.alert('操作', '撤回这条消息?', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{ text: '撤回', style: 'destructive', onPress: () => handleRevoke(msg.id) },
|
|
])
|
|
}
|
|
|
|
const loadMore = async () => {
|
|
if (loadingMore) return
|
|
setLoadingMore(true)
|
|
await loadHistory(page + 1)
|
|
setLoadingMore(false)
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.root}>
|
|
<KeyboardAvoidingView
|
|
style={styles.flex}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
|
|
>
|
|
<FlatList
|
|
ref={listRef}
|
|
data={messages}
|
|
keyExtractor={m => m.id}
|
|
renderItem={({ item }) => (
|
|
<MessageBubble
|
|
message={item}
|
|
isOwn={item.fromUserId === userId}
|
|
peerNickname={targetName}
|
|
onRevoke={handleRevoke}
|
|
/>
|
|
)}
|
|
contentContainerStyle={styles.list}
|
|
onEndReachedThreshold={0.1}
|
|
onEndReached={loadMore}
|
|
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
|
|
/>
|
|
<ChatInput toId={targetId} chatType="SINGLE" onSent={onSent} />
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
root: { flex: 1, backgroundColor: '#f0ede8' },
|
|
flex: { flex: 1 },
|
|
list: { padding: 8, paddingBottom: 4 },
|
|
})
|