diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index 174e966..4bba66f 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -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 } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 90ae1e5..cad6108 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -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' diff --git a/packages/common/src/sdk.ts b/packages/common/src/sdk.ts index a79fce8..192bf6b 100644 --- a/packages/common/src/sdk.ts +++ b/packages/common/src/sdk.ts @@ -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() + }, } diff --git a/packages/im/src/ImClient.ts b/packages/im/src/ImClient.ts index 1752fee..2ee0f30 100644 --- a/packages/im/src/ImClient.ts +++ b/packages/im/src/ImClient.ts @@ -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), + } + } } diff --git a/packages/im/src/ImSDK.ts b/packages/im/src/ImSDK.ts index b18829f..1af3c64 100644 --- a/packages/im/src/ImSDK.ts +++ b/packages/im/src/ImSDK.ts @@ -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 { + 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 { 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 { const config = getConfig() - const msg = await apiRequest('/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('/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 { + const config = getConfig() + return apiRequest(`/api/im/accounts/${encodeURIComponent(userId)}`, { + params: { appId: config.appId }, + }) + }, + + async updateProfile( + userId: string, + nickname?: string, + avatar?: string, + gender?: string, + ): Promise { + const config = getConfig() + return apiRequest(`/api/im/accounts/${encodeURIComponent(userId)}`, { + method: 'PUT', + params: { + appId: config.appId, + ...(nickname ? { nickname } : {}), + ...(avatar ? { avatar } : {}), + ...(gender ? { gender } : {}), + }, + }) + }, + async listConversations(): Promise { 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) }, } diff --git a/packages/im/src/index.ts b/packages/im/src/index.ts index b8ec220..6041be1 100644 --- a/packages/im/src/index.ts +++ b/packages/im/src/index.ts @@ -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' diff --git a/packages/im/src/types.ts b/packages/im/src/types.ts index c0fc641..7f2e0da 100644 --- a/packages/im/src/types.ts +++ b/packages/im/src/types.ts @@ -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 { + 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 +} diff --git a/src/index.ts b/src/index.ts index f804d9a..b6cfb19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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,