XuqmGroup-RNChatDemo/src/screens/contact/FriendRequestsScreen.tsx
徐勤民 069f3454fe feat(chat): 添加聊天界面和文件更新SDK功能
- 实现完整的聊天界面UI组件,支持文本、图片、视频、音频、文件等多种消息类型
- 集成IM消息收发功能,实现消息气泡显示和用户头像占位符
- 添加媒体文件选择和拍摄功能,支持相册图片、视频及相机拍照录像
- 实现语音录制和播放功能,包含按住说话交互和权限处理
- 添加群组提及功能,支持@用户和提及候选列表显示
- 实现消息回复和引用功能,支持消息长按回复操作
- 添加本地消息搜索功能,支持搜索当前会话的历史消息
- 实现文件上传下载功能,集成FileSDK进行文件传输管理
- 添加应用更新检查功能,集成UpdateSDK支持版本更新
- 实现消息状态显示,包括发送、送达、已读等状态标识
- 添加群组已读人数统计,显示消息在群聊中的阅读情况
- 实现草稿保存和恢复功能,支持断点续聊体验
- 添加连接状态横幅,实时显示IM服务连接状态
- 实现滚动加载更多历史消息,优化大量消息的性能表现
- 添加多媒体文件下载保存功能,支持保存到应用专属目录
2026-04-28 20:11:38 +08:00

135 行
4.5 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import React, { useCallback, useState } from 'react'
import {
View, Text, FlatList, StyleSheet, TouchableOpacity, SafeAreaView,
ActivityIndicator, Alert,
} from 'react-native'
import { useFocusEffect } from '@react-navigation/native'
import { ImSDK, type FriendRequest } from '@xuqm/rn-sdk'
import type { UserProfile } from '../../api/demo'
import { toDemoUserProfile } from '../../utils/userProfiles'
function RequestRow({
request,
profile,
onAccept,
onReject,
}: {
request: FriendRequest
profile?: UserProfile
onAccept(): void
onReject(): void
}) {
const name = profile?.nickname || request.fromUserId
return (
<View style={styles.row}>
<View style={styles.body}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.uid}>@{request.fromUserId}</Text>
{!!request.remark && <Text style={styles.remark}>{request.remark}</Text>}
<Text style={styles.time}>{request.status}</Text>
</View>
<View style={styles.actions}>
<TouchableOpacity style={[styles.actionBtn, styles.acceptBtn]} onPress={onAccept}>
<Text style={styles.actionText}></Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionBtn, styles.rejectBtn]} onPress={onReject}>
<Text style={styles.actionText}></Text>
</TouchableOpacity>
</View>
</View>
)
}
export default function FriendRequestsScreen() {
const [requests, setRequests] = useState<FriendRequest[]>([])
const [loading, setLoading] = useState(true)
const [profiles, setProfiles] = useState<Record<string, UserProfile>>({})
const load = useCallback(async () => {
setLoading(true)
try {
const list = await ImSDK.listFriendRequests('incoming')
setRequests(list)
const nextProfiles: Record<string, UserProfile> = {}
await Promise.all(list.map(async (req) => {
try {
const result = await ImSDK.searchUsers(req.fromUserId)
const match = result.find(u => u.userId === req.fromUserId)
if (match) nextProfiles[req.fromUserId] = toDemoUserProfile(match)
} catch {
/* ignore */
}
}))
setProfiles(nextProfiles)
} catch {
setRequests([])
setProfiles({})
} finally {
setLoading(false)
}
}, [])
useFocusEffect(useCallback(() => { load() }, [load]))
const handleAccept = async (request: FriendRequest) => {
try {
await ImSDK.acceptFriendRequest(request.id)
await load()
} catch (e: any) {
Alert.alert('处理失败', e?.message ?? '请稍后重试')
}
}
const handleReject = async (request: FriendRequest) => {
try {
await ImSDK.rejectFriendRequest(request.id)
await load()
} catch (e: any) {
Alert.alert('处理失败', e?.message ?? '请稍后重试')
}
}
return (
<SafeAreaView style={styles.root}>
{loading ? (
<ActivityIndicator style={{ flex: 1 }} color="#07C160" />
) : (
<FlatList
data={requests}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<RequestRow
request={item}
profile={profiles[item.fromUserId]}
onAccept={() => handleAccept(item)}
onReject={() => handleReject(item)}
/>
)}
ItemSeparatorComponent={() => <View style={styles.sep} />}
ListEmptyComponent={<View style={styles.empty}><Text style={styles.emptyText}></Text></View>}
contentContainerStyle={requests.length === 0 ? styles.emptyContainer : undefined}
/>
)}
</SafeAreaView>
)
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: '#f5f5f5' },
row: { flexDirection: 'row', padding: 12, backgroundColor: '#fff', alignItems: 'center' },
body: { flex: 1, paddingRight: 8 },
name: { fontSize: 16, fontWeight: '600', color: '#111' },
uid: { fontSize: 13, color: '#888', marginTop: 2 },
remark: { fontSize: 13, color: '#555', marginTop: 4 },
time: { fontSize: 12, color: '#999', marginTop: 4 },
actions: { gap: 8 },
actionBtn: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6 },
acceptBtn: { backgroundColor: '#07C160' },
rejectBtn: { backgroundColor: '#ff3b30' },
actionText: { color: '#fff', fontSize: 13, fontWeight: '600' },
sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#e0e0e0', marginLeft: 12 },
emptyContainer: { flexGrow: 1 },
empty: { flex: 1, alignItems: 'center', justifyContent: 'center' },
emptyText: { color: '#bbb', fontSize: 15 },
})