import { apiRequest, _getToken, _saveToken, getConfig } from '@xuqm/rn-common' import { ImClient } from './ImClient' import { ImDatabase } from './db/ImDatabase' import type { ChatType, ImEventListener, ImGroup, ImMessage, MsgType } from './types' let client: ImClient | null = null let _currentUserId: string | null = null export const ImSDK = { /** * Login to IM service. Fetches a token internally and opens the WebSocket connection. * Pass dbName to enable local SQLite message caching (requires @nozbe/watermelondb). */ async login(userId: string, nickname?: string, avatar?: string, dbName?: string): Promise { const config = getConfig() const res = await apiRequest<{ token: string }>('/api/im/auth/login', { method: 'POST', skipAuth: true, params: { appId: config.appId, userId, ...(nickname ? { nickname } : {}), ...(avatar ? { avatar } : {}), }, }) await _saveToken(res.token) _currentUserId = userId if (dbName !== undefined || ImDatabase.isInitialized()) { ImDatabase.init(dbName ?? 'xuqm_im') } client = new ImClient(config.imWsUrl, res.token, config.appId) client.connect() }, async reconnect(): Promise { const config = getConfig() const token = await _getToken() if (!token) throw new Error('[ImSDK] No active session — call login() first.') client = new ImClient(config.imWsUrl, token, config.appId) client.connect() }, /** * Fetch message history. Reads from local DB first; falls back to server if DB is empty * or not initialized, then caches results locally. */ async fetchHistory(toId: string, page = 0, size = 20): Promise { const config = getConfig() if (ImDatabase.isInitialized() && page === 0 && _currentUserId) { const local = await ImDatabase.getMessages(config.appId, toId, 'SINGLE', _currentUserId, size) if (local.length > 0) { return local.map(m => ({ id: m.serverId, appId: m.appId, fromUserId: m.fromUserId, toId: m.toId, chatType: m.chatType as ChatType, msgType: m.msgType as MsgType, content: m.content, status: m.status as ImMessage['status'], mentionedUserIds: m.mentionedUserIds ?? undefined, createdAt: new Date(m.serverCreatedAt).toISOString(), })) } } const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>( `/api/im/messages/history/${encodeURIComponent(toId)}`, { params: { appId: config.appId, page: String(page), size: String(size) } }, ) const messages = Array.isArray(res) ? res : (res.content ?? []) if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.bulkSave(messages, _currentUserId) } return messages }, async sendMessage( toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string, ): Promise { const config = getConfig() const msg = await apiRequest('/api/im/messages/send', { method: 'POST', params: { appId: config.appId }, body: { toId, chatType, msgType, content, mentionedUserIds: mentionedUserIds ?? '' }, }) if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.saveMessage(msg, _currentUserId) } return msg }, async revokeMessage(messageId: string): Promise { const config = getConfig() const msg = await apiRequest(`/api/im/messages/${encodeURIComponent(messageId)}/revoke`, { method: 'POST', params: { appId: config.appId }, }) if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.saveMessage(msg, _currentUserId) } return msg }, async createGroup(name: string, memberIds: string[]): Promise { const config = getConfig() return apiRequest('/api/im/groups', { method: 'POST', params: { appId: config.appId }, body: { name, memberIds }, }) }, async listGroups(): Promise { const config = getConfig() const res = await apiRequest('/api/im/groups', { params: { appId: config.appId }, }) return Array.isArray(res) ? res : (res.content ?? []) }, async fetchGroupHistory(groupId: string, page = 0, size = 50): Promise { const config = getConfig() if (ImDatabase.isInitialized() && page === 0 && _currentUserId) { const local = await ImDatabase.getMessages(config.appId, groupId, 'GROUP', _currentUserId, size) if (local.length > 0) { return local.map(m => ({ id: m.serverId, appId: m.appId, fromUserId: m.fromUserId, toId: m.toId, chatType: m.chatType as ChatType, msgType: m.msgType as MsgType, content: m.content, status: m.status as ImMessage['status'], mentionedUserIds: m.mentionedUserIds ?? undefined, createdAt: new Date(m.serverCreatedAt).toISOString(), })) } } const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>( `/api/im/messages/history/${encodeURIComponent(groupId)}`, { params: { appId: config.appId, page: String(page), size: String(size) } }, ) const messages = Array.isArray(res) ? res : (res.content ?? []) if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.bulkSave(messages, _currentUserId) } return messages }, /** List all conversations from local DB sorted by last message time. */ async listConversations() { if (!ImDatabase.isInitialized()) return [] const config = getConfig() return ImDatabase.getConversations(config.appId) }, /** Mark a conversation as read (clears unread count). */ async markRead(targetId: string): Promise { if (!ImDatabase.isInitialized()) return const config = getConfig() await ImDatabase.markRead(config.appId, targetId) }, addListener(listener: ImEventListener): void { client?.addListener({ ...listener, onMessage: async (msg) => { if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.saveMessage(msg, _currentUserId) } listener.onMessage?.(msg) }, onGroupMessage: async (msg) => { if (ImDatabase.isInitialized() && _currentUserId) { await ImDatabase.saveMessage(msg, _currentUserId) } listener.onGroupMessage?.(msg) }, }) }, removeListener(listener: ImEventListener): void { client?.removeListener(listener) }, subscribeGroup(groupId: string): void { client?.subscribeGroup(groupId) }, isConnected(): boolean { return client?.isConnected() ?? false }, disconnect(): void { client?.disconnect() client = null _currentUserId = null }, }