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