- 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>
81 行
3.1 KiB
TypeScript
81 行
3.1 KiB
TypeScript
import React, { useState } from 'react'
|
|
import {
|
|
View, Text, TextInput, StyleSheet, TouchableOpacity, SafeAreaView,
|
|
ActivityIndicator, Alert, ScrollView,
|
|
} from 'react-native'
|
|
import { useAuth } from '../../context/AuthContext'
|
|
import { useNavigation } from '@react-navigation/native'
|
|
|
|
type Gender = 'UNKNOWN' | 'MALE' | 'FEMALE'
|
|
|
|
export default function EditProfileScreen() {
|
|
const navigation = useNavigation()
|
|
const { profile, updateProfile } = useAuth()
|
|
const [nickname, setNickname] = useState(profile?.nickname ?? '')
|
|
const [gender, setGender] = useState<Gender>(profile?.gender ?? 'UNKNOWN')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const save = async () => {
|
|
const nick = nickname.trim()
|
|
if (!nick) { Alert.alert('提示', '昵称不能为空'); return }
|
|
setLoading(true)
|
|
try {
|
|
await updateProfile({ nickname: nick, gender })
|
|
navigation.goBack()
|
|
} catch (e: any) {
|
|
Alert.alert('失败', e?.message ?? '保存失败,请重试')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.root}>
|
|
<ScrollView contentContainerStyle={styles.inner}>
|
|
<Text style={styles.label}>昵称</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={nickname}
|
|
onChangeText={setNickname}
|
|
placeholder="请输入昵称"
|
|
maxLength={32}
|
|
/>
|
|
|
|
<Text style={styles.label}>性别</Text>
|
|
<View style={styles.genderRow}>
|
|
{(['MALE', 'FEMALE', 'UNKNOWN'] as Gender[]).map(g => {
|
|
const label = { MALE: '男', FEMALE: '女', UNKNOWN: '不设置' }[g]
|
|
return (
|
|
<TouchableOpacity
|
|
key={g}
|
|
style={[styles.genderBtn, gender === g && styles.genderBtnActive]}
|
|
onPress={() => setGender(g)}
|
|
>
|
|
<Text style={[styles.genderBtnText, gender === g && styles.genderBtnTextActive]}>{label}</Text>
|
|
</TouchableOpacity>
|
|
)
|
|
})}
|
|
</View>
|
|
|
|
<TouchableOpacity style={styles.saveBtn} onPress={save} disabled={loading}>
|
|
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveBtnText}>保存</Text>}
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
root: { flex: 1, backgroundColor: '#f5f5f5' },
|
|
inner: { padding: 20 },
|
|
label: { fontSize: 13, color: '#888', marginBottom: 8, marginTop: 16 },
|
|
input: { backgroundColor: '#fff', borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 8, padding: 12, fontSize: 16 },
|
|
genderRow: { flexDirection: 'row', gap: 12 },
|
|
genderBtn: { flex: 1, padding: 12, borderRadius: 8, borderWidth: 1, borderColor: '#e0e0e0', backgroundColor: '#fff', alignItems: 'center' },
|
|
genderBtnActive: { borderColor: '#07C160', backgroundColor: '#e8f9f0' },
|
|
genderBtnText: { fontSize: 15, color: '#555' },
|
|
genderBtnTextActive: { color: '#07C160', fontWeight: '600' },
|
|
saveBtn: { marginTop: 32, backgroundColor: '#07C160', borderRadius: 8, padding: 14, alignItems: 'center' },
|
|
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
})
|