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 React, { useState, useCallback, useRef } from 'react'
|
|
|
|
|
import {
|
|
|
|
|
View, Text, TextInput, FlatList, StyleSheet, TouchableOpacity,
|
2026-04-27 11:58:07 +08:00
|
|
|
SafeAreaView, ActivityIndicator, Alert, 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'
|
|
|
|
|
import { useNavigation } from '@react-navigation/native'
|
|
|
|
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
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'
|
|
|
|
|
import type { RootStackParams } from '../../navigation/types'
|
|
|
|
|
|
|
|
|
|
type Nav = NativeStackNavigationProp<RootStackParams>
|
|
|
|
|
|
|
|
|
|
export default function UserSearchScreen() {
|
|
|
|
|
const navigation = useNavigation<Nav>()
|
|
|
|
|
const [keyword, setKeyword] = useState('')
|
|
|
|
|
const [results, setResults] = useState<UserProfile[]>([])
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
|
|
|
|
|
|
const search = useCallback((text: string) => {
|
|
|
|
|
setKeyword(text)
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
|
|
|
if (!text.trim()) { setResults([]); return }
|
|
|
|
|
debounceRef.current = setTimeout(async () => {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
try {
|
|
|
|
|
const res = await demoApi.searchUsers(text.trim())
|
|
|
|
|
setResults(res)
|
|
|
|
|
} catch {
|
|
|
|
|
setResults([])
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, 300)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const addContact = async (user: UserProfile) => {
|
|
|
|
|
try {
|
2026-04-25 17:27:22 +08:00
|
|
|
await ImSDK.addFriend(user.userId)
|
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 current = (await load<UserProfile[]>(K.CONTACTS)) ?? []
|
|
|
|
|
if (!current.find(c => c.userId === user.userId)) {
|
|
|
|
|
await save(K.CONTACTS, [...current, user])
|
|
|
|
|
}
|
2026-04-25 17:27:22 +08:00
|
|
|
Alert.alert('已添加为好友')
|
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
|
|
|
} catch {
|
2026-04-25 17:27:22 +08:00
|
|
|
Alert.alert('添加失败')
|
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.searchBar}>
|
|
|
|
|
<TextInput
|
|
|
|
|
style={styles.input}
|
|
|
|
|
placeholder="搜索用户名或昵称"
|
|
|
|
|
value={keyword}
|
|
|
|
|
onChangeText={search}
|
|
|
|
|
autoFocus
|
|
|
|
|
clearButtonMode="while-editing"
|
|
|
|
|
/>
|
|
|
|
|
{loading && <ActivityIndicator style={styles.spinner} color="#07C160" />}
|
|
|
|
|
</View>
|
|
|
|
|
<FlatList
|
|
|
|
|
data={results}
|
|
|
|
|
keyExtractor={u => u.userId}
|
|
|
|
|
renderItem={({ item }) => {
|
|
|
|
|
const letter = (item.nickname || item.userId).charAt(0).toUpperCase()
|
|
|
|
|
return (
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
<TouchableOpacity style={styles.rowBody} onPress={() => openChat(item)}>
|
2026-04-27 11:58:07 +08:00
|
|
|
{item.avatar ? (
|
|
|
|
|
<Image source={{ uri: item.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>
|
|
|
|
|
<Text style={styles.name}>{item.nickname}</Text>
|
|
|
|
|
<Text style={styles.uid}>@{item.userId}</Text>
|
|
|
|
|
</View>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
<TouchableOpacity style={styles.addBtn} onPress={() => addContact(item)}>
|
|
|
|
|
<Text style={styles.addBtnText}>+ 添加</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</View>
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
ItemSeparatorComponent={() => <View style={styles.sep} />}
|
|
|
|
|
ListEmptyComponent={
|
|
|
|
|
keyword.trim() && !loading
|
|
|
|
|
? <View style={styles.empty}><Text style={styles.emptyText}>未找到用户</Text></View>
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
root: { flex: 1, backgroundColor: '#f5f5f5' },
|
|
|
|
|
searchBar: { flexDirection: 'row', alignItems: 'center', margin: 12, backgroundColor: '#fff', borderRadius: 8, paddingHorizontal: 12, borderWidth: 1, borderColor: '#e0e0e0' },
|
|
|
|
|
input: { flex: 1, fontSize: 16, paddingVertical: 10 },
|
|
|
|
|
spinner: { marginLeft: 8 },
|
|
|
|
|
row: { flexDirection: 'row', alignItems: 'center', padding: 12, backgroundColor: '#fff' },
|
|
|
|
|
rowBody: { flex: 1, flexDirection: 'row', alignItems: 'center' },
|
2026-04-27 11:58:07 +08:00
|
|
|
avatar: { width: 44, height: 44, 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' },
|
|
|
|
|
name: { fontSize: 16, fontWeight: '500', color: '#111' },
|
|
|
|
|
uid: { fontSize: 13, color: '#888' },
|
|
|
|
|
addBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#07C160', borderRadius: 6 },
|
|
|
|
|
addBtnText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
|
|
|
|
sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#e0e0e0', marginLeft: 68 },
|
|
|
|
|
empty: { alignItems: 'center', paddingTop: 60 },
|
|
|
|
|
emptyText: { color: '#bbb', fontSize: 15 },
|
|
|
|
|
})
|