import React, {startTransition, useEffect, useRef, useState} from 'react'; import { Alert, Pressable, ScrollView, StatusBar, StyleSheet, Text, TextInput, useColorScheme, View, } from 'react-native'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; import {ImSDK, type ImEventListener, type ImMessage, UpdateSDK, XuqmSDK} from '@xuqm/rn-sdk'; const APP_ID = 'ak_demo_chat' const MODULE_ID = 'chat-home' const CURRENT_APP_VERSION_CODE = 1 const CURRENT_RN_VERSION = '1.0.0' const API_BASE_URL = 'https://sentry.xuqinmin.com' const IM_WS_URL = 'wss://sentry.xuqinmin.com/ws/im' type DemoUser = { id: string nickname: string } const DEMO_USERS: DemoUser[] = [ {id: 'demo_alice', nickname: 'Alice'}, {id: 'demo_bob', nickname: 'Bob'}, ] function App() { const isDarkMode = useColorScheme() === 'dark' return ( ) } function DemoConsole() { const [currentUser, setCurrentUser] = useState(DEMO_USERS[0]) const [connected, setConnected] = useState(false) const [messages, setMessages] = useState([]) const [draft, setDraft] = useState('你好,我是 RN Demo。') const [activityLog, setActivityLog] = useState([]) const [appUpdateResult, setAppUpdateResult] = useState('点击“检查 App 更新”查看线上版本结果') const [rnUpdateResult, setRnUpdateResult] = useState('点击“检查插件更新”查看热更新结果') const [cachedBundleSummary, setCachedBundleSummary] = useState('暂无本地缓存') const activeUserRef = useRef(currentUser) const peerUser = DEMO_USERS.find(user => user.id !== currentUser.id) ?? DEMO_USERS[1] const appendLog = (message: string) => { startTransition(() => { setActivityLog(prev => [message, ...prev].slice(0, 12)) }) } const mergeMessage = (message: ImMessage) => { startTransition(() => { setMessages(prev => { const exists = prev.some(item => item.id === message.id) if (exists) { return prev.map(item => (item.id === message.id ? message : item)) } return [...prev, message].sort((a, b) => a.createdAt.localeCompare(b.createdAt)) }) }) } const connectUser = async (user: DemoUser) => { try { ImSDK.disconnect() setConnected(false) setMessages([]) await ImSDK.login(user.id, user.nickname) appendLog(`已登录 IM:${user.nickname}`) const history = await ImSDK.fetchHistory(peerUser.id, 0, 50) history .slice() .reverse() .forEach(item => mergeMessage(item)) setConnected(ImSDK.isConnected()) } catch (error) { const message = error instanceof Error ? error.message : 'IM 登录失败' appendLog(`连接失败:${message}`) Alert.alert('IM 登录失败', message) } } useEffect(() => { XuqmSDK.init({ appId: APP_ID, appKey: APP_ID, appSecret: 'demo-secret-not-used-by-current-services', apiBaseUrl: API_BASE_URL, imWsUrl: IM_WS_URL, debug: true, }) }, []) useEffect(() => { activeUserRef.current = currentUser }, [currentUser, peerUser.id]) useEffect(() => { const listener: ImEventListener = { onConnected() { setConnected(true) appendLog(`WebSocket 已连接:${activeUserRef.current.nickname}`) }, onDisconnected(reason) { setConnected(false) appendLog(`WebSocket 断开:${reason || 'unknown'}`) }, onMessage(message) { mergeMessage(message) }, onGroupMessage(message) { mergeMessage(message) }, onError(error) { appendLog(`IM 错误:${error}`) }, } ImSDK.addListener(listener) return () => { ImSDK.removeListener(listener) ImSDK.disconnect() } }, []) useEffect(() => { async function run() { try { ImSDK.disconnect() setConnected(false) setMessages([]) await ImSDK.login(currentUser.id, currentUser.nickname) appendLog(`已登录 IM:${currentUser.nickname}`) const history = await ImSDK.fetchHistory(peerUser.id, 0, 50) history .slice() .reverse() .forEach(item => mergeMessage(item)) setConnected(ImSDK.isConnected()) } catch (error) { const message = error instanceof Error ? error.message : 'IM 登录失败' appendLog(`连接失败:${message}`) Alert.alert('IM 登录失败', message) } } run() }, [currentUser, peerUser.id]) useEffect(() => { UpdateSDK.getCachedRnBundle(MODULE_ID).then(bundle => { if (!bundle) return setCachedBundleSummary(`已缓存 ${bundle.version},md5=${bundle.md5}`) }) }, []) const handleSwitchUser = (user: DemoUser) => { setCurrentUser(user) } const handleSend = async () => { const content = draft.trim() if (!content) { return } try { const sent = await ImSDK.sendMessage(peerUser.id, 'SINGLE', 'TEXT', content) mergeMessage(sent) setDraft('') appendLog(`已发送给 ${peerUser.nickname}`) } catch (error) { const message = error instanceof Error ? error.message : '消息发送失败' appendLog(`发送失败:${message}`) Alert.alert('发送失败', message) } } const handleReloadHistory = async () => { try { const history = await ImSDK.fetchHistory(peerUser.id, 0, 50) setMessages(history.slice().reverse()) appendLog(`历史消息已刷新,共 ${history.length} 条`) } catch (error) { const message = error instanceof Error ? error.message : '历史消息加载失败' appendLog(`历史拉取失败:${message}`) } } const handleCheckAppUpdate = async () => { try { const result = await UpdateSDK.checkAppUpdate(CURRENT_APP_VERSION_CODE) if (!result.needsUpdate) { setAppUpdateResult('当前已是最新 App 版本') appendLog('App 更新检查:当前最新') return } setAppUpdateResult( `发现版本 ${result.versionName}(code=${result.versionCode})\nforce=${String(result.forceUpdate)}\n下载地址:${result.downloadUrl || '未提供'}`, ) appendLog(`发现 App 新版本:${result.versionName}`) } catch (error) { const message = error instanceof Error ? error.message : 'App 更新检查失败' setAppUpdateResult(message) appendLog(`App 更新失败:${message}`) } } const handleCheckRnUpdate = async () => { try { const result = await UpdateSDK.checkRnUpdate(MODULE_ID, CURRENT_RN_VERSION) if (!result.needsUpdate) { setRnUpdateResult('当前已是最新插件版本') appendLog('插件更新检查:当前最新') return } const source = await UpdateSDK.downloadRnBundle(result.downloadUrl) const cached = await UpdateSDK.cacheRnBundle(MODULE_ID, result.latestVersion, result.md5, source) setRnUpdateResult( `发现插件版本 ${result.latestVersion}\nmd5=${result.md5}\n说明:${result.note || '无'}\n已下载源码长度:${source.length}`, ) setCachedBundleSummary(`已缓存 ${cached.version},下载于 ${cached.downloadedAt}`) appendLog(`插件更新已缓存:${cached.version}`) } catch (error) { const message = error instanceof Error ? error.message : '插件更新检查失败' setRnUpdateResult(message) appendLog(`插件更新失败:${message}`) } } return ( XuqmGroup RN SDK Demo 聊天、App 更新、插件热更新一屏演示 这个项目直接连接 {API_BASE_URL},默认使用两个演示用户互聊。 {DEMO_USERS.map(user => { const active = user.id === currentUser.id return ( { handleSwitchUser(user) }} style={[styles.userChip, active && styles.userChipActive]}> {user.nickname} ) })} 当前用户:{currentUser.nickname}({currentUser.id}) 聊天对象:{peerUser.nickname}({peerUser.id}) 连接状态:{connected ? '已连接' : '连接中 / 未连接'} { handleReloadHistory() }} /> { connectUser(currentUser) }} /> {messages.length === 0 ? ( 还没有消息,发一条试试看。 ) : ( messages.map(message => { const mine = message.fromUserId === currentUser.id return ( {mine ? '我' : peerUser.nickname} · {message.msgType} {message.content} ) }) )} { handleSend() }} /> 当前本地版本号写死为 {CURRENT_APP_VERSION_CODE},方便你在服务端发布更高版本后立即观察效果。 { handleCheckAppUpdate() }} /> {appUpdateResult} 当前模块:{MODULE_ID},本地插件版本:{CURRENT_RN_VERSION}。 { handleCheckRnUpdate() }} /> {rnUpdateResult} {cachedBundleSummary} {activityLog.length === 0 ? ( 暂无日志 ) : ( activityLog.map(item => ( {item} )) )} ) } function SectionCard({title, children}: {title: string; children: React.ReactNode}) { return ( {title} {children} ) } function PrimaryButton({title, onPress}: {title: string; onPress: () => void}) { return ( {title} ) } function GhostButton({title, onPress}: {title: string; onPress: () => void}) { return ( {title} ) } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: '#f5efe3', }, container: { flex: 1, backgroundColor: '#f5efe3', }, content: { padding: 18, gap: 14, }, heroEyebrow: { fontSize: 13, fontWeight: '700', color: '#915f34', textTransform: 'uppercase', letterSpacing: 1.1, }, heroTitle: { fontSize: 30, lineHeight: 36, fontWeight: '800', color: '#1f2933', }, heroSubtitle: { fontSize: 15, lineHeight: 22, color: '#4b5563', }, card: { borderRadius: 24, padding: 16, backgroundColor: '#fffaf2', borderWidth: 1, borderColor: '#ead7b7', gap: 12, }, cardTitle: { fontSize: 19, fontWeight: '800', color: '#1f2933', }, userSwitcher: { flexDirection: 'row', gap: 10, }, userChip: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 999, backgroundColor: '#efe2c5', }, userChipActive: { backgroundColor: '#1f2933', }, userChipText: { color: '#4b5563', fontWeight: '700', }, userChipTextActive: { color: '#fffdf8', }, metaText: { fontSize: 14, color: '#4b5563', }, inlineActions: { flexDirection: 'row', gap: 10, }, chatPanel: { maxHeight: 340, borderRadius: 18, backgroundColor: '#f4ecdf', padding: 12, gap: 8, }, bubbleRow: { flexDirection: 'row', }, bubbleRowMine: { justifyContent: 'flex-end', }, bubbleRowPeer: { justifyContent: 'flex-start', }, bubble: { maxWidth: '85%', borderRadius: 18, paddingHorizontal: 12, paddingVertical: 10, gap: 4, }, bubbleMine: { backgroundColor: '#cae7d8', }, bubblePeer: { backgroundColor: '#ffffff', borderWidth: 1, borderColor: '#ddd3c2', }, bubbleMeta: { fontSize: 11, color: '#6b7280', fontWeight: '700', }, bubbleText: { fontSize: 15, lineHeight: 21, color: '#111827', }, input: { minHeight: 48, borderRadius: 16, borderWidth: 1, borderColor: '#dac8a8', backgroundColor: '#fff', paddingHorizontal: 14, color: '#111827', }, paragraph: { fontSize: 14, lineHeight: 21, color: '#4b5563', }, primaryButton: { minHeight: 46, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: '#1f2933', paddingHorizontal: 16, }, primaryButtonText: { color: '#fffdf8', fontSize: 15, fontWeight: '800', }, ghostButton: { minHeight: 46, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: '#efe2c5', paddingHorizontal: 16, }, ghostButtonText: { color: '#4b5563', fontSize: 15, fontWeight: '800', }, resultText: { fontSize: 14, lineHeight: 21, color: '#1f2933', }, cacheText: { fontSize: 13, color: '#6b7280', }, emptyText: { fontSize: 14, color: '#7c7f85', }, logLine: { fontSize: 13, lineHeight: 20, color: '#374151', }, }) export default App