2026-04-24 16:16:31 +08:00
|
|
|
import { apiRequest, _getToken, _saveToken, getConfig } from '@xuqm/rn-common'
|
|
|
|
|
import { ImClient } from './ImClient'
|
2026-04-24 20:54:12 +08:00
|
|
|
import { ImDatabase } from './db/ImDatabase'
|
2026-04-24 16:16:31 +08:00
|
|
|
import type { ChatType, ImEventListener, ImGroup, ImMessage, MsgType } from './types'
|
|
|
|
|
|
|
|
|
|
let client: ImClient | null = null
|
2026-04-24 20:54:12 +08:00
|
|
|
let _currentUserId: string | null = null
|
2026-04-24 16:16:31 +08:00
|
|
|
|
|
|
|
|
export const ImSDK = {
|
|
|
|
|
/**
|
|
|
|
|
* Login to IM service. Fetches a token internally and opens the WebSocket connection.
|
2026-04-24 20:54:12 +08:00
|
|
|
* Pass dbName to enable local SQLite message caching (requires @nozbe/watermelondb).
|
2026-04-24 16:16:31 +08:00
|
|
|
*/
|
2026-04-24 20:54:12 +08:00
|
|
|
async login(userId: string, nickname?: string, avatar?: string, dbName?: string): Promise<void> {
|
2026-04-24 16:16:31 +08:00
|
|
|
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)
|
2026-04-24 20:54:12 +08:00
|
|
|
_currentUserId = userId
|
|
|
|
|
|
|
|
|
|
if (dbName !== undefined || ImDatabase.isInitialized()) {
|
|
|
|
|
ImDatabase.init(dbName ?? 'xuqm_im')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:31 +08:00
|
|
|
client = new ImClient(config.imWsUrl, res.token, config.appId)
|
|
|
|
|
client.connect()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async reconnect(): Promise<void> {
|
|
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-24 20:54:12 +08:00
|
|
|
/**
|
|
|
|
|
* Fetch message history. Reads from local DB first; falls back to server if DB is empty
|
|
|
|
|
* or not initialized, then caches results locally.
|
|
|
|
|
*/
|
2026-04-24 16:16:31 +08:00
|
|
|
async fetchHistory(toId: string, page = 0, size = 20): Promise<ImMessage[]> {
|
|
|
|
|
const config = getConfig()
|
2026-04-24 20:54:12 +08:00
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:31 +08:00
|
|
|
const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(
|
|
|
|
|
`/api/im/messages/history/${encodeURIComponent(toId)}`,
|
|
|
|
|
{ params: { appId: config.appId, page: String(page), size: String(size) } },
|
|
|
|
|
)
|
2026-04-24 20:54:12 +08:00
|
|
|
const messages = Array.isArray(res) ? res : (res.content ?? [])
|
|
|
|
|
|
|
|
|
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
|
|
|
await ImDatabase.bulkSave(messages, _currentUserId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return messages
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async sendMessage(
|
|
|
|
|
toId: string,
|
|
|
|
|
chatType: ChatType,
|
|
|
|
|
msgType: MsgType,
|
|
|
|
|
content: string,
|
|
|
|
|
mentionedUserIds?: string,
|
|
|
|
|
): Promise<ImMessage> {
|
|
|
|
|
const config = getConfig()
|
2026-04-24 20:54:12 +08:00
|
|
|
const msg = await apiRequest<ImMessage>('/api/im/messages/send', {
|
2026-04-24 16:16:31 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
body: { toId, chatType, msgType, content, mentionedUserIds: mentionedUserIds ?? '' },
|
|
|
|
|
})
|
2026-04-24 20:54:12 +08:00
|
|
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
|
|
|
await ImDatabase.saveMessage(msg, _currentUserId)
|
|
|
|
|
}
|
|
|
|
|
return msg
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async revokeMessage(messageId: string): Promise<ImMessage> {
|
|
|
|
|
const config = getConfig()
|
2026-04-24 20:54:12 +08:00
|
|
|
const msg = await apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}/revoke`, {
|
2026-04-24 16:16:31 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
})
|
2026-04-24 20:54:12 +08:00
|
|
|
if (ImDatabase.isInitialized() && _currentUserId) {
|
|
|
|
|
await ImDatabase.saveMessage(msg, _currentUserId)
|
|
|
|
|
}
|
|
|
|
|
return msg
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async createGroup(name: string, memberIds: string[]): Promise<ImGroup> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
return apiRequest<ImGroup>('/api/im/groups', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
body: { name, memberIds },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async listGroups(): Promise<ImGroup[]> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
const res = await apiRequest<ImGroup[] | { content?: ImGroup[] }>('/api/im/groups', {
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
})
|
|
|
|
|
return Array.isArray(res) ? res : (res.content ?? [])
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async fetchGroupHistory(groupId: string, page = 0, size = 50): Promise<ImMessage[]> {
|
|
|
|
|
const config = getConfig()
|
2026-04-24 20:54:12 +08:00
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:31 +08:00
|
|
|
const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(
|
|
|
|
|
`/api/im/messages/history/${encodeURIComponent(groupId)}`,
|
|
|
|
|
{ params: { appId: config.appId, page: String(page), size: String(size) } },
|
|
|
|
|
)
|
2026-04-24 20:54:12 +08:00
|
|
|
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<void> {
|
|
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-24 20:54:12 +08:00
|
|
|
_currentUserId = null
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
}
|