XuqmGroup-RNChatDemo/src/screens/chat/SingleChatScreen.tsx
徐勤民 536759c14a feat(app): update check flow, friend-based contacts, global disconnect banner
- 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>
2026-04-25 17:27:22 +08:00

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 },
})