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>
这个提交包含在:
父节点
e5adc00b44
当前提交
536759c14a
@ -1,7 +1,9 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { XuqmSDK, ImSDK } from '@xuqm/rn-sdk'
|
||||
import { Alert, Linking } from 'react-native'
|
||||
import { XuqmSDK, ImSDK, UpdateSDK } from '@xuqm/rn-sdk'
|
||||
import { demoApi, type UserProfile } from '../api/demo'
|
||||
import { save, load, clearSession, K } from '../utils/storage'
|
||||
import pluginMeta from '../../plugin.json'
|
||||
|
||||
const APP_ID = 'ak_demo_chat'
|
||||
const SERVER_URL = 'https://sentry.xuqinmin.com'
|
||||
@ -29,6 +31,23 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
await XuqmSDK.initialize({ appId: APP_ID, serverUrl: SERVER_URL })
|
||||
await ImSDK.loginWithToken(userId, imToken, 'xuqm_im')
|
||||
setState({ ready: true, userId, profile })
|
||||
|
||||
UpdateSDK.checkAppUpdate()
|
||||
.then(update => {
|
||||
if (update) {
|
||||
Alert.alert(
|
||||
'发现新版本',
|
||||
`${update.versionName} 是否立即更新?`,
|
||||
[
|
||||
{ text: '稍后', style: 'cancel' },
|
||||
{ text: '立即更新', onPress: () => Linking.openURL(update.downloadUrl) },
|
||||
],
|
||||
)
|
||||
} else {
|
||||
UpdateSDK.checkRnUpdate(pluginMeta.moduleId).catch(() => {})
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -22,6 +22,7 @@ import GroupMembersScreen from '../screens/group/GroupMembersScreen'
|
||||
import GroupSettingsScreen from '../screens/group/GroupSettingsScreen'
|
||||
import EditProfileScreen from '../screens/profile/EditProfileScreen'
|
||||
import MessageSearchScreen from '../screens/chat/MessageSearchScreen'
|
||||
import DisconnectBanner from '../components/DisconnectBanner'
|
||||
|
||||
const AuthStack = createNativeStackNavigator<AuthStackParams>()
|
||||
const Tab = createBottomTabNavigator<MainTabParams>()
|
||||
@ -55,6 +56,7 @@ function MainTabs() {
|
||||
function AppStack() {
|
||||
return (
|
||||
<IMProvider>
|
||||
<DisconnectBanner />
|
||||
<Root.Navigator screenOptions={{ headerStyle: { backgroundColor: '#fff' }, headerTintColor: '#333' }}>
|
||||
<Root.Screen name="Main" component={MainTabs} options={{ headerShown: false }} />
|
||||
<Root.Screen name="SingleChat" component={SingleChatScreen} options={({ route }) => ({ title: route.params.targetName })} />
|
||||
|
||||
@ -10,7 +10,6 @@ import { useAuth } from '../../context/AuthContext'
|
||||
import type { RootStackParams } from '../../navigation/types'
|
||||
import { MessageBubble } from '../../components/MessageBubble'
|
||||
import ChatInput from '../../components/ChatInput'
|
||||
import DisconnectBanner from '../../components/DisconnectBanner'
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParams, 'GroupChat'>
|
||||
|
||||
@ -85,7 +84,6 @@ export default function GroupChatScreen({ route, navigation }: Props) {
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.root}>
|
||||
<DisconnectBanner />
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
|
||||
@ -10,7 +10,6 @@ import { useAuth } from '../../context/AuthContext'
|
||||
import type { RootStackParams } from '../../navigation/types'
|
||||
import { MessageBubble } from '../../components/MessageBubble'
|
||||
import ChatInput from '../../components/ChatInput'
|
||||
import DisconnectBanner from '../../components/DisconnectBanner'
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParams, 'SingleChat'>
|
||||
|
||||
@ -86,7 +85,6 @@ export default function SingleChatScreen({ route, navigation }: Props) {
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.root}>
|
||||
<DisconnectBanner />
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
View, Text, FlatList, StyleSheet, TouchableOpacity, SafeAreaView,
|
||||
TextInput, Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native'
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import type { RootStackParams } from '../../navigation/types'
|
||||
import { ImSDK } from '@xuqm/rn-sdk'
|
||||
import { demoApi, type UserProfile } from '../../api/demo'
|
||||
import { load, save, K } from '../../utils/storage'
|
||||
|
||||
@ -28,11 +29,39 @@ function ContactRow({ user, onPress }: { user: UserProfile; onPress(): void }) {
|
||||
export default function ContactsScreen() {
|
||||
const navigation = useNavigation<Nav>()
|
||||
const [contacts, setContacts] = useState<UserProfile[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
load<UserProfile[]>(K.CONTACTS).then(c => { if (c) setContacts(c) })
|
||||
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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchContacts()
|
||||
}, [fetchContacts]),
|
||||
)
|
||||
|
||||
const openChat = (user: UserProfile) => {
|
||||
navigation.navigate('SingleChat', { targetId: user.userId, targetName: user.nickname, targetAvatar: user.avatar })
|
||||
}
|
||||
@ -50,18 +79,21 @@ export default function ContactsScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{loading && <ActivityIndicator style={styles.loadingIndicator} color="#07C160" />}
|
||||
<FlatList
|
||||
data={contacts}
|
||||
keyExtractor={u => u.userId}
|
||||
renderItem={({ item }) => <ContactRow user={item} onPress={() => openChat(item)} />}
|
||||
ItemSeparatorComponent={() => <View style={styles.sep} />}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<Text style={styles.emptyText}>还没有联系人</Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('UserSearch')}>
|
||||
<Text style={styles.emptyLink}>搜索添加</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
!loading ? (
|
||||
<View style={styles.empty}>
|
||||
<Text style={styles.emptyText}>还没有联系人</Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('UserSearch')}>
|
||||
<Text style={styles.emptyLink}>搜索添加</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
@ -75,6 +107,7 @@ const styles = StyleSheet.create({
|
||||
headerActions: { flexDirection: 'row', gap: 12 },
|
||||
headerBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#07C160', borderRadius: 6 },
|
||||
headerBtnText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
||||
loadingIndicator: { marginVertical: 8 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', padding: 12, backgroundColor: '#fff' },
|
||||
avatar: { width: 46, height: 46, borderRadius: 8, backgroundColor: '#07C160', alignItems: 'center', justifyContent: 'center', marginRight: 12 },
|
||||
avatarText: { color: '#fff', fontSize: 18, fontWeight: '600' },
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
} from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { ImSDK } from '@xuqm/rn-sdk'
|
||||
import { demoApi, type UserProfile } from '../../api/demo'
|
||||
import { load, save, K } from '../../utils/storage'
|
||||
import type { RootStackParams } from '../../navigation/types'
|
||||
@ -37,13 +38,14 @@ export default function UserSearchScreen() {
|
||||
|
||||
const addContact = async (user: UserProfile) => {
|
||||
try {
|
||||
await ImSDK.addFriend(user.userId)
|
||||
const current = (await load<UserProfile[]>(K.CONTACTS)) ?? []
|
||||
if (!current.find(c => c.userId === user.userId)) {
|
||||
await save(K.CONTACTS, [...current, user])
|
||||
}
|
||||
Alert.alert('已添加', `${user.nickname} 已添加到通讯录`)
|
||||
Alert.alert('已添加为好友')
|
||||
} catch {
|
||||
Alert.alert('失败', '添加失败,请重试')
|
||||
Alert.alert('添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户