feat(im): 添加即时通讯功能模块
- 添加了 IM API 接口定义,包含登录、消息、群组、好友等接口 - 实现了 ImSDK 核心功能,支持发送各类消息和管理会话 - 集成了 WebSocket 连接管理和自动重连机制 - 添加了本地联系人缓存并优化对话标题显示逻辑 - 实现了 HarmonyOS 平台 HTTP 客户端基础功能
这个提交包含在:
父节点
2ca58aa458
当前提交
765d8a0333
@ -16,6 +16,7 @@ export interface XuqmConfig {
|
||||
}
|
||||
|
||||
let _config: XuqmConfig | null = null
|
||||
let _userId: string | null = null
|
||||
|
||||
export function initConfigFromRemote(
|
||||
options: XuqmInitOptions,
|
||||
@ -37,6 +38,14 @@ export function getConfig(): XuqmConfig {
|
||||
return _config
|
||||
}
|
||||
|
||||
export function setUserId(userId: string | null): void {
|
||||
_userId = userId
|
||||
}
|
||||
|
||||
export function getUserId(): string | null {
|
||||
return _userId
|
||||
}
|
||||
|
||||
export function isInitialized(): boolean {
|
||||
return _config !== null
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export { XuqmSDK } from './sdk'
|
||||
export type { XuqmInitOptions, XuqmConfig } from './config'
|
||||
export { getConfig, isInitialized } from './config'
|
||||
export { getConfig, isInitialized, setUserId, getUserId } from './config'
|
||||
export { apiRequest, _getToken, _saveToken, _clearToken } from './http'
|
||||
export { API_BASE_URL, IM_WS_URL } from './constants'
|
||||
export { getDeviceId, getDeviceInfo, detectPushVendor } from './device'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { initConfigFromRemote, isInitialized, type XuqmInitOptions } from './config'
|
||||
import { initConfigFromRemote, isInitialized, type XuqmInitOptions, setUserId as setCommonUserId, getUserId as getCommonUserId } from './config'
|
||||
|
||||
export const XuqmSDK = {
|
||||
/**
|
||||
@ -50,4 +50,12 @@ export const XuqmSDK = {
|
||||
apiBaseUrl: options.serverUrl,
|
||||
})
|
||||
},
|
||||
|
||||
setUserId(userId: string | null): void {
|
||||
setCommonUserId(userId)
|
||||
},
|
||||
|
||||
getUserId(): string | null {
|
||||
return getCommonUserId()
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { _getToken, getConfig } from '@xuqm/rn-common'
|
||||
import { _getToken, getConfig, getUserId } from '@xuqm/rn-common'
|
||||
import type { ImEventListener, ImMessage, SendMessageParams } from './types'
|
||||
|
||||
interface StompFrame {
|
||||
@ -50,8 +50,8 @@ export class ImClient {
|
||||
msgType: SendMessageParams['msgType'],
|
||||
content: string,
|
||||
mentionedUserIds?: string,
|
||||
) {
|
||||
this.send({
|
||||
): ImMessage {
|
||||
return this.send({
|
||||
toId,
|
||||
chatType,
|
||||
msgType,
|
||||
@ -60,13 +60,14 @@ export class ImClient {
|
||||
})
|
||||
}
|
||||
|
||||
send(params: SendMessageParams) {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('IM not connected')
|
||||
}
|
||||
send(params: SendMessageParams): ImMessage {
|
||||
if (!this.activeAppId) {
|
||||
throw new Error('IM appId not configured')
|
||||
}
|
||||
const outgoing = this.buildOutgoingMessage(params)
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
return { ...outgoing, status: 'FAILED' }
|
||||
}
|
||||
|
||||
this.sendFrame(
|
||||
'SEND',
|
||||
@ -76,6 +77,7 @@ export class ImClient {
|
||||
},
|
||||
JSON.stringify({
|
||||
appId: this.activeAppId,
|
||||
messageId: outgoing.id,
|
||||
toId: params.toId,
|
||||
chatType: params.chatType,
|
||||
msgType: params.msgType,
|
||||
@ -83,6 +85,8 @@ export class ImClient {
|
||||
mentionedUserIds: params.mentionedUserIds ?? '',
|
||||
}),
|
||||
)
|
||||
|
||||
return outgoing
|
||||
}
|
||||
|
||||
revoke(messageId: string) {
|
||||
@ -205,7 +209,7 @@ export class ImClient {
|
||||
}
|
||||
|
||||
if (frame.command === 'MESSAGE') {
|
||||
const message: ImMessage = JSON.parse(frame.body)
|
||||
const message: ImMessage = this.normalizeMessage(JSON.parse(frame.body))
|
||||
if (message.chatType === 'GROUP') {
|
||||
this.listeners.forEach(listener => listener.onGroupMessage?.(message))
|
||||
return
|
||||
@ -253,4 +257,38 @@ export class ImClient {
|
||||
return { command, headers, body }
|
||||
})
|
||||
}
|
||||
|
||||
private buildOutgoingMessage(params: SendMessageParams): ImMessage {
|
||||
const userId = getUserId() ?? ''
|
||||
const messageId = params.messageId ?? this.generateMessageId()
|
||||
return {
|
||||
id: messageId,
|
||||
appId: this.activeAppId ?? getConfig().appId,
|
||||
fromUserId: userId,
|
||||
fromId: userId,
|
||||
toId: params.toId,
|
||||
chatType: params.chatType,
|
||||
msgType: params.msgType,
|
||||
content: params.content,
|
||||
status: 'SENDING',
|
||||
mentionedUserIds: params.mentionedUserIds,
|
||||
groupReadCount: 0,
|
||||
revoked: false,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
private generateMessageId(): string {
|
||||
const cryptoId = globalThis.crypto?.randomUUID?.()
|
||||
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
private normalizeMessage(message: ImMessage): ImMessage {
|
||||
return {
|
||||
...message,
|
||||
fromId: message.fromId ?? message.fromUserId,
|
||||
revoked: message.revoked ?? message.status === 'REVOKED',
|
||||
appId: message.appId ?? (this.activeAppId ?? getConfig().appId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo } from '@xuqm/rn-common'
|
||||
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId } from '@xuqm/rn-common'
|
||||
import { ImClient } from './ImClient'
|
||||
import { ImDatabase } from './db/ImDatabase'
|
||||
import type { MessageSearchParams } from './db/ImDatabase'
|
||||
@ -12,12 +12,56 @@ import type {
|
||||
ImGroup,
|
||||
ImMessage,
|
||||
MsgType,
|
||||
UserProfile,
|
||||
} from './types'
|
||||
import { uploadFile } from './upload'
|
||||
|
||||
let client: ImClient | null = null
|
||||
let _currentUserId: string | null = null
|
||||
|
||||
function generateMessageId(): string {
|
||||
const cryptoId = globalThis.crypto?.randomUUID?.()
|
||||
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
function normalizeMessage(msg: ImMessage, fallback?: Partial<ImMessage>): ImMessage {
|
||||
return {
|
||||
...fallback,
|
||||
...msg,
|
||||
appId: msg.appId ?? fallback?.appId ?? getConfig().appId,
|
||||
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,
|
||||
appId: config.appId,
|
||||
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
|
||||
|
||||
@ -124,6 +168,7 @@ export const ImSDK = {
|
||||
})
|
||||
await _saveToken(res.token)
|
||||
_currentUserId = userId
|
||||
setCommonUserId(userId)
|
||||
|
||||
if (dbName !== undefined || ImDatabase.isInitialized()) {
|
||||
ImDatabase.init(dbName ?? 'xuqm_im')
|
||||
@ -146,6 +191,7 @@ export const ImSDK = {
|
||||
const config = getConfig()
|
||||
await _saveToken(token)
|
||||
_currentUserId = userId
|
||||
setCommonUserId(userId)
|
||||
|
||||
if (dbName !== undefined || ImDatabase.isInitialized()) {
|
||||
ImDatabase.init(dbName ?? 'xuqm_im')
|
||||
@ -294,15 +340,38 @@ export const ImSDK = {
|
||||
mentionedUserIds?: string,
|
||||
): Promise<ImMessage> {
|
||||
const config = getConfig()
|
||||
const msg = await apiRequest<ImMessage>('/api/im/messages/send', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { toId, chatType, msgType, content, mentionedUserIds: mentionedUserIds ?? '' },
|
||||
const outgoing = buildOutgoingMessage({
|
||||
toId,
|
||||
chatType,
|
||||
msgType,
|
||||
content,
|
||||
mentionedUserIds,
|
||||
})
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(msg, _currentUserId)
|
||||
try {
|
||||
const msg = await apiRequest<ImMessage>('/api/im/messages/send', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: {
|
||||
toId,
|
||||
chatType,
|
||||
msgType,
|
||||
content,
|
||||
mentionedUserIds: mentionedUserIds ?? '',
|
||||
messageId: outgoing.id,
|
||||
},
|
||||
})
|
||||
const finalMsg = normalizeMessage(msg, outgoing)
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(finalMsg, _currentUserId)
|
||||
}
|
||||
return finalMsg
|
||||
} catch (error) {
|
||||
const failed = { ...outgoing, status: 'FAILED' as const }
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(failed, _currentUserId)
|
||||
}
|
||||
return failed
|
||||
}
|
||||
return msg
|
||||
},
|
||||
|
||||
async sendTextMessage(
|
||||
@ -457,7 +526,7 @@ export const ImSDK = {
|
||||
params: { appId: config.appId },
|
||||
})
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(msg, _currentUserId)
|
||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||
}
|
||||
return msg
|
||||
},
|
||||
@ -669,6 +738,31 @@ export const ImSDK = {
|
||||
})
|
||||
},
|
||||
|
||||
async getProfile(userId: string): Promise<UserProfile> {
|
||||
const config = getConfig()
|
||||
return apiRequest<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
|
||||
params: { appId: config.appId },
|
||||
})
|
||||
},
|
||||
|
||||
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: {
|
||||
appId: config.appId,
|
||||
...(nickname ? { nickname } : {}),
|
||||
...(avatar ? { avatar } : {}),
|
||||
...(gender ? { gender } : {}),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async listConversations(): Promise<ConversationData[]> {
|
||||
const config = getConfig()
|
||||
if (ImDatabase.isInitialized()) {
|
||||
@ -802,15 +896,15 @@ export const ImSDK = {
|
||||
...listener,
|
||||
onMessage: async (msg) => {
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(msg, _currentUserId)
|
||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||
}
|
||||
listener.onMessage?.(msg)
|
||||
listener.onMessage?.(normalizeMessage(msg))
|
||||
},
|
||||
onGroupMessage: async (msg) => {
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
await ImDatabase.saveMessage(msg, _currentUserId)
|
||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||
}
|
||||
listener.onGroupMessage?.(msg)
|
||||
listener.onGroupMessage?.(normalizeMessage(msg))
|
||||
},
|
||||
})
|
||||
},
|
||||
@ -835,5 +929,6 @@ export const ImSDK = {
|
||||
client?.disconnect()
|
||||
client = null
|
||||
_currentUserId = null
|
||||
setCommonUserId(null)
|
||||
},
|
||||
}
|
||||
|
||||
@ -12,9 +12,12 @@ export type {
|
||||
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
|
||||
ImEventListener, SendMessageParams,
|
||||
ConversationData,
|
||||
HistoryQuery,
|
||||
PageResult,
|
||||
FriendRequest,
|
||||
GroupJoinRequest,
|
||||
BlacklistEntry,
|
||||
UserProfile,
|
||||
} from './types'
|
||||
export { uploadFile } from './upload'
|
||||
export type { UploadResult } from './upload'
|
||||
|
||||
@ -23,6 +23,7 @@ export interface ImMessage {
|
||||
id: string
|
||||
appId: string
|
||||
fromUserId: string
|
||||
fromId?: string
|
||||
toId: string
|
||||
chatType: ChatType
|
||||
msgType: MsgType
|
||||
@ -30,6 +31,7 @@ export interface ImMessage {
|
||||
status: MsgStatus
|
||||
mentionedUserIds?: string
|
||||
groupReadCount?: number
|
||||
revoked?: boolean
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
@ -42,6 +44,7 @@ export interface ImEventListener {
|
||||
}
|
||||
|
||||
export interface SendMessageParams {
|
||||
messageId?: string
|
||||
toId: string
|
||||
chatType: ChatType
|
||||
msgType: MsgType
|
||||
@ -72,6 +75,27 @@ export interface ConversationData {
|
||||
isPinned: boolean
|
||||
}
|
||||
|
||||
export interface HistoryQuery {
|
||||
msgType?: MsgType
|
||||
keyword?: string
|
||||
startTime?: Date | string | number
|
||||
endTime?: Date | string | number
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
content: T[]
|
||||
totalElements: number
|
||||
totalPages: number
|
||||
size: number
|
||||
number: number
|
||||
numberOfElements: number
|
||||
first: boolean
|
||||
last: boolean
|
||||
empty: boolean
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
id: string
|
||||
appId: string
|
||||
@ -101,3 +125,14 @@ export interface BlacklistEntry {
|
||||
blockedUserId: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id?: string
|
||||
appId?: string
|
||||
userId: string
|
||||
nickname?: string | null
|
||||
avatar?: string | null
|
||||
gender?: string | null
|
||||
status?: string | null
|
||||
createdAt?: number | null
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
export { XuqmSDK } from '../packages/common/src'
|
||||
export type { XuqmInitOptions, DeviceInfo } from '../packages/common/src'
|
||||
export { getDeviceId, getDeviceInfo, detectPushVendor } from '../packages/common/src'
|
||||
export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '../packages/common/src'
|
||||
export { ScaledImage } from '../packages/common/src'
|
||||
|
||||
export { ImSDK } from '../packages/im/src'
|
||||
@ -15,6 +15,8 @@ export type {
|
||||
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
|
||||
ImEventListener, SendMessageParams,
|
||||
ConversationData,
|
||||
HistoryQuery,
|
||||
PageResult,
|
||||
FriendRequest,
|
||||
GroupJoinRequest,
|
||||
BlacklistEntry,
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户