2026-04-25 17:27:22 +08:00
|
|
|
|
import React, { useEffect, useState, useCallback } from 'react'
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View, Text, FlatList, StyleSheet, TouchableOpacity, SafeAreaView,
|
2026-04-27 11:58:07 +08:00
|
|
|
|
ActivityIndicator, Image,
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
} from 'react-native'
|
2026-04-25 17:27:22 +08:00
|
|
|
|
import { useNavigation, useFocusEffect } from '@react-navigation/native'
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
|
|
|
|
|
import type { RootStackParams } from '../../navigation/types'
|
2026-04-25 17:27:22 +08:00
|
|
|
|
import { ImSDK } from '@xuqm/rn-sdk'
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
import { demoApi, type UserProfile } from '../../api/demo'
|
|
|
|
|
|
import { load, save, K } from '../../utils/storage'
|
|
|
|
|
|
|
|
|
|
|
|
type Nav = NativeStackNavigationProp<RootStackParams>
|
|
|
|
|
|
|
|
|
|
|
|
function ContactRow({ user, onPress }: { user: UserProfile; onPress(): void }) {
|
|
|
|
|
|
const letter = (user.nickname || user.userId).charAt(0).toUpperCase()
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TouchableOpacity style={styles.row} onPress={onPress} activeOpacity={0.7}>
|
2026-04-27 11:58:07 +08:00
|
|
|
|
{user.avatar ? (
|
|
|
|
|
|
<Image source={{ uri: user.avatar }} style={styles.avatar} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={[styles.avatar, styles.avatarFallback]}>
|
|
|
|
|
|
<Text style={styles.avatarText}>{letter}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
<View style={styles.body}>
|
|
|
|
|
|
<Text style={styles.name}>{user.nickname}</Text>
|
|
|
|
|
|
<Text style={styles.uid}>@{user.userId}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text style={styles.arrow}>›</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function ContactsScreen() {
|
|
|
|
|
|
const navigation = useNavigation<Nav>()
|
|
|
|
|
|
const [contacts, setContacts] = useState<UserProfile[]>([])
|
2026-04-25 17:27:22 +08:00
|
|
|
|
const [loading, setLoading] = useState(false)
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
|
2026-04-25 17:27:22 +08:00
|
|
|
|
const fetchContacts = useCallback(async () => {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const friendIds = await ImSDK.listFriends()
|
|
|
|
|
|
const profiles: UserProfile[] = []
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
|
friendIds.map(async (id) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const results = await demoApi.searchUsers(id)
|
|
|
|
|
|
const match = results.find(u => u.userId === id)
|
|
|
|
|
|
if (match) profiles.push(match)
|
|
|
|
|
|
} catch {/* skip individual failures */}
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
setContacts(profiles)
|
|
|
|
|
|
await save(K.CONTACTS, profiles)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// network failed — load from local cache
|
|
|
|
|
|
const cached = await load<UserProfile[]>(K.CONTACTS)
|
|
|
|
|
|
if (cached) setContacts(cached)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
}, [])
|
|
|
|
|
|
|
2026-04-25 17:27:22 +08:00
|
|
|
|
useFocusEffect(
|
|
|
|
|
|
useCallback(() => {
|
|
|
|
|
|
fetchContacts()
|
|
|
|
|
|
}, [fetchContacts]),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
const openChat = (user: UserProfile) => {
|
|
|
|
|
|
navigation.navigate('SingleChat', { targetId: user.userId, targetName: user.nickname, targetAvatar: user.avatar })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.root}>
|
|
|
|
|
|
<View style={styles.header}>
|
|
|
|
|
|
<Text style={styles.title}>通讯录</Text>
|
|
|
|
|
|
<View style={styles.headerActions}>
|
|
|
|
|
|
<TouchableOpacity onPress={() => navigation.navigate('GroupList')} style={styles.headerBtn}>
|
|
|
|
|
|
<Text style={styles.headerBtnText}>群聊</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
<TouchableOpacity onPress={() => navigation.navigate('UserSearch')} style={styles.headerBtn}>
|
|
|
|
|
|
<Text style={styles.headerBtnText}>添加</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
2026-04-25 17:27:22 +08:00
|
|
|
|
{loading && <ActivityIndicator style={styles.loadingIndicator} color="#07C160" />}
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
<FlatList
|
|
|
|
|
|
data={contacts}
|
|
|
|
|
|
keyExtractor={u => u.userId}
|
|
|
|
|
|
renderItem={({ item }) => <ContactRow user={item} onPress={() => openChat(item)} />}
|
|
|
|
|
|
ItemSeparatorComponent={() => <View style={styles.sep} />}
|
|
|
|
|
|
ListEmptyComponent={
|
2026-04-25 17:27:22 +08:00
|
|
|
|
!loading ? (
|
|
|
|
|
|
<View style={styles.empty}>
|
|
|
|
|
|
<Text style={styles.emptyText}>还没有联系人</Text>
|
|
|
|
|
|
<TouchableOpacity onPress={() => navigation.navigate('UserSearch')}>
|
|
|
|
|
|
<Text style={styles.emptyLink}>搜索添加</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : null
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
|
root: { flex: 1, backgroundColor: '#f5f5f5' },
|
|
|
|
|
|
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#e0e0e0' },
|
|
|
|
|
|
title: { fontSize: 20, fontWeight: '700', color: '#111' },
|
|
|
|
|
|
headerActions: { flexDirection: 'row', gap: 12 },
|
|
|
|
|
|
headerBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#07C160', borderRadius: 6 },
|
|
|
|
|
|
headerBtnText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
2026-04-25 17:27:22 +08:00
|
|
|
|
loadingIndicator: { marginVertical: 8 },
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
row: { flexDirection: 'row', alignItems: 'center', padding: 12, backgroundColor: '#fff' },
|
2026-04-27 11:58:07 +08:00
|
|
|
|
avatar: { width: 46, height: 46, borderRadius: 8, marginRight: 12 },
|
|
|
|
|
|
avatarFallback: { backgroundColor: '#07C160', alignItems: 'center', justifyContent: 'center' },
|
feat: complete production chat demo — all screens, real media, SDK remote init
- App.tsx: replaced dev console with AuthProvider + AppNavigator
- Auth: Login, Register, ResetPassword screens via demo-service
- Conversations: reactive WatermelonDB subscription with user profile enrichment
- Chat: SingleChat + GroupChat with full media (image/video/audio/file), revoke, pull-up load more
- Contacts: local contacts list + UserSearch with debounced fuzzy search
- Groups: GroupList, CreateGroup (fuzzy member picker), GroupMembers, GroupSettings
- Profile: view + EditProfile (nickname, gender)
- MessageSearch: local DB full-text search across conversations
- ChatInput: text, 20-emoji picker, image/video/audio/file send, tap-to-record audio
- DisconnectBanner: connection status with reconnect
- AuthContext: uses await XuqmSDK.initialize({ appId, serverUrl }) for remote config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:54 +08:00
|
|
|
|
avatarText: { color: '#fff', fontSize: 18, fontWeight: '600' },
|
|
|
|
|
|
body: { flex: 1 },
|
|
|
|
|
|
name: { fontSize: 16, fontWeight: '500', color: '#111' },
|
|
|
|
|
|
uid: { fontSize: 13, color: '#888', marginTop: 2 },
|
|
|
|
|
|
arrow: { color: '#ccc', fontSize: 20 },
|
|
|
|
|
|
sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#e0e0e0', marginLeft: 70 },
|
|
|
|
|
|
empty: { alignItems: 'center', paddingTop: 80 },
|
|
|
|
|
|
emptyText: { color: '#bbb', fontSize: 15 },
|
|
|
|
|
|
emptyLink: { color: '#07C160', fontSize: 14, marginTop: 12 },
|
|
|
|
|
|
})
|