import React, {startTransition, useEffect, useRef, useState} from 'react' import { Alert, Pressable, ScrollView, StatusBar, StyleSheet, Text, useColorScheme, View, } from 'react-native' import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context' import {ImSDK, type ImEventListener, type ImMessage, type MsgType, XuqmSDK} from '@xuqm/rn-sdk' import {MessageBubble} from './src/components/MessageBubble' import {DEMO_CONTENT, MessageComposer} from './src/components/MessageComposer' import {GroupChatPanel} from './src/components/GroupChatPanel' import {UpdatePanel} from './src/components/UpdatePanel' 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'}, ] const ALL_DEMO_TYPES: MsgType[] = [ 'TEXT', 'IMAGE', 'VIDEO', 'AUDIO', 'FILE', 'CUSTOM', 'LOCATION', 'NOTIFY', 'RICH_TEXT', 'CALL_AUDIO', 'CALL_VIDEO', 'FORWARD', ] function App() { const isDarkMode = useColorScheme() === 'dark' return ( ) } function DemoConsole() { const [currentUser, setCurrentUser] = useState(DEMO_USERS[0]) const [connected, setConnected] = useState(false) const [singleMessages, setSingleMessages] = useState([]) const [groupMessages, setGroupMessages] = useState([]) const [sending, setSending] = useState(false) const [activityLog, setActivityLog] = useState([]) const activeUserRef = useRef(currentUser) const peerUser = DEMO_USERS.find(u => u.id !== currentUser.id) ?? DEMO_USERS[1] const appendLog = (msg: string) => { startTransition(() => { setActivityLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev].slice(0, 30), ) }) } const mergeSingle = (message: ImMessage) => { startTransition(() => { setSingleMessages(prev => { const exists = prev.some(m => m.id === message.id) if (exists) return prev.map(m => m.id === message.id ? message : m) return [...prev, message].sort((a, b) => a.createdAt.localeCompare(b.createdAt)) }) }) } const mergeGroup = (message: ImMessage) => { startTransition(() => { setGroupMessages(prev => { const exists = prev.some(m => m.id === message.id) if (exists) return prev.map(m => m.id === message.id ? message : m) return [...prev, message].sort((a, b) => a.createdAt.localeCompare(b.createdAt)) }) }) } 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]) useEffect(() => { const listener: ImEventListener = { onConnected() { setConnected(true) appendLog(`WebSocket 已连接:${activeUserRef.current.nickname}`) }, onDisconnected(reason) { setConnected(false) appendLog(`WebSocket 断开:${reason || 'unknown'}`) }, onMessage(msg) { if (msg.chatType === 'SINGLE') mergeSingle(msg) else mergeGroup(msg) appendLog(`收到 [${msg.chatType}/${msg.msgType}] from ${msg.fromUserId}`) }, onGroupMessage(msg) { mergeGroup(msg) appendLog(`收到群消息 [${msg.msgType}]`) }, onError(error) { appendLog(`IM 错误:${error}`) }, } ImSDK.addListener(listener) return () => { ImSDK.removeListener(listener) ImSDK.disconnect() } }, []) useEffect(() => { const run = async () => { try { ImSDK.disconnect() setConnected(false) setSingleMessages([]) await ImSDK.login(currentUser.id, currentUser.nickname) appendLog(`已登录 IM:${currentUser.nickname}`) const history = await ImSDK.fetchHistory(peerUser.id, 0, 50) history.slice().reverse().forEach(m => mergeSingle(m)) setConnected(ImSDK.isConnected()) } catch (e) { const msg = e instanceof Error ? e.message : 'IM 登录失败' appendLog(`连接失败:${msg}`) Alert.alert('IM 登录失败', msg) } } run() }, [currentUser, peerUser.id]) const handleSendSingle = async (msgType: MsgType, content: string) => { setSending(true) try { const sent = await ImSDK.sendMessage(peerUser.id, 'SINGLE', msgType, content) mergeSingle(sent) appendLog(`已发送 [${msgType}] 给 ${peerUser.nickname}`) } catch (e) { const msg = e instanceof Error ? e.message : '发送失败' appendLog(`发送失败 [${msgType}]:${msg}`) Alert.alert('发送失败', msg) } finally { setSending(false) } } const handleSendAllSingle = async () => { setSending(true) appendLog(`开始发送全部 ${ALL_DEMO_TYPES.length} 种消息类型(单聊)…`) for (const type of ALL_DEMO_TYPES) { const content = DEMO_CONTENT[type] if (!content) continue try { const sent = await ImSDK.sendMessage(peerUser.id, 'SINGLE', type, content) mergeSingle(sent) appendLog(`✓ [${type}]`) await new Promise(resolve => setTimeout(resolve, 300)) } catch (e) { appendLog(`✗ [${type}]:${e instanceof Error ? e.message : 'error'}`) } } appendLog('单聊全类型演示完成') setSending(false) } const handleRevokeSingle = async (messageId: string) => { try { const revoked = await ImSDK.revokeMessage(messageId) mergeSingle(revoked) appendLog(`消息已撤回:${messageId}`) } catch (e) { const msg = e instanceof Error ? e.message : '撤回失败' Alert.alert('撤回失败', msg) } } const handleReloadSingle = async () => { try { const history = await ImSDK.fetchHistory(peerUser.id, 0, 50) setSingleMessages(history.slice().reverse()) appendLog(`历史消息已刷新,共 ${history.length} 条`) } catch (e) { appendLog(`历史拉取失败:${e instanceof Error ? e.message : 'error'}`) } } return ( XuqmGroup RN SDK Demo IM · 全消息类型 · 群聊 · App 更新 · 热更新 连接 {API_BASE_URL},演示单聊/群聊全消息类型、整包更新和 RN 插件热更新完整流程。 {/* Section 1: 当前会话 */} {DEMO_USERS.map(user => { const active = user.id === currentUser.id return ( setCurrentUser(user)} style={[styles.userChip, active && styles.userChipActive]}> {user.nickname} ) })} 当前用户:{currentUser.nickname}({currentUser.id}) 单聊对象:{peerUser.nickname}({peerUser.id}) {connected ? 'IM 已连接' : '连接中 / 未连接'} { ImSDK.disconnect() setConnected(false) setSingleMessages([]) ImSDK.login(currentUser.id, currentUser.nickname) .then(() => ImSDK.fetchHistory(peerUser.id, 0, 50)) .then(h => { h.slice().reverse().forEach(m => mergeSingle(m)); setConnected(ImSDK.isConnected()) }) .catch(e => appendLog(`重登录失败:${e instanceof Error ? e.message : 'error'}`)) }} /> {/* Section 2: 单聊 */} 长按自己的消息可撤回。"全部类型演示"依次发送所有 12 种消息类型。 {singleMessages.length === 0 ? ( 还没有消息,发一条试试看。 ) : ( singleMessages.map(m => ( )) )} {/* Section 3: 群聊 */} 点击"创建演示群"创建包含 Alice+Bob 的群组,"加载群列表"列出已有群,然后即可发送群消息。 {/* Section 4: 更新演示 */} {/* Section 5: 活动日志 */} {activityLog.length === 0 ? ( 暂无日志 ) : ( activityLog.map((item, i) => ( {item} )) )} ) } function SectionCard({title, children}: {title: string; children: React.ReactNode}) { return ( {title} {children} ) } function PrimaryBtn({title, onPress, disabled}: {title: string; onPress: () => void; disabled?: boolean}) { return ( {title} ) } function GhostBtn({title, onPress}: {title: string; onPress: () => void}) { return ( {title} ) } const styles = StyleSheet.create({ safeArea: {flex: 1, backgroundColor: '#f5efe3'}, scroll: {flex: 1, backgroundColor: '#f5efe3'}, content: {padding: 18, gap: 14, paddingBottom: 40}, eyebrow: {fontSize: 13, fontWeight: '700', color: '#915f34', textTransform: 'uppercase', letterSpacing: 1.1}, title: {fontSize: 26, lineHeight: 32, fontWeight: '800', color: '#1f2933'}, subtitle: {fontSize: 13, lineHeight: 20, color: '#4b5563'}, card: {borderRadius: 24, padding: 16, backgroundColor: '#fffaf2', borderWidth: 1, borderColor: '#ead7b7', gap: 12}, cardTitle: {fontSize: 17, fontWeight: '800', color: '#1f2933'}, hint: {fontSize: 13, lineHeight: 19, color: '#6b7280'}, userRow: {flexDirection: 'row', gap: 10}, userChip: {paddingHorizontal: 16, paddingVertical: 10, borderRadius: 999, backgroundColor: '#efe2c5'}, userChipActive: {backgroundColor: '#1f2933'}, userChipText: {color: '#4b5563', fontWeight: '700'}, userChipTextActive: {color: '#fffdf8'}, statusRow: {flexDirection: 'row', alignItems: 'center', gap: 6}, dot: {width: 8, height: 8, borderRadius: 4}, dotGreen: {backgroundColor: '#22c55e'}, dotGray: {backgroundColor: '#9ca3af'}, meta: {fontSize: 13, color: '#4b5563'}, row: {flexDirection: 'row', gap: 10}, chatPanel: {maxHeight: 340, borderRadius: 18, backgroundColor: '#f4ecdf'}, chatContent: {padding: 12, gap: 6}, empty: {fontSize: 14, color: '#7c7f85', textAlign: 'center', paddingVertical: 20}, primaryBtn: {minHeight: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: '#1f2933', paddingHorizontal: 16}, primaryBtnText: {color: '#fffdf8', fontSize: 14, fontWeight: '800'}, ghostBtn: {minHeight: 44, borderRadius: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: '#efe2c5', paddingHorizontal: 16}, ghostBtnText: {color: '#4b5563', fontSize: 14, fontWeight: '700'}, btnDisabled: {opacity: 0.45}, logLine: {fontSize: 11, lineHeight: 17, color: '#374151', fontFamily: 'monospace'}, }) export default App