1527 行
49 KiB
TypeScript
1527 行
49 KiB
TypeScript
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId, _registerUserInfoHandler } from '@xuqm/rn-common'
|
||
import type { XuqmUserInfo } from '@xuqm/rn-common'
|
||
import { ImClient } from './ImClient'
|
||
import { ImDatabase } from './db/ImDatabase'
|
||
import type { MessageSearchParams } from './db/ImDatabase'
|
||
import type {
|
||
BlacklistEntry,
|
||
BlacklistCheckResult,
|
||
ChatType,
|
||
ConversationData,
|
||
ConversationGroupItem,
|
||
FriendRequest,
|
||
GroupReadReceiptSummary,
|
||
GroupJoinRequest,
|
||
ImEventListener,
|
||
ImGroup,
|
||
ImMessage,
|
||
MsgType,
|
||
PageResult,
|
||
UserProfile,
|
||
} from './types'
|
||
import { uploadFile } from './upload'
|
||
import { buildDbName, sortConversations as sortConversationsByRuntime } from './runtime'
|
||
|
||
let client: ImClient | null = null
|
||
let _currentUserId: string | null = null
|
||
let _currentUserSig: string | null = null
|
||
const draftStore = new Map<string, string>()
|
||
const listenerMap = new WeakMap<ImEventListener, ImEventListener>()
|
||
let conversationMemory: ConversationData[] = []
|
||
const conversationListeners = new Set<(conversations: ConversationData[]) => void>()
|
||
const messageConversationMemory = new Map<string, { targetId: string; chatType: ChatType }>()
|
||
const conversationLastMessageMemory = new Map<string, string>()
|
||
|
||
function generateMessageId(): string {
|
||
const cryptoApi = globalThis as typeof globalThis & { crypto?: { randomUUID?: () => string } }
|
||
const cryptoId = cryptoApi.crypto?.randomUUID?.()
|
||
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||
}
|
||
|
||
function normalizeMessage(msg: ImMessage, fallback?: Partial<ImMessage>): ImMessage {
|
||
return {
|
||
...fallback,
|
||
...msg,
|
||
appKey: msg.appKey ?? fallback?.appKey ?? getConfig().appKey,
|
||
fromId: msg.fromId ?? fallback?.fromId ?? msg.fromUserId,
|
||
revoked: msg.revoked ?? msg.status === 'REVOKED',
|
||
}
|
||
}
|
||
|
||
function buildOutgoingMessage(params: {
|
||
messageId?: string
|
||
toId: string
|
||
chatType: ChatType
|
||
msgType: MsgType
|
||
content: string
|
||
mentionedUserIds?: string
|
||
}): ImMessage {
|
||
const config = getConfig()
|
||
const fromId = _currentUserId ?? getCommonUserId() ?? ''
|
||
const id = params.messageId ?? generateMessageId()
|
||
return {
|
||
id,
|
||
appKey: config.appKey,
|
||
fromUserId: fromId,
|
||
fromId,
|
||
toId: params.toId,
|
||
chatType: params.chatType,
|
||
msgType: params.msgType,
|
||
content: params.content,
|
||
status: 'SENDING',
|
||
mentionedUserIds: params.mentionedUserIds,
|
||
groupReadCount: 0,
|
||
revoked: false,
|
||
createdAt: Date.now(),
|
||
}
|
||
}
|
||
|
||
async function _syncHistoryForAllConversations(): Promise<void> {
|
||
if (!ImDatabase.isInitialized() || !_currentUserId) return
|
||
|
||
try {
|
||
const conversations = await ImSDK.listConversations()
|
||
const targetIds = new Set(conversations.map(item => item.targetId))
|
||
await Promise.all(
|
||
[...targetIds].map(async (targetId) => {
|
||
try {
|
||
const messages = await ImSDK.fetchHistory(targetId, 0, 30)
|
||
if (messages.length > 0 && _currentUserId) {
|
||
await ImDatabase.bulkSave(messages, _currentUserId)
|
||
}
|
||
} catch {
|
||
// best-effort sync only
|
||
}
|
||
}),
|
||
)
|
||
} catch {
|
||
// best-effort sync only
|
||
}
|
||
}
|
||
|
||
function normalizeConversation(item: {
|
||
targetId: string
|
||
chatType: string
|
||
lastMsgContent?: string | null
|
||
lastMsgType?: string | null
|
||
lastMsgTime: number
|
||
unreadCount: number
|
||
isMuted: boolean
|
||
isPinned: boolean
|
||
conversationGroup?: string | null
|
||
}): ConversationData {
|
||
return {
|
||
targetId: item.targetId,
|
||
chatType: item.chatType as ChatType,
|
||
lastMsgContent: item.lastMsgContent ?? null,
|
||
lastMsgType: item.lastMsgType ?? null,
|
||
lastMsgTime: item.lastMsgTime,
|
||
unreadCount: item.unreadCount,
|
||
isMuted: item.isMuted,
|
||
isPinned: item.isPinned,
|
||
conversationGroup: item.conversationGroup ?? null,
|
||
}
|
||
}
|
||
|
||
function conversationKey(targetId: string, chatType: ChatType): string {
|
||
return `${chatType}:${targetId}`
|
||
}
|
||
|
||
function emitConversationMemory(): void {
|
||
const snapshot = sortConversationsByRuntime(conversationMemory)
|
||
conversationMemory = snapshot
|
||
conversationListeners.forEach(listener => listener(snapshot))
|
||
}
|
||
|
||
function upsertConversationMemory(conversation: ConversationData): void {
|
||
const index = conversationMemory.findIndex(
|
||
item => item.targetId === conversation.targetId && item.chatType === conversation.chatType,
|
||
)
|
||
if (index >= 0) {
|
||
const existing = conversationMemory[index]
|
||
conversationMemory[index] = {
|
||
...existing,
|
||
...conversation,
|
||
unreadCount: conversation.unreadCount,
|
||
isMuted: conversation.isMuted,
|
||
isPinned: conversation.isPinned,
|
||
}
|
||
} else {
|
||
conversationMemory = [...conversationMemory, conversation]
|
||
}
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function markConversationReadMemory(targetId: string, chatType: ChatType): void {
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === targetId && item.chatType === chatType
|
||
? { ...item, unreadCount: 0 }
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function setConversationMutedMemory(targetId: string, chatType: ChatType, muted: boolean): void {
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === targetId && item.chatType === chatType
|
||
? { ...item, isMuted: muted }
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function setConversationPinnedMemory(targetId: string, chatType: ChatType, pinned: boolean): void {
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === targetId && item.chatType === chatType
|
||
? { ...item, isPinned: pinned }
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function setConversationGroupMemory(targetId: string, chatType: ChatType, groupName?: string | null): void {
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === targetId && item.chatType === chatType
|
||
? { ...item, conversationGroup: groupName ?? null }
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function deleteConversationMemory(targetId: string, chatType: ChatType): void {
|
||
conversationMemory = conversationMemory.filter(
|
||
item => !(item.targetId === targetId && item.chatType === chatType),
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function applyMessageToMemory(message: ImMessage): void {
|
||
const normalized = normalizeMessage(message)
|
||
const key = conversationKey(normalized.toId, normalized.chatType)
|
||
messageConversationMemory.set(normalized.id, { targetId: normalized.toId, chatType: normalized.chatType })
|
||
conversationLastMessageMemory.set(key, normalized.id)
|
||
const index = conversationMemory.findIndex(
|
||
item => item.targetId === normalized.toId && item.chatType === normalized.chatType,
|
||
)
|
||
const revoked = normalized.revoked || normalized.status === 'REVOKED' || normalized.msgType === 'REVOKED'
|
||
const lastMsgContent = revoked ? '消息已撤回' : normalized.content
|
||
const unreadDelta = !revoked && normalized.fromId !== _currentUserId ? 1 : 0
|
||
const nextConversation: ConversationData = {
|
||
targetId: normalized.toId,
|
||
chatType: normalized.chatType,
|
||
lastMsgContent,
|
||
lastMsgType: revoked ? 'REVOKED' : normalized.msgType,
|
||
lastMsgTime: normalized.createdAt,
|
||
unreadCount: unreadDelta,
|
||
isMuted: index >= 0 ? conversationMemory[index].isMuted : false,
|
||
isPinned: index >= 0 ? conversationMemory[index].isPinned : false,
|
||
}
|
||
if (index >= 0) {
|
||
const current = conversationMemory[index]
|
||
conversationMemory[index] = {
|
||
...current,
|
||
...nextConversation,
|
||
unreadCount: unreadDelta > 0 ? current.unreadCount + unreadDelta : current.unreadCount,
|
||
}
|
||
} else {
|
||
conversationMemory = [...conversationMemory, nextConversation]
|
||
}
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function applyRevokeToMemory(messageId: string): void {
|
||
const messageConversation = messageConversationMemory.get(messageId)
|
||
if (!messageConversation) return
|
||
const key = conversationKey(messageConversation.targetId, messageConversation.chatType)
|
||
if (conversationLastMessageMemory.get(key) !== messageId) return
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === messageConversation.targetId && item.chatType === messageConversation.chatType
|
||
? { ...item, lastMsgContent: '消息已撤回', lastMsgType: 'REVOKED' }
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function applyEditToMemory(message: ImMessage): void {
|
||
const normalized = normalizeMessage(message)
|
||
const messageConversation = messageConversationMemory.get(normalized.id)
|
||
if (!messageConversation) return
|
||
const key = conversationKey(messageConversation.targetId, messageConversation.chatType)
|
||
if (conversationLastMessageMemory.get(key) !== normalized.id) return
|
||
conversationMemory = conversationMemory.map(item =>
|
||
item.targetId === messageConversation.targetId && item.chatType === messageConversation.chatType
|
||
? {
|
||
...item,
|
||
lastMsgContent: normalized.content,
|
||
lastMsgType: normalized.msgType,
|
||
lastMsgTime: normalized.createdAt,
|
||
}
|
||
: item,
|
||
)
|
||
emitConversationMemory()
|
||
}
|
||
|
||
function formatQueryDateTime(value?: Date | string | number): string | undefined {
|
||
if (value === undefined || value === null) return undefined
|
||
if (typeof value === 'string') return value
|
||
const date = value instanceof Date ? value : new Date(value)
|
||
if (Number.isNaN(date.getTime())) return undefined
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
return [
|
||
date.getFullYear(),
|
||
'-',
|
||
pad(date.getMonth() + 1),
|
||
'-',
|
||
pad(date.getDate()),
|
||
'T',
|
||
pad(date.getHours()),
|
||
':',
|
||
pad(date.getMinutes()),
|
||
':',
|
||
pad(date.getSeconds()),
|
||
].join('')
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
export const ImSDK = {
|
||
/**
|
||
* Login to IM service with a pre-obtained userSig and open the WebSocket connection.
|
||
*/
|
||
async login(userId: string, userSig: string): Promise<void> {
|
||
const config = getConfig()
|
||
if (client && _currentUserId === userId && _currentUserSig === userSig) {
|
||
return
|
||
}
|
||
client?.disconnect()
|
||
await _saveToken(userSig)
|
||
_currentUserId = userId
|
||
_currentUserSig = userSig
|
||
setCommonUserId(userId)
|
||
|
||
ImDatabase.init(buildDbName(config.appKey, userId))
|
||
|
||
client = new ImClient(config.imWsUrl, userSig, config.appKey)
|
||
client.addListener({
|
||
onConnected: () => {
|
||
_syncHistoryForAllConversations().catch(() => {})
|
||
},
|
||
})
|
||
await 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.appKey)
|
||
await client.connect()
|
||
},
|
||
|
||
async fetchHistory(
|
||
toId: string,
|
||
page = 0,
|
||
size = 20,
|
||
chatType: ChatType = 'SINGLE',
|
||
): Promise<ImMessage[]> {
|
||
const config = getConfig()
|
||
if (ImDatabase.isInitialized() && page === 0 && _currentUserId) {
|
||
const local = await ImDatabase.getMessages(config.appKey, toId, chatType, _currentUserId, size)
|
||
if (local.length > 0) {
|
||
return local.map(model => ({
|
||
id: model.serverId,
|
||
appKey: model.appKey,
|
||
fromUserId: model.fromUserId,
|
||
toId: model.toId,
|
||
chatType: model.chatType as ChatType,
|
||
msgType: model.msgType as MsgType,
|
||
content: model.content,
|
||
status: model.status as ImMessage['status'],
|
||
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
||
revoked: model.status === 'REVOKED',
|
||
createdAt: model.serverCreatedAt,
|
||
editedAt: model.editedAt ?? undefined,
|
||
}))
|
||
}
|
||
}
|
||
return ImSDK.fetchHistoryWithFilters(toId, { page, size })
|
||
},
|
||
|
||
async fetchHistoryWithFilters(
|
||
toId: string,
|
||
params: {
|
||
page?: number
|
||
size?: number
|
||
msgType?: MsgType | string
|
||
keyword?: string
|
||
startTime?: Date | string | number
|
||
endTime?: Date | string | number
|
||
} = {},
|
||
): Promise<ImMessage[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(
|
||
`/api/im/messages/history/${encodeURIComponent(toId)}`,
|
||
{
|
||
params: {
|
||
appKey: config.appKey,
|
||
page: String(params.page ?? 0),
|
||
size: String(params.size ?? 20),
|
||
...(params.msgType ? { msgType: String(params.msgType) } : {}),
|
||
...(params.keyword ? { keyword: params.keyword } : {}),
|
||
...(formatQueryDateTime(params.startTime) ? { startTime: formatQueryDateTime(params.startTime)! } : {}),
|
||
...(formatQueryDateTime(params.endTime) ? { endTime: formatQueryDateTime(params.endTime)! } : {}),
|
||
},
|
||
},
|
||
)
|
||
const messages = Array.isArray(res) ? res : (res.content ?? [])
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.bulkSave(messages, _currentUserId)
|
||
}
|
||
return messages
|
||
},
|
||
|
||
async fetchGroupHistory(
|
||
groupId: string,
|
||
page = 0,
|
||
size = 50,
|
||
): Promise<ImMessage[]> {
|
||
const config = getConfig()
|
||
if (ImDatabase.isInitialized() && page === 0 && _currentUserId) {
|
||
const local = await ImDatabase.getMessages(config.appKey, groupId, 'GROUP', _currentUserId, size)
|
||
if (local.length > 0) {
|
||
return local.map(model => ({
|
||
id: model.serverId,
|
||
appKey: model.appKey,
|
||
fromUserId: model.fromUserId,
|
||
toId: model.toId,
|
||
chatType: model.chatType as ChatType,
|
||
msgType: model.msgType as MsgType,
|
||
content: model.content,
|
||
status: model.status as ImMessage['status'],
|
||
mentionedUserIds: model.mentionedUserIds ?? undefined,
|
||
revoked: model.status === 'REVOKED',
|
||
createdAt: model.serverCreatedAt,
|
||
editedAt: model.editedAt ?? undefined,
|
||
}))
|
||
}
|
||
}
|
||
return ImSDK.fetchGroupHistoryWithFilters(groupId, { page, size })
|
||
},
|
||
|
||
async fetchGroupHistoryWithFilters(
|
||
groupId: string,
|
||
params: {
|
||
page?: number
|
||
size?: number
|
||
msgType?: MsgType | string
|
||
keyword?: string
|
||
startTime?: Date | string | number
|
||
endTime?: Date | string | number
|
||
} = {},
|
||
): Promise<ImMessage[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(
|
||
`/api/im/messages/group-history/${encodeURIComponent(groupId)}`,
|
||
{
|
||
params: {
|
||
appKey: config.appKey,
|
||
page: String(params.page ?? 0),
|
||
size: String(params.size ?? 50),
|
||
...(params.msgType ? { msgType: String(params.msgType) } : {}),
|
||
...(params.keyword ? { keyword: params.keyword } : {}),
|
||
...(formatQueryDateTime(params.startTime) ? { startTime: formatQueryDateTime(params.startTime)! } : {}),
|
||
...(formatQueryDateTime(params.endTime) ? { endTime: formatQueryDateTime(params.endTime)! } : {}),
|
||
},
|
||
},
|
||
)
|
||
const messages = Array.isArray(res) ? res : (res.content ?? [])
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.bulkSave(messages, _currentUserId)
|
||
}
|
||
return messages
|
||
},
|
||
|
||
async locateHistoryPage(
|
||
toId: string,
|
||
messageId: string,
|
||
pageSize = 20,
|
||
maxPages = 20,
|
||
): Promise<ImMessage[] | null> {
|
||
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
|
||
const messages = await this.fetchHistory(toId, page, pageSize)
|
||
if (messages.some((item) => item.id === messageId)) {
|
||
return messages
|
||
}
|
||
if (messages.length < pageSize) return null
|
||
}
|
||
return null
|
||
},
|
||
|
||
async locateGroupHistoryPage(
|
||
groupId: string,
|
||
messageId: string,
|
||
pageSize = 50,
|
||
maxPages = 20,
|
||
): Promise<ImMessage[] | null> {
|
||
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
|
||
const messages = await this.fetchGroupHistory(groupId, page, pageSize)
|
||
if (messages.some((item) => item.id === messageId)) {
|
||
return messages
|
||
}
|
||
if (messages.length < pageSize) return null
|
||
}
|
||
return null
|
||
},
|
||
|
||
async sendMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
msgType: MsgType,
|
||
content: string,
|
||
mentionedUserIds?: string,
|
||
): Promise<ImMessage> {
|
||
const config = getConfig()
|
||
const outgoing = buildOutgoingMessage({
|
||
toId,
|
||
chatType,
|
||
msgType,
|
||
content,
|
||
mentionedUserIds,
|
||
})
|
||
try {
|
||
const msg = await apiRequest<ImMessage>('/api/im/messages/send', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: {
|
||
toId,
|
||
chatType,
|
||
msgType,
|
||
content,
|
||
mentionedUserIds: mentionedUserIds ?? '',
|
||
messageId: outgoing.id,
|
||
},
|
||
})
|
||
const finalMsg = normalizeMessage(msg, outgoing)
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(finalMsg, _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(finalMsg)
|
||
}
|
||
return finalMsg
|
||
} catch (error) {
|
||
const failed = { ...outgoing, status: 'FAILED' as const }
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(failed, _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(failed)
|
||
}
|
||
return failed
|
||
}
|
||
},
|
||
|
||
async sendTextMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
content: string,
|
||
mentionedUserIds?: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(toId, chatType, 'TEXT', content, mentionedUserIds)
|
||
},
|
||
|
||
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)
|
||
},
|
||
|
||
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)
|
||
},
|
||
|
||
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)
|
||
},
|
||
|
||
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)
|
||
},
|
||
|
||
async sendNotifyMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
title: string,
|
||
content: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(
|
||
toId,
|
||
chatType,
|
||
'NOTIFY',
|
||
JSON.stringify({ title, content }),
|
||
)
|
||
},
|
||
|
||
async sendQuoteMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
quotedMsgId: string,
|
||
quotedContent: string,
|
||
text: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(
|
||
toId,
|
||
chatType,
|
||
'QUOTE',
|
||
JSON.stringify({ quotedMsgId, quotedContent, text }),
|
||
)
|
||
},
|
||
|
||
async sendMergeMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
title: string,
|
||
msgList: string[],
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(
|
||
toId,
|
||
chatType,
|
||
'MERGE',
|
||
JSON.stringify({ title, msgList }),
|
||
)
|
||
},
|
||
|
||
async sendCallAudioMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
action: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(toId, chatType, 'CALL_AUDIO', JSON.stringify({ action }))
|
||
},
|
||
|
||
async sendCallVideoMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
action: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(toId, chatType, 'CALL_VIDEO', JSON.stringify({ action }))
|
||
},
|
||
|
||
async sendCustomMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
data: Record<string, unknown>,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(toId, chatType, 'CUSTOM', JSON.stringify(data))
|
||
},
|
||
|
||
async sendLocationMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
latitude: number,
|
||
longitude: number,
|
||
title?: string,
|
||
address?: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(
|
||
toId,
|
||
chatType,
|
||
'LOCATION',
|
||
JSON.stringify({ latitude, longitude, title: title ?? '', address: address ?? '' }),
|
||
)
|
||
},
|
||
|
||
async sendRichTextMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
html: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(toId, chatType, 'RICH_TEXT', html)
|
||
},
|
||
|
||
async sendForwardMessage(
|
||
toId: string,
|
||
chatType: ChatType,
|
||
originalSender: string,
|
||
originalContent: string,
|
||
): Promise<ImMessage> {
|
||
return ImSDK.sendMessage(
|
||
toId,
|
||
chatType,
|
||
'FORWARD',
|
||
JSON.stringify({ originalSender, originalContent }),
|
||
)
|
||
},
|
||
|
||
async revokeMessage(messageId: string): Promise<ImMessage> {
|
||
const config = getConfig()
|
||
const msg = await apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}/revoke`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
})
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
await ImDatabase.revokeMessage(config.appKey, messageId)
|
||
} else {
|
||
applyMessageToMemory(msg)
|
||
applyRevokeToMemory(messageId)
|
||
}
|
||
return msg
|
||
},
|
||
|
||
async editMessage(messageId: string, content: string): Promise<ImMessage> {
|
||
const config = getConfig()
|
||
const msg = await apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}`, {
|
||
method: 'PUT',
|
||
params: { appKey: config.appKey },
|
||
body: { content },
|
||
})
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
} else {
|
||
applyEditToMemory(msg)
|
||
}
|
||
return msg
|
||
},
|
||
|
||
async syncOfflineMessages(maxCount = 100): Promise<ImMessage[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<ImMessage[] | { data?: ImMessage[] }>('/api/im/messages/offline', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey, maxCount: String(maxCount) },
|
||
})
|
||
const messages = Array.isArray(res) ? res : (res.data ?? [])
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
for (const msg of messages) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
}
|
||
}
|
||
return messages
|
||
},
|
||
|
||
async offlineMessageCount(): Promise<number> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<{ count: number }>('/api/im/messages/offline/count', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return res.count ?? 0
|
||
},
|
||
|
||
async createGroup(name: string, memberIds: string[], groupType = 'WORK'): Promise<ImGroup> {
|
||
const config = getConfig()
|
||
return apiRequest<ImGroup>('/api/im/groups', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { name, memberIds, groupType },
|
||
})
|
||
},
|
||
|
||
async listGroups(): Promise<ImGroup[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<ImGroup[] | { content?: ImGroup[] }>('/api/im/groups', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async listPublicGroups(keyword?: string): Promise<ImGroup[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<ImGroup[] | { content?: ImGroup[] }>('/api/im/groups/public', {
|
||
params: {
|
||
appKey: config.appKey,
|
||
...(keyword ? { keyword } : {}),
|
||
},
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async getGroupInfo(groupId: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`)
|
||
},
|
||
|
||
async listGroupMembers(groupId: string): Promise<UserProfile[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>(`/api/im/groups/${encodeURIComponent(groupId)}/members`, {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async searchGroupMembers(groupId: string, keyword: string, size = 20): Promise<UserProfile[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>(`/api/im/groups/${encodeURIComponent(groupId)}/members/search`, {
|
||
params: {
|
||
appKey: config.appKey,
|
||
keyword,
|
||
size: String(size),
|
||
},
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async updateGroupInfo(groupId: string, name?: string, announcement?: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`, {
|
||
method: 'PUT',
|
||
body: { name, announcement },
|
||
})
|
||
},
|
||
|
||
async addGroupMember(groupId: string, userId: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/members`, {
|
||
method: 'POST',
|
||
body: { userId },
|
||
})
|
||
},
|
||
|
||
async removeGroupMember(groupId: string, targetUserId: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(
|
||
`/api/im/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(targetUserId)}`,
|
||
{ method: 'DELETE' },
|
||
)
|
||
},
|
||
|
||
async leaveGroup(groupId: string): Promise<ImGroup> {
|
||
if (!_currentUserId) throw new Error('[ImSDK] Not logged in')
|
||
return ImSDK.removeGroupMember(groupId, _currentUserId)
|
||
},
|
||
|
||
async setGroupRole(groupId: string, userId: string, role: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/roles`, {
|
||
method: 'POST',
|
||
body: { userId, role },
|
||
})
|
||
},
|
||
|
||
async muteGroupMember(groupId: string, userId: string, minutes: number): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/mute`, {
|
||
method: 'POST',
|
||
body: { userId, minutes },
|
||
})
|
||
},
|
||
|
||
async transferGroupOwner(groupId: string, newOwnerId: string): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/owner`, {
|
||
method: 'POST',
|
||
body: { newOwnerId },
|
||
})
|
||
},
|
||
|
||
async updateGroupAttributes(groupId: string, attributes: Record<string, unknown>): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/attributes`, {
|
||
method: 'PUT',
|
||
body: attributes,
|
||
})
|
||
},
|
||
|
||
async removeGroupAttributes(groupId: string, keys: string[]): Promise<ImGroup> {
|
||
return apiRequest<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}/attributes/delete`, {
|
||
method: 'POST',
|
||
body: { keys },
|
||
})
|
||
},
|
||
|
||
async dismissGroup(groupId: string): Promise<void> {
|
||
await apiRequest(`/api/im/groups/${encodeURIComponent(groupId)}`, { method: 'DELETE' })
|
||
},
|
||
|
||
async sendGroupJoinRequest(groupId: string, remark?: string): Promise<GroupJoinRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<GroupJoinRequest>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests`, {
|
||
method: 'POST',
|
||
params: {
|
||
appKey: config.appKey,
|
||
...(remark ? { remark } : {}),
|
||
},
|
||
})
|
||
},
|
||
|
||
async listGroupJoinRequests(groupId: string): Promise<GroupJoinRequest[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<GroupJoinRequest[] | { content?: GroupJoinRequest[] }>(
|
||
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests`,
|
||
{
|
||
params: { appKey: config.appKey },
|
||
},
|
||
)
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async acceptGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<GroupJoinRequest>(
|
||
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/accept`,
|
||
{
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
},
|
||
)
|
||
},
|
||
|
||
async rejectGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<GroupJoinRequest>(
|
||
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/reject`,
|
||
{
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
},
|
||
)
|
||
},
|
||
|
||
async listFriends(): Promise<string[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<{ data?: string[] } | string[]>('/api/im/friends', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.data ?? [])
|
||
},
|
||
|
||
async addFriend(friendId: string): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest('/api/im/friends', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey, friendId },
|
||
})
|
||
},
|
||
|
||
async removeFriend(friendId: string): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/friends/${encodeURIComponent(friendId)}`, {
|
||
method: 'DELETE',
|
||
params: { appKey: config.appKey },
|
||
})
|
||
},
|
||
|
||
async removeAllFriends(): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest('/api/im/friends', {
|
||
method: 'DELETE',
|
||
params: { appKey: config.appKey },
|
||
})
|
||
},
|
||
|
||
async setFriendGroup(friendId: string, groupName?: string): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/friends/${encodeURIComponent(friendId)}/group`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
...(groupName ? { groupName } : {}),
|
||
},
|
||
})
|
||
},
|
||
|
||
async listFriendGroups(): Promise<string[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<string[] | { content?: string[] }>('/api/im/friends/groups', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async listFriendsByGroup(groupName: string): Promise<string[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<string[] | { content?: string[] }>(
|
||
`/api/im/friends/groups/${encodeURIComponent(groupName)}`,
|
||
{ params: { appKey: config.appKey } },
|
||
)
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise<FriendRequest[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<FriendRequest[] | { content?: FriendRequest[] }>('/api/im/friend-requests', {
|
||
params: {
|
||
appKey: config.appKey,
|
||
direction,
|
||
},
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async sendFriendRequest(toUserId: string, remark?: string): Promise<FriendRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<FriendRequest>('/api/im/friend-requests', {
|
||
method: 'POST',
|
||
params: {
|
||
appKey: config.appKey,
|
||
toUserId,
|
||
...(remark ? { remark } : {}),
|
||
},
|
||
})
|
||
},
|
||
|
||
async acceptFriendRequest(requestId: string): Promise<FriendRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<FriendRequest>(`/api/im/friend-requests/${encodeURIComponent(requestId)}/accept`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
})
|
||
},
|
||
|
||
async rejectFriendRequest(requestId: string): Promise<FriendRequest> {
|
||
const config = getConfig()
|
||
return apiRequest<FriendRequest>(`/api/im/friend-requests/${encodeURIComponent(requestId)}/reject`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
})
|
||
},
|
||
|
||
async listBlacklist(): Promise<BlacklistEntry[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<BlacklistEntry[] | { content?: BlacklistEntry[] }>('/api/im/blacklist', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async addToBlacklist(blockedUserId: string): Promise<BlacklistEntry> {
|
||
const config = getConfig()
|
||
return apiRequest<BlacklistEntry>('/api/im/blacklist', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey, blockedUserId },
|
||
})
|
||
},
|
||
|
||
async removeFromBlacklist(blockedUserId: string): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest('/api/im/blacklist', {
|
||
method: 'DELETE',
|
||
params: { appKey: config.appKey, blockedUserId },
|
||
})
|
||
},
|
||
|
||
async checkBlacklist(targetUserId: string): Promise<BlacklistCheckResult> {
|
||
const config = getConfig()
|
||
return apiRequest<BlacklistCheckResult>('/api/im/blacklist/check', {
|
||
params: { appKey: config.appKey, targetUserId },
|
||
})
|
||
},
|
||
|
||
async getProfile(userId: string): Promise<UserProfile> {
|
||
const config = getConfig()
|
||
return apiRequest<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
},
|
||
|
||
async updateProfile(
|
||
userId: string,
|
||
nickname?: string,
|
||
avatar?: string,
|
||
gender?: string,
|
||
): Promise<UserProfile> {
|
||
const config = getConfig()
|
||
return apiRequest<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
...(nickname ? { nickname } : {}),
|
||
...(avatar ? { avatar } : {}),
|
||
...(gender ? { gender } : {}),
|
||
},
|
||
})
|
||
},
|
||
|
||
async searchUsers(keyword: string, size = 20): Promise<UserProfile[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<UserProfile[] | { content?: UserProfile[] }>('/api/im/accounts/search', {
|
||
params: {
|
||
appKey: config.appKey,
|
||
keyword,
|
||
size: String(size),
|
||
},
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async searchGroups(keyword: string, size = 20): Promise<ImGroup[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<ImGroup[] | { content?: ImGroup[] }>('/api/im/groups/search', {
|
||
params: {
|
||
appKey: config.appKey,
|
||
keyword,
|
||
size: String(size),
|
||
},
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async listConversations(): Promise<ConversationData[]> {
|
||
const config = getConfig()
|
||
if (ImDatabase.isInitialized()) {
|
||
const models = await ImDatabase.getConversations(config.appKey)
|
||
if (models.length > 0) {
|
||
const conversations = models.map(model => normalizeConversation({
|
||
targetId: model.targetId,
|
||
chatType: model.chatType,
|
||
lastMsgContent: model.lastMsgContent,
|
||
lastMsgType: model.lastMsgType,
|
||
lastMsgTime: model.lastMsgTime,
|
||
unreadCount: model.unreadCount,
|
||
isMuted: model.isMuted,
|
||
isPinned: model.isPinned,
|
||
conversationGroup: null,
|
||
}))
|
||
conversationMemory = conversations
|
||
emitConversationMemory()
|
||
return conversations
|
||
}
|
||
}
|
||
|
||
const res = await apiRequest<ConversationData[] | { content?: ConversationData[] }>('/api/im/conversations', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
const conversations = Array.isArray(res) ? res : (res.content ?? [])
|
||
const normalized = conversations.map(normalizeConversation)
|
||
conversationMemory = normalized
|
||
emitConversationMemory()
|
||
return normalized
|
||
},
|
||
|
||
/**
|
||
* Subscribe to conversation list changes.
|
||
* Returns the current conversation list immediately via the callback,
|
||
* then invokes the callback again whenever the list changes.
|
||
* @returns An unsubscribe function.
|
||
*/
|
||
subscribeConversations(callback: (conversations: ConversationData[]) => void): () => void {
|
||
if (!ImDatabase.isInitialized()) {
|
||
conversationListeners.add(callback)
|
||
callback(sortConversationsByRuntime(conversationMemory))
|
||
return () => {
|
||
conversationListeners.delete(callback)
|
||
}
|
||
}
|
||
const config = getConfig()
|
||
return ImDatabase.subscribeConversations(config.appKey, (models) => {
|
||
const data: ConversationData[] = models.map(c => normalizeConversation({
|
||
targetId: c.targetId,
|
||
chatType: c.chatType,
|
||
lastMsgContent: c.lastMsgContent,
|
||
lastMsgType: c.lastMsgType,
|
||
lastMsgTime: c.lastMsgTime,
|
||
unreadCount: c.unreadCount,
|
||
isMuted: c.isMuted,
|
||
isPinned: c.isPinned,
|
||
conversationGroup: null,
|
||
}))
|
||
callback(data)
|
||
})
|
||
},
|
||
|
||
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, {
|
||
method: 'PUT',
|
||
params: { appKey: config.appKey, chatType },
|
||
})
|
||
if (ImDatabase.isInitialized()) {
|
||
await ImDatabase.markRead(config.appKey, targetId)
|
||
} else {
|
||
markConversationReadMemory(targetId, chatType)
|
||
}
|
||
},
|
||
|
||
async setConversationMuted(targetId: string, chatType: ChatType, muted: boolean): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/muted`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
chatType,
|
||
muted: String(muted),
|
||
},
|
||
})
|
||
if (ImDatabase.isInitialized()) {
|
||
await ImDatabase.setConversationMuted(config.appKey, targetId, muted)
|
||
} else {
|
||
setConversationMutedMemory(targetId, chatType, muted)
|
||
}
|
||
},
|
||
|
||
async setConversationPinned(targetId: string, chatType: ChatType, pinned: boolean): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/pinned`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
chatType,
|
||
pinned: String(pinned),
|
||
},
|
||
})
|
||
if (ImDatabase.isInitialized()) {
|
||
await ImDatabase.setConversationPinned(config.appKey, targetId, pinned)
|
||
} else {
|
||
setConversationPinnedMemory(targetId, chatType, pinned)
|
||
}
|
||
},
|
||
|
||
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
|
||
const config = getConfig()
|
||
const draftKey = `${config.appKey}:${chatType}:${targetId}`
|
||
draftStore.set(draftKey, draft)
|
||
if (ImDatabase.isInitialized()) {
|
||
await ImDatabase.setDraft(config.appKey, targetId, chatType, draft)
|
||
}
|
||
},
|
||
|
||
async setConversationHidden(targetId: string, chatType: ChatType, hidden: boolean): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/hidden`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
chatType,
|
||
hidden: String(hidden),
|
||
},
|
||
})
|
||
if (hidden) {
|
||
deleteConversationMemory(targetId, chatType)
|
||
}
|
||
},
|
||
|
||
async setConversationGroup(targetId: string, chatType: ChatType, groupName?: string): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}/group`, {
|
||
method: 'PUT',
|
||
params: {
|
||
appKey: config.appKey,
|
||
chatType,
|
||
...(groupName ? { groupName } : {}),
|
||
},
|
||
})
|
||
setConversationGroupMemory(targetId, chatType, groupName ?? null)
|
||
},
|
||
|
||
async listConversationGroups(): Promise<string[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<string[] | { content?: string[] }>('/api/im/conversation-groups', {
|
||
params: { appKey: config.appKey },
|
||
})
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async listConversationGroupItems(groupName: string): Promise<ConversationGroupItem[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<ConversationGroupItem[] | { content?: ConversationGroupItem[] }>(
|
||
`/api/im/conversation-groups/${encodeURIComponent(groupName)}`,
|
||
{ params: { appKey: config.appKey } },
|
||
)
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async adminGroupReadReceipts(groupId: string, messageIds: string[]): Promise<GroupReadReceiptSummary[]> {
|
||
const config = getConfig()
|
||
const res = await apiRequest<GroupReadReceiptSummary[] | { content?: GroupReadReceiptSummary[] }>(
|
||
`/api/im/admin/groups/${encodeURIComponent(groupId)}/read-receipts`,
|
||
{
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { messageIds },
|
||
},
|
||
)
|
||
return Array.isArray(res) ? res : (res.content ?? [])
|
||
},
|
||
|
||
async batchAddFriends(friendIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>('/api/im/friends/batch', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { friendIds },
|
||
})
|
||
},
|
||
|
||
async batchRemoveFriends(friendIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>('/api/im/friends/batch/remove', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { friendIds },
|
||
})
|
||
},
|
||
|
||
async batchAcceptFriendRequests(requestIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>('/api/im/friend-requests/batch/accept', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { requestIds },
|
||
})
|
||
},
|
||
|
||
async batchRejectFriendRequests(requestIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>('/api/im/friend-requests/batch/reject', {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { requestIds },
|
||
})
|
||
},
|
||
|
||
async batchAddGroupMembers(groupId: string, userIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/batch`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { userIds },
|
||
})
|
||
},
|
||
|
||
async batchRemoveGroupMembers(groupId: string, userIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/batch/remove`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { userIds },
|
||
})
|
||
},
|
||
|
||
async batchAcceptGroupJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/batch/accept`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { requestIds },
|
||
})
|
||
},
|
||
|
||
async batchRejectGroupJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/batch/reject`, {
|
||
method: 'POST',
|
||
params: { appKey: config.appKey },
|
||
body: { requestIds },
|
||
})
|
||
},
|
||
|
||
async modifyGroupMemberInfo(groupId: string, userId: string, nickname?: string, role?: string): Promise<void> {
|
||
const config = getConfig()
|
||
const body: Record<string, string> = {}
|
||
if (nickname !== undefined) body.nickname = nickname
|
||
if (role !== undefined) body.role = role
|
||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}/info`, {
|
||
method: 'PUT',
|
||
params: { appKey: config.appKey },
|
||
body,
|
||
})
|
||
},
|
||
|
||
async getDraft(targetId: string, chatType: ChatType): Promise<string> {
|
||
const config = getConfig()
|
||
if (ImDatabase.isInitialized()) {
|
||
const draft = await ImDatabase.getConversationDraft(config.appKey, targetId, chatType)
|
||
if (draft !== null) {
|
||
return draft
|
||
}
|
||
}
|
||
const draftKey = `${config.appKey}:${chatType}:${targetId}`
|
||
return draftStore.get(draftKey) ?? ''
|
||
},
|
||
|
||
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
|
||
const config = getConfig()
|
||
await apiRequest(`/api/im/conversations/${encodeURIComponent(targetId)}`, {
|
||
method: 'DELETE',
|
||
params: {
|
||
appKey: config.appKey,
|
||
chatType,
|
||
},
|
||
})
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.deleteConversation(config.appKey, targetId, chatType, _currentUserId)
|
||
} else {
|
||
deleteConversationMemory(targetId, chatType)
|
||
}
|
||
},
|
||
|
||
async getTotalUnreadCount(): Promise<number> {
|
||
const conversations = await ImSDK.listConversations()
|
||
return conversations.reduce((sum, conversation) => sum + conversation.unreadCount, 0)
|
||
},
|
||
|
||
async syncHistoryForAllConversations(): Promise<void> {
|
||
await _syncHistoryForAllConversations()
|
||
},
|
||
|
||
async searchMessages(params: MessageSearchParams & { appKey?: string }) {
|
||
if (!ImDatabase.isInitialized()) return []
|
||
const config = getConfig()
|
||
const { appKey, ...rest } = params
|
||
return ImDatabase.searchMessages(appKey ?? config.appKey, rest)
|
||
},
|
||
|
||
addListener(listener: ImEventListener): void {
|
||
const wrapped: ImEventListener = {
|
||
...listener,
|
||
onMessage: async (msg) => {
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(normalizeMessage(msg))
|
||
}
|
||
listener.onMessage?.(normalizeMessage(msg))
|
||
},
|
||
onGroupMessage: async (msg) => {
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(normalizeMessage(msg))
|
||
}
|
||
listener.onGroupMessage?.(normalizeMessage(msg))
|
||
},
|
||
onSystemMessage: async (msg) => {
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(normalizeMessage(msg))
|
||
}
|
||
listener.onSystemMessage?.(normalizeMessage(msg))
|
||
},
|
||
onRead: async (msg) => {
|
||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||
} else {
|
||
applyMessageToMemory(normalizeMessage(msg))
|
||
}
|
||
listener.onRead?.(normalizeMessage(msg))
|
||
},
|
||
onRevoke: async (data) => {
|
||
if (ImDatabase.isInitialized()) {
|
||
await ImDatabase.revokeMessage(getConfig().appKey, data.msgId)
|
||
} else {
|
||
applyRevokeToMemory(data.msgId)
|
||
}
|
||
listener.onRevoke?.(data)
|
||
},
|
||
}
|
||
listenerMap.set(listener, wrapped)
|
||
client?.addListener(wrapped)
|
||
},
|
||
|
||
removeListener(listener: ImEventListener): void {
|
||
const wrapped = listenerMap.get(listener)
|
||
client?.removeListener(wrapped ?? listener)
|
||
listenerMap.delete(listener)
|
||
},
|
||
|
||
subscribeGroup(groupId: string): void {
|
||
client?.subscribeGroup(groupId)
|
||
},
|
||
|
||
unsubscribeGroup(groupId: string): void {
|
||
client?.unsubscribeGroup(groupId)
|
||
},
|
||
|
||
isConnected(): boolean {
|
||
return client?.isConnected() ?? false
|
||
},
|
||
|
||
disconnect(): void {
|
||
client?.disconnect()
|
||
client = null
|
||
_currentUserId = null
|
||
_currentUserSig = null
|
||
setCommonUserId(null)
|
||
},
|
||
|
||
/**
|
||
* 刷新 IM userSig(过期时调用)。
|
||
*/
|
||
async refreshToken(userSig: string): Promise<void> {
|
||
if (!_currentUserId) throw new Error('[ImSDK] Not logged in — call setUserInfo first.')
|
||
await ImSDK.login(_currentUserId, userSig)
|
||
},
|
||
}
|
||
|
||
// ─── 订阅 XuqmSDK.setUserInfo ──────────────────────────────────────────────────
|
||
|
||
_registerUserInfoHandler(async (info: XuqmUserInfo | null) => {
|
||
if (!info) {
|
||
ImSDK.disconnect()
|
||
return
|
||
}
|
||
// 仅当 IM 服务已开通且 userSig 存在时自动登录
|
||
if (!info.userSig) return
|
||
try {
|
||
const { getConfig, isInitialized } = await import('@xuqm/rn-common')
|
||
if (!isInitialized()) return
|
||
const config = getConfig()
|
||
if (!config.imEnabled) return
|
||
} catch {
|
||
return
|
||
}
|
||
await ImSDK.login(info.userId, info.userSig).catch(() => {})
|
||
})
|