2026-04-25 16:41:19 +08:00
|
|
|
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo } from '@xuqm/rn-common'
|
2026-04-24 16:16:31 +08:00
|
|
|
import { ImClient } from './ImClient'
|
2026-04-24 20:54:12 +08:00
|
|
|
import { ImDatabase } from './db/ImDatabase'
|
2026-04-25 16:41:19 +08:00
|
|
|
import type { MessageSearchParams } from './db/ImDatabase'
|
2026-04-24 16:16:31 +08:00
|
|
|
import type { ChatType, ImEventListener, ImGroup, ImMessage, MsgType } from './types'
|
2026-04-25 16:41:19 +08:00
|
|
|
import { uploadFile } from './upload'
|
2026-04-24 16:16:31 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
2026-04-25 16:41:19 +08:00
|
|
|
export interface ConversationData {
|
|
|
|
|
targetId: string
|
|
|
|
|
chatType: 'SINGLE' | 'GROUP'
|
|
|
|
|
lastMsgContent: string
|
|
|
|
|
lastMsgType: string
|
|
|
|
|
lastMsgTime: number
|
|
|
|
|
unreadCount: number
|
|
|
|
|
isMuted: boolean
|
|
|
|
|
isPinned: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _syncHistoryForAllConversations(): Promise<void> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
if (!ImDatabase.isInitialized() || !_currentUserId) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Fetch top 20 conversations from server
|
|
|
|
|
const res = await apiRequest<ImMessage[] | { content?: ImMessage[] }>(
|
|
|
|
|
'/api/im/conversations',
|
|
|
|
|
{ params: { appId: config.appId, page: '0', size: '20' } },
|
|
|
|
|
)
|
|
|
|
|
const serverConversations = Array.isArray(res) ? res : (res.content ?? [])
|
|
|
|
|
|
|
|
|
|
// For each conversation, fetch last 30 messages and save locally
|
|
|
|
|
// serverConversations items may be conversation summaries; we use the common
|
|
|
|
|
// history endpoint that accepts a targetId. If the response is ImMessage[]
|
|
|
|
|
// we extract unique toIds.
|
|
|
|
|
const targetIds: string[] = []
|
|
|
|
|
for (const item of serverConversations as any[]) {
|
|
|
|
|
const targetId: string | undefined = item.targetId ?? item.toId ?? item.id
|
|
|
|
|
if (targetId && !targetIds.includes(targetId)) {
|
|
|
|
|
targetIds.push(targetId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
targetIds.map(async (targetId) => {
|
|
|
|
|
try {
|
|
|
|
|
const msgRes = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(
|
|
|
|
|
`/api/im/messages/history/${encodeURIComponent(targetId)}`,
|
|
|
|
|
{ params: { appId: config.appId, page: '0', size: '30' } },
|
|
|
|
|
)
|
|
|
|
|
const messages = Array.isArray(msgRes) ? msgRes : (msgRes.content ?? [])
|
|
|
|
|
if (messages.length > 0 && _currentUserId) {
|
|
|
|
|
await ImDatabase.bulkSave(messages, _currentUserId)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Silently skip conversations that fail to sync
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
// Sync is best-effort; do not throw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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()
|
2026-04-25 16:41:19 +08:00
|
|
|
const device = await getDeviceInfo()
|
2026-04-24 16:16:31 +08:00
|
|
|
const res = await apiRequest<{ token: string }>('/api/im/auth/login', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
skipAuth: true,
|
|
|
|
|
params: {
|
2026-04-25 16:41:19 +08:00
|
|
|
appId: config.appId,
|
2026-04-24 16:16:31 +08:00
|
|
|
userId,
|
2026-04-25 16:41:19 +08:00
|
|
|
deviceId: device.deviceId,
|
|
|
|
|
platform: device.platform,
|
|
|
|
|
brand: device.brand,
|
|
|
|
|
model: device.model,
|
|
|
|
|
osVersion: device.osVersion,
|
2026-04-24 16:16:31 +08:00
|
|
|
...(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)
|
2026-04-25 16:41:19 +08:00
|
|
|
client.addListener({
|
|
|
|
|
onConnected: () => {
|
|
|
|
|
_syncHistoryForAllConversations().catch(() => {})
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
client.connect()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Login with a pre-obtained IM token (e.g. from demo-service).
|
|
|
|
|
* Sets up the IM connection without calling the auth endpoint.
|
|
|
|
|
*/
|
|
|
|
|
async loginWithToken(userId: string, token: string, dbName?: string): Promise<void> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
await _saveToken(token)
|
|
|
|
|
_currentUserId = userId
|
|
|
|
|
|
|
|
|
|
if (dbName !== undefined || ImDatabase.isInitialized()) {
|
|
|
|
|
ImDatabase.init(dbName ?? 'xuqm_im')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client = new ImClient(config.imWsUrl, token, config.appId)
|
|
|
|
|
client.addListener({
|
|
|
|
|
onConnected: () => {
|
|
|
|
|
_syncHistoryForAllConversations().catch(() => {})
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-04-24 16:16:31 +08:00
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-25 16:41:19 +08:00
|
|
|
/**
|
|
|
|
|
* Subscribe to conversation list changes.
|
|
|
|
|
* Fires immediately with current data, then on every change (new message, read, mute, pin, etc.).
|
|
|
|
|
* Returns an unsubscribe function.
|
|
|
|
|
*/
|
|
|
|
|
subscribeConversations(callback: (conversations: ConversationData[]) => void): () => void {
|
|
|
|
|
if (!ImDatabase.isInitialized()) {
|
|
|
|
|
// Return no-op if DB not ready
|
|
|
|
|
return () => {}
|
|
|
|
|
}
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
return ImDatabase.subscribeConversations(config.appId, (models) => {
|
|
|
|
|
const data: ConversationData[] = models.map(c => ({
|
|
|
|
|
targetId: c.targetId,
|
|
|
|
|
chatType: c.chatType as 'SINGLE' | 'GROUP',
|
|
|
|
|
lastMsgContent: c.lastMsgContent ?? '',
|
|
|
|
|
lastMsgType: c.lastMsgType ?? '',
|
|
|
|
|
lastMsgTime: c.lastMsgTime,
|
|
|
|
|
unreadCount: c.unreadCount,
|
|
|
|
|
isMuted: c.isMuted,
|
|
|
|
|
isPinned: c.isPinned,
|
|
|
|
|
}))
|
|
|
|
|
callback(data)
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-24 20:54:12 +08:00
|
|
|
/** 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)
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-25 16:41:19 +08:00
|
|
|
/**
|
|
|
|
|
* Fetch last page of messages for each known conversation from server and save locally.
|
|
|
|
|
* Called automatically after connection is established via login/loginWithToken.
|
|
|
|
|
*/
|
|
|
|
|
async syncHistoryForAllConversations(): Promise<void> {
|
|
|
|
|
await _syncHistoryForAllConversations()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Search messages in local DB.
|
|
|
|
|
* appId defaults to the configured appId if not provided.
|
|
|
|
|
*/
|
|
|
|
|
async searchMessages(params: MessageSearchParams & { appId?: string }) {
|
|
|
|
|
if (!ImDatabase.isInitialized()) return []
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
const { appId, ...rest } = params
|
|
|
|
|
return ImDatabase.searchMessages(appId ?? config.appId, rest)
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-24 20:54:12 +08:00
|
|
|
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 },
|
|
|
|
|
|
2026-04-25 16:41:19 +08:00
|
|
|
/**
|
|
|
|
|
* Upload a local image and send it as an IMAGE message.
|
|
|
|
|
* The file-service generates a thumbnail automatically for image/* content.
|
|
|
|
|
*/
|
|
|
|
|
async sendImageMessage(
|
|
|
|
|
toId: string,
|
|
|
|
|
chatType: ChatType,
|
|
|
|
|
localUri: string,
|
|
|
|
|
width?: number,
|
|
|
|
|
height?: number,
|
|
|
|
|
): Promise<ImMessage> {
|
|
|
|
|
const filename = localUri.split('/').pop() ?? 'image.jpg'
|
|
|
|
|
const mimeType = resolveMimeTypeFromUri(localUri, 'image/jpeg')
|
|
|
|
|
const result = await uploadFile(localUri, mimeType, filename)
|
|
|
|
|
const content = JSON.stringify({
|
|
|
|
|
url: result.url,
|
|
|
|
|
thumbnailUrl: result.thumbnailUrl,
|
|
|
|
|
width: width ?? 0,
|
|
|
|
|
height: height ?? 0,
|
|
|
|
|
size: result.size,
|
|
|
|
|
name: result.originalName,
|
|
|
|
|
})
|
|
|
|
|
return ImSDK.sendMessage(toId, chatType, 'IMAGE', content)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload a local video (with optional pre-generated thumbnail) and send as a VIDEO message.
|
|
|
|
|
*/
|
|
|
|
|
async sendVideoMessage(
|
|
|
|
|
toId: string,
|
|
|
|
|
chatType: ChatType,
|
|
|
|
|
localUri: string,
|
|
|
|
|
thumbnailUri?: string,
|
|
|
|
|
duration?: number,
|
|
|
|
|
): Promise<ImMessage> {
|
|
|
|
|
const filename = localUri.split('/').pop() ?? 'video.mp4'
|
|
|
|
|
const mimeType = resolveMimeTypeFromUri(localUri, 'video/mp4')
|
|
|
|
|
const result = await uploadFile(localUri, mimeType, filename, thumbnailUri)
|
|
|
|
|
const content = JSON.stringify({
|
|
|
|
|
url: result.url,
|
|
|
|
|
thumbnailUrl: result.thumbnailUrl,
|
|
|
|
|
duration: duration ?? 0,
|
|
|
|
|
size: result.size,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
})
|
|
|
|
|
return ImSDK.sendMessage(toId, chatType, 'VIDEO', content)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload a local audio file and send as an AUDIO message.
|
|
|
|
|
*/
|
|
|
|
|
async sendAudioMessage(
|
|
|
|
|
toId: string,
|
|
|
|
|
chatType: ChatType,
|
|
|
|
|
localUri: string,
|
|
|
|
|
duration: number,
|
|
|
|
|
): Promise<ImMessage> {
|
|
|
|
|
const filename = localUri.split('/').pop() ?? 'audio.m4a'
|
|
|
|
|
const mimeType = resolveMimeTypeFromUri(localUri, 'audio/mp4')
|
|
|
|
|
const result = await uploadFile(localUri, mimeType, filename)
|
|
|
|
|
const content = JSON.stringify({
|
|
|
|
|
url: result.url,
|
|
|
|
|
duration,
|
|
|
|
|
size: result.size,
|
|
|
|
|
})
|
|
|
|
|
return ImSDK.sendMessage(toId, chatType, 'AUDIO', content)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload a local file and send as a FILE message.
|
|
|
|
|
*/
|
|
|
|
|
async sendFileMessage(
|
|
|
|
|
toId: string,
|
|
|
|
|
chatType: ChatType,
|
|
|
|
|
localUri: string,
|
|
|
|
|
filename: string,
|
|
|
|
|
size: number,
|
|
|
|
|
): Promise<ImMessage> {
|
|
|
|
|
const mimeType = resolveMimeTypeFromUri(localUri, 'application/octet-stream')
|
|
|
|
|
const result = await uploadFile(localUri, mimeType, filename)
|
|
|
|
|
const content = JSON.stringify({
|
|
|
|
|
url: result.url,
|
|
|
|
|
name: filename,
|
|
|
|
|
size,
|
|
|
|
|
mimeType: result.mimeType,
|
|
|
|
|
})
|
|
|
|
|
return ImSDK.sendMessage(toId, chatType, 'FILE', content)
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-25 17:27:19 +08:00
|
|
|
async listFriends(): Promise<string[]> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
const res = await apiRequest<{ data?: string[] } | string[]>('/api/im/friends', {
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
})
|
|
|
|
|
if (Array.isArray(res)) return res
|
|
|
|
|
return (res as { data?: string[] }).data ?? []
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async addFriend(friendId: string): Promise<void> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
await apiRequest('/api/im/friends', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
params: { appId: config.appId, friendId },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async removeFriend(friendId: string): Promise<void> {
|
|
|
|
|
const config = getConfig()
|
|
|
|
|
await apiRequest(`/api/im/friends/${encodeURIComponent(friendId)}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
params: { appId: config.appId },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-24 16:16:31 +08:00
|
|
|
disconnect(): void {
|
|
|
|
|
client?.disconnect()
|
|
|
|
|
client = null
|
2026-04-24 20:54:12 +08:00
|
|
|
_currentUserId = null
|
2026-04-24 16:16:31 +08:00
|
|
|
},
|
|
|
|
|
}
|
2026-04-25 16:41:19 +08:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Internal helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function resolveMimeTypeFromUri(uri: string, fallback: string): string {
|
|
|
|
|
const lower = uri.toLowerCase().split('?')[0]
|
|
|
|
|
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg'
|
|
|
|
|
if (lower.endsWith('.png')) return 'image/png'
|
|
|
|
|
if (lower.endsWith('.gif')) return 'image/gif'
|
|
|
|
|
if (lower.endsWith('.webp')) return 'image/webp'
|
|
|
|
|
if (lower.endsWith('.mp4')) return 'video/mp4'
|
|
|
|
|
if (lower.endsWith('.mov')) return 'video/quicktime'
|
|
|
|
|
if (lower.endsWith('.m4a')) return 'audio/mp4'
|
|
|
|
|
if (lower.endsWith('.mp3')) return 'audio/mpeg'
|
|
|
|
|
if (lower.endsWith('.aac')) return 'audio/aac'
|
|
|
|
|
if (lower.endsWith('.pdf')) return 'application/pdf'
|
|
|
|
|
return fallback
|
|
|
|
|
}
|