feat(chat): 添加聊天界面和文件更新SDK功能
- 实现完整的聊天界面UI组件,支持文本、图片、视频、音频、文件等多种消息类型 - 集成IM消息收发功能,实现消息气泡显示和用户头像占位符 - 添加媒体文件选择和拍摄功能,支持相册图片、视频及相机拍照录像 - 实现语音录制和播放功能,包含按住说话交互和权限处理 - 添加群组提及功能,支持@用户和提及候选列表显示 - 实现消息回复和引用功能,支持消息长按回复操作 - 添加本地消息搜索功能,支持搜索当前会话的历史消息 - 实现文件上传下载功能,集成FileSDK进行文件传输管理 - 添加应用更新检查功能,集成UpdateSDK支持版本更新 - 实现消息状态显示,包括发送、送达、已读等状态标识 - 添加群组已读人数统计,显示消息在群聊中的阅读情况 - 实现草稿保存和恢复功能,支持断点续聊体验 - 添加连接状态横幅,实时显示IM服务连接状态 - 实现滚动加载更多历史消息,优化大量消息的性能表现 - 添加多媒体文件下载保存功能,支持保存到应用专属目录
这个提交包含在:
父节点
930c8f36ae
当前提交
d7f156f160
1
.hvigor/cache/file-cache.json
vendored
普通文件
1
.hvigor/cache/file-cache.json
vendored
普通文件
文件差异因一行或多行过长而隐藏
1
.hvigor/cache/meta.json
vendored
普通文件
1
.hvigor/cache/meta.json
vendored
普通文件
@ -0,0 +1 @@
|
||||
{"compileSdkVersion":"6.0.2(22)","hvigorVersion":"6.22.4","toolChainsVersion":"6.0.2.130"}
|
||||
1
.hvigor/cache/task-cache.json
vendored
普通文件
1
.hvigor/cache/task-cache.json
vendored
普通文件
文件差异因一行或多行过长而隐藏
@ -0,0 +1 @@
|
||||
{"basePath":"/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-HarmonySDK/.hvigor/dependencyMap/dependencyMap.json5","rootDependency":"./oh-package.json5","dependencyMap":{"xuqmSdk":"./xuqmSdk/oh-package.json5","entry":"./entry/oh-package.json5"},"modules":[{"name":"xuqmSdk","srcPath":"../../../xuqm-sdk"},{"name":"entry","srcPath":"../../../entry"}]}
|
||||
@ -0,0 +1 @@
|
||||
{"name":"entry","version":"1.0.0","description":"SDK sample app","main":"","author":"","license":"MIT","dependencies":{"@xuqm/harmony-sdk":"file:../xuqm-sdk"}}
|
||||
@ -0,0 +1 @@
|
||||
{"name":"xuqm-harmony-sdk-workspace","version":"0.1.0","modelVersion":"5.0.0","description":"XuqmGroup HarmonyOS SDK workspace","author":"xuqm","license":"MIT"}
|
||||
@ -0,0 +1 @@
|
||||
{"name":"@xuqm/harmony-sdk","version":"0.1.0","description":"XuqmGroup HarmonyOS SDK — IM, Push, Version Management","main":"Index.ets","author":"xuqm","license":"MIT","publishConfig":{"registry":"https://ohpm.openharmony.cn/ohpm/"},"dependencies":{}}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"HVIGOR_OHOS_PLUGIN": {
|
||||
"MODULES": [
|
||||
{
|
||||
"MODULE_NAME": "77aabe6c19463543339f337db9c84e4d10fd2f56ea0aedaf85a0214d59e93ec4",
|
||||
"API_TYPE": "stageMode",
|
||||
"INCLUDE_IN_BUILD": true,
|
||||
"IS_COMMAND_LINE_ENTRY_MODULE": false,
|
||||
"MODULE_TYPE": "har",
|
||||
"IS_INCREMENTAL_MODULE": true
|
||||
},
|
||||
{
|
||||
"MODULE_NAME": "923fe53966c6cd9343e11af776cd4b05be315ea4b200b02e4d5dfb0f929b73bf",
|
||||
"API_TYPE": "stageMode",
|
||||
"INCLUDE_IN_BUILD": true,
|
||||
"IS_COMMAND_LINE_ENTRY_MODULE": false,
|
||||
"MODULE_TYPE": "entry",
|
||||
"INCREMENTAL_TASKS": {
|
||||
"COMPILE_ARKTS": false
|
||||
},
|
||||
"IS_INCREMENTAL_MODULE": false
|
||||
}
|
||||
],
|
||||
"NATIVE_COMPILER": "Default",
|
||||
"IS_FULL_BUILD": true,
|
||||
"BUILD_MODE": "debug"
|
||||
},
|
||||
"HVIGOR": {
|
||||
"IS_INCREMENTAL": true,
|
||||
"IS_DAEMON": false,
|
||||
"IS_PARALLEL": true,
|
||||
"IS_HVIGORFILE_TYPE_CHECK": false,
|
||||
"TASK_TIME": {
|
||||
"923fe53966c6cd9343e11af776cd4b05be315ea4b200b02e4d5dfb0f929b73bf": {
|
||||
"CreateModuleInfo": 327208,
|
||||
"PreCheckSyscap": 124792,
|
||||
"ProcessIntegratedHsp": 226959,
|
||||
"SyscapTransform": 6058625,
|
||||
"ProcessStartupConfig": 545625,
|
||||
"ConfigureCmake": 60125,
|
||||
"BuildNativeWithCmake": 69833,
|
||||
"BuildNativeWithNinja": 146584,
|
||||
"BuildJS": 700000,
|
||||
"CompileArkTS": 2592629166,
|
||||
"GeneratePkgModuleJson": 988958,
|
||||
"ProcessCompiledResources": 154291,
|
||||
"PackageHap": 252960708,
|
||||
"PackingCheck": 2527625,
|
||||
"SignHap": 375083,
|
||||
"CollectDebugSymbol": 263875,
|
||||
"assembleHap": 49500
|
||||
},
|
||||
"77aabe6c19463543339f337db9c84e4d10fd2f56ea0aedaf85a0214d59e93ec4": {
|
||||
"ConfigureCmake": 69459,
|
||||
"BuildNativeWithCmake": 70958,
|
||||
"BuildNativeWithNinja": 202959
|
||||
}
|
||||
},
|
||||
"APIS": [
|
||||
"getProperty"
|
||||
],
|
||||
"CONFIG_EXPERIMENT": {
|
||||
"ENABLE_MODULE_SKIP": false,
|
||||
"ENABLE_CPP_FUNCTION_LEVEL_INCREMENTAL": false
|
||||
},
|
||||
"CONFIG_PROPERTIES": {},
|
||||
"BUILD_ID": "202604281921524500",
|
||||
"TOTAL_TIME": 3511224541
|
||||
}
|
||||
}
|
||||
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
文件差异内容过多而无法显示
加载差异
10
AppScope/app.json5
普通文件
10
AppScope/app.json5
普通文件
@ -0,0 +1,10 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.xuqmgroup.harmony.sdk",
|
||||
"vendor": "xuqm",
|
||||
"versionCode": 1000000,
|
||||
"versionName": "1.0.0",
|
||||
"icon": "$media:app_icon",
|
||||
"label": "$string:EntryAbility_label"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
# XuqmGroup HarmonyOS SDK 文档
|
||||
|
||||
> ArkTS · HarmonyOS 5 (API 12) · 发布至 ohpm
|
||||
> 当前工程已可成功执行 `hvigorw assembleHap`
|
||||
|
||||
## 模块结构
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "xuqm-sdk",
|
||||
"name": "xuqmSdk",
|
||||
"srcPath": "./xuqm-sdk",
|
||||
"targets": [
|
||||
{ "name": "default", "applyToProducts": ["default"] }
|
||||
|
||||
19
entry/oh-package-lock.json5
普通文件
19
entry/oh-package-lock.json5
普通文件
@ -0,0 +1,19 @@
|
||||
{
|
||||
"meta": {
|
||||
"stableOrder": true,
|
||||
"enableUnifiedLockfile": false
|
||||
},
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@xuqm/harmony-sdk@../xuqm-sdk": "@xuqm/harmony-sdk@../xuqm-sdk"
|
||||
},
|
||||
"packages": {
|
||||
"@xuqm/harmony-sdk@../xuqm-sdk": {
|
||||
"name": "@xuqm/harmony-sdk",
|
||||
"version": "0.1.0",
|
||||
"resolved": "../xuqm-sdk",
|
||||
"registryType": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
../../../xuqm-sdk
|
||||
@ -1,6 +1,42 @@
|
||||
import { XuqmSDK, ImMessage } from '@xuqm/harmony-sdk'
|
||||
import type { ImEventDelegate } from '@xuqm/harmony-sdk'
|
||||
import promptAction from '@ohos.promptAction'
|
||||
|
||||
class DemoImDelegate implements ImEventDelegate {
|
||||
private readonly onConnectedAction: () => void
|
||||
private readonly onDisconnectedAction: (code: number, reason: string) => void
|
||||
private readonly onMessageAction: (msg: ImMessage) => void
|
||||
private readonly onErrorAction: (message: string) => void
|
||||
|
||||
constructor(
|
||||
onConnectedAction: () => void,
|
||||
onDisconnectedAction: (code: number, reason: string) => void,
|
||||
onMessageAction: (msg: ImMessage) => void,
|
||||
onErrorAction: (message: string) => void,
|
||||
) {
|
||||
this.onConnectedAction = onConnectedAction
|
||||
this.onDisconnectedAction = onDisconnectedAction
|
||||
this.onMessageAction = onMessageAction
|
||||
this.onErrorAction = onErrorAction
|
||||
}
|
||||
|
||||
onConnected(): void {
|
||||
this.onConnectedAction()
|
||||
}
|
||||
|
||||
onDisconnected(code: number, reason: string): void {
|
||||
this.onDisconnectedAction(code, reason)
|
||||
}
|
||||
|
||||
onMessage(msg: ImMessage): void {
|
||||
this.onMessageAction(msg)
|
||||
}
|
||||
|
||||
onError(message: string): void {
|
||||
this.onErrorAction(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct Index {
|
||||
@ -11,22 +47,23 @@ struct Index {
|
||||
|
||||
aboutToAppear(): void {
|
||||
const im = XuqmSDK.im
|
||||
im.delegate = {
|
||||
onConnected: () => {
|
||||
const delegate = new DemoImDelegate(
|
||||
() => {
|
||||
this.connected = true
|
||||
promptAction.showToast({ message: 'IM 已连接' })
|
||||
},
|
||||
onDisconnected: (code, reason) => {
|
||||
(code: number, reason: string) => {
|
||||
this.connected = false
|
||||
console.log(`IM disconnected: ${code} ${reason}`)
|
||||
console.log('IM disconnected: ' + code + ' ' + reason)
|
||||
},
|
||||
onMessage: (msg) => {
|
||||
(msg: ImMessage) => {
|
||||
this.messages = [...this.messages, msg]
|
||||
},
|
||||
onError: (err) => {
|
||||
(err: string) => {
|
||||
promptAction.showToast({ message: 'IM 错误: ' + err })
|
||||
},
|
||||
}
|
||||
)
|
||||
im.delegate = delegate
|
||||
im.connect()
|
||||
}
|
||||
|
||||
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 68 B |
19
hvigor/hvigor-config.json5
普通文件
19
hvigor/hvigor-config.json5
普通文件
@ -0,0 +1,19 @@
|
||||
{
|
||||
"modelVersion": "5.0.0",
|
||||
"dependencies": {},
|
||||
"execution": {
|
||||
"daemon": false,
|
||||
"incremental": true,
|
||||
"parallel": true,
|
||||
"typeCheck": false
|
||||
},
|
||||
"logging": {
|
||||
"level": "info"
|
||||
},
|
||||
"debugging": {
|
||||
"stacktrace": false
|
||||
},
|
||||
"nodeOptions": {
|
||||
"maxOldSpaceSize": 4096
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "xuqm-harmony-sdk-workspace",
|
||||
"version": "0.1.0",
|
||||
"modelVersion": "5.0.0",
|
||||
"description": "XuqmGroup HarmonyOS SDK workspace",
|
||||
"author": "xuqm",
|
||||
"license": "MIT"
|
||||
|
||||
48
oh_modules/.ohpm/lock.json5
普通文件
48
oh_modules/.ohpm/lock.json5
普通文件
@ -0,0 +1,48 @@
|
||||
{
|
||||
"lockVersion": "1.0",
|
||||
"settings": {
|
||||
"resolveConflict": true,
|
||||
"resolveConflictStrict": false,
|
||||
"installAll": true
|
||||
},
|
||||
"overrides": {},
|
||||
"overrideDependencyMap": {},
|
||||
"modules": {
|
||||
".": {
|
||||
"name": "",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"dynamicDependencies": {},
|
||||
"maskedByOverrideDependencyMap": false
|
||||
},
|
||||
"entry": {
|
||||
"name": "entry",
|
||||
"dependencies": {
|
||||
"@xuqm/harmony-sdk": {
|
||||
"specifier": "file:xuqm-sdk",
|
||||
"version": "file:xuqm-sdk"
|
||||
}
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dynamicDependencies": {},
|
||||
"maskedByOverrideDependencyMap": false
|
||||
},
|
||||
"xuqm-sdk": {
|
||||
"name": "xuqm-sdk",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"dynamicDependencies": {},
|
||||
"maskedByOverrideDependencyMap": false
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"@xuqm/harmony-sdk@file:xuqm-sdk": {
|
||||
"storePath": "xuqm-sdk",
|
||||
"dependencies": {},
|
||||
"dynamicDependencies": {},
|
||||
"dev": false,
|
||||
"dynamic": false,
|
||||
"maskedByOverrideDependencyMap": false
|
||||
}
|
||||
}
|
||||
}
|
||||
17
xuqm-sdk/BuildProfile.ets
普通文件
17
xuqm-sdk/BuildProfile.ets
普通文件
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '0.1.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@ -15,6 +15,9 @@ export type {
|
||||
ChatType,
|
||||
MsgStatus,
|
||||
ConversationData,
|
||||
FriendRequest,
|
||||
GroupJoinRequest,
|
||||
ImGroup,
|
||||
HistoryQuery,
|
||||
PageResult,
|
||||
UserProfile,
|
||||
|
||||
@ -2,8 +2,6 @@ import common from '@ohos.app.ability.common'
|
||||
import type { SDKConfig } from './core/Types'
|
||||
import { SDKContext } from './core/SDKContext'
|
||||
import { ImClient } from './im/ImClient'
|
||||
import { PushSDK } from './push/PushSDK'
|
||||
import { UpdateSDK } from './update/UpdateSDK'
|
||||
|
||||
export class XuqmSDK {
|
||||
private static _imClient: ImClient | null = null
|
||||
@ -35,12 +33,4 @@ export class XuqmSDK {
|
||||
}
|
||||
return XuqmSDK._imClient
|
||||
}
|
||||
|
||||
static get push(): typeof PushSDK {
|
||||
return PushSDK
|
||||
}
|
||||
|
||||
static get update(): typeof UpdateSDK {
|
||||
return UpdateSDK
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,35 +1,30 @@
|
||||
import http from '@ohos.net.http'
|
||||
import type { ApiResponse } from './Types'
|
||||
import type { ApiResponse, HttpHeaders } from './Types'
|
||||
import { SDKContext } from './SDKContext'
|
||||
|
||||
export class HttpClient {
|
||||
static async request<T>(
|
||||
method: http.RequestMethod,
|
||||
path: string,
|
||||
body?: object,
|
||||
query?: Record<string, string | number | boolean | Date | null | undefined>,
|
||||
body?: Object,
|
||||
query?: string,
|
||||
): Promise<T> {
|
||||
const config = SDKContext.getConfig()
|
||||
const token = SDKContext.getToken()
|
||||
const queryPairs: string[] = []
|
||||
if (query) {
|
||||
for (const key of Object.keys(query)) {
|
||||
const value = query[key]
|
||||
if (value === undefined || value === null || value === '') continue
|
||||
queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value instanceof Date ? value.toISOString() : String(value))}`)
|
||||
}
|
||||
}
|
||||
const url = config.apiBaseUrl.replace(/\/$/, '') + path + (queryPairs.length > 0 ? `?${queryPairs.join('&')}` : '')
|
||||
const url = config.apiBaseUrl.replace(/\/$/, '') + path + (query ? '?' + query : '')
|
||||
|
||||
const client = http.createHttp()
|
||||
try {
|
||||
const header: HttpHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (token) {
|
||||
header.Authorization = 'Bearer ' + token
|
||||
}
|
||||
const options: http.HttpRequestOptions = {
|
||||
method,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
},
|
||||
extraData: body ? JSON.stringify(body) : undefined,
|
||||
header,
|
||||
extraData: body ? JSON.stringify(body) : '',
|
||||
connectTimeout: 15000,
|
||||
readTimeout: 15000,
|
||||
}
|
||||
@ -43,19 +38,19 @@ export class HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
static get<T>(path: string, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> {
|
||||
static get<T>(path: string, query?: string): Promise<T> {
|
||||
return HttpClient.request<T>(http.RequestMethod.GET, path, undefined, query)
|
||||
}
|
||||
|
||||
static post<T>(path: string, body?: object, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> {
|
||||
static post<T>(path: string, body?: Object, query?: string): Promise<T> {
|
||||
return HttpClient.request<T>(http.RequestMethod.POST, path, body, query)
|
||||
}
|
||||
|
||||
static put<T>(path: string, body?: object, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> {
|
||||
static put<T>(path: string, body?: Object, query?: string): Promise<T> {
|
||||
return HttpClient.request<T>(http.RequestMethod.PUT, path, body, query)
|
||||
}
|
||||
|
||||
static delete<T>(path: string, query?: Record<string, string | number | boolean | Date | null | undefined>): Promise<T> {
|
||||
static delete<T>(path: string, query?: string): Promise<T> {
|
||||
return HttpClient.request<T>(http.RequestMethod.DELETE, path, undefined, query)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,11 @@ export interface ApiResponse<T> {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface HttpHeaders {
|
||||
'Content-Type': string
|
||||
Authorization?: string
|
||||
}
|
||||
|
||||
export type MsgType =
|
||||
| 'TEXT'
|
||||
| 'IMAGE'
|
||||
@ -93,6 +98,40 @@ export interface UserProfile {
|
||||
createdAt?: number | null
|
||||
}
|
||||
|
||||
export interface ImGroup {
|
||||
id: string
|
||||
appId?: string
|
||||
name: string
|
||||
groupType?: string
|
||||
creatorId: string
|
||||
memberIds: string
|
||||
adminIds: string
|
||||
announcement?: string | null
|
||||
createdAt?: number | null
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
id: string
|
||||
appId?: string
|
||||
fromUserId: string
|
||||
toUserId: string
|
||||
remark?: string | null
|
||||
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
|
||||
createdAt: number
|
||||
reviewedAt?: number | null
|
||||
}
|
||||
|
||||
export interface GroupJoinRequest {
|
||||
id: string
|
||||
appId?: string
|
||||
groupId: string
|
||||
requesterId: string
|
||||
remark?: string | null
|
||||
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
|
||||
createdAt: number
|
||||
reviewedAt?: number | null
|
||||
}
|
||||
|
||||
export interface SendMessageParams {
|
||||
messageId?: string
|
||||
toId: string
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
import webSocket from '@ohos.net.webSocket'
|
||||
import { HttpClient } from '../core/HttpClient'
|
||||
import { SDKContext } from '../core/SDKContext'
|
||||
import type { ChatType, ConversationData, HistoryQuery, ImMessage, MsgType, PageResult, SendMessageParams, UserProfile } from '../core/Types'
|
||||
import type {
|
||||
ChatType,
|
||||
ConversationData,
|
||||
FriendRequest,
|
||||
GroupJoinRequest,
|
||||
HistoryQuery,
|
||||
ImGroup,
|
||||
ImMessage,
|
||||
MsgType,
|
||||
PageResult,
|
||||
SendMessageParams,
|
||||
UserProfile,
|
||||
} from '../core/Types'
|
||||
|
||||
export interface ImEventDelegate {
|
||||
onConnected?(): void
|
||||
@ -16,6 +28,89 @@ export interface RevokeData {
|
||||
operatorId: string
|
||||
}
|
||||
|
||||
class WebSocketFrame {
|
||||
type: string = ''
|
||||
payload: Object = new Object()
|
||||
}
|
||||
|
||||
class WebSocketEnvelope {
|
||||
destination: string = ''
|
||||
payload: Object = new Object()
|
||||
}
|
||||
|
||||
class AppBody {
|
||||
appId: string = ''
|
||||
}
|
||||
|
||||
class HistoryQueryParams {
|
||||
appId: string = ''
|
||||
page: number = 0
|
||||
size: number = 0
|
||||
msgType: MsgType = 'TEXT'
|
||||
keyword: string = ''
|
||||
startTime: string = ''
|
||||
endTime: string = ''
|
||||
}
|
||||
|
||||
class ConversationActionBody {
|
||||
appId: string = ''
|
||||
chatType: ChatType = 'SINGLE'
|
||||
}
|
||||
|
||||
class FriendRequestQueryBody {
|
||||
appId: string = ''
|
||||
direction: 'incoming' | 'outgoing' = 'incoming'
|
||||
}
|
||||
|
||||
class FriendRequestBody {
|
||||
appId: string = ''
|
||||
toUserId: string = ''
|
||||
remark: string = ''
|
||||
}
|
||||
|
||||
class GroupJoinRequestBody {
|
||||
appId: string = ''
|
||||
remark: string = ''
|
||||
}
|
||||
|
||||
class UpdateProfileBody {
|
||||
appId: string = ''
|
||||
nickname: string = ''
|
||||
avatar: string = ''
|
||||
gender: string = ''
|
||||
}
|
||||
|
||||
class DraftBody {
|
||||
appId: string = ''
|
||||
chatType: ChatType = 'SINGLE'
|
||||
draft: string = ''
|
||||
}
|
||||
|
||||
class PinBody {
|
||||
appId: string = ''
|
||||
chatType: ChatType = 'SINGLE'
|
||||
pinned: boolean = false
|
||||
}
|
||||
|
||||
class MuteBody {
|
||||
appId: string = ''
|
||||
chatType: ChatType = 'SINGLE'
|
||||
muted: boolean = false
|
||||
}
|
||||
|
||||
class SendEnvelopePayload {
|
||||
messageId: string = ''
|
||||
toId: string = ''
|
||||
chatType: ChatType = 'SINGLE'
|
||||
msgType: MsgType = 'TEXT'
|
||||
content: string = ''
|
||||
mentionedUserIds: string = ''
|
||||
}
|
||||
|
||||
class RevokeEnvelopePayload {
|
||||
msgId: string = ''
|
||||
}
|
||||
|
||||
const MAX_RECONNECT_DELAY = 30_000
|
||||
|
||||
export class ImClient {
|
||||
@ -29,7 +124,7 @@ export class ImClient {
|
||||
if (this.destroyed) return
|
||||
const config = SDKContext.getConfig()
|
||||
const token = SDKContext.getToken() ?? ''
|
||||
const url = `${config.imBaseUrl}/ws/im?token=${token}`
|
||||
const url = config.imBaseUrl.replace(/\/$/, '') + '/ws/im?token=' + encodeURIComponent(token)
|
||||
|
||||
this.ws = webSocket.createWebSocket()
|
||||
|
||||
@ -41,8 +136,10 @@ export class ImClient {
|
||||
|
||||
this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => {
|
||||
try {
|
||||
const text = typeof value === 'string' ? value : new TextDecoder().decode(value)
|
||||
const frame = JSON.parse(text) as { type: string; payload: unknown }
|
||||
if (typeof value !== 'string') {
|
||||
return
|
||||
}
|
||||
const frame = JSON.parse(value) as WebSocketFrame
|
||||
if (frame.type === 'MESSAGE') {
|
||||
this.delegate?.onMessage?.(this.normalizeMessage(frame.payload as ImMessage))
|
||||
} else if (frame.type === 'REVOKE') {
|
||||
@ -68,23 +165,27 @@ export class ImClient {
|
||||
send(params: SendMessageParams): ImMessage {
|
||||
const outgoing = this.buildOutgoingMessage(params)
|
||||
if (!this.ws) {
|
||||
return { ...outgoing, status: 'FAILED' }
|
||||
return this.markFailed(outgoing)
|
||||
}
|
||||
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
destination: '/app/chat.send',
|
||||
payload: {
|
||||
...params,
|
||||
messageId: outgoing.id,
|
||||
},
|
||||
}),
|
||||
(_err: Error) => {
|
||||
const payload = new SendEnvelopePayload()
|
||||
payload.messageId = outgoing.id
|
||||
payload.toId = params.toId
|
||||
payload.chatType = params.chatType
|
||||
payload.msgType = params.msgType
|
||||
payload.content = params.content
|
||||
if (params.mentionedUserIds !== undefined) {
|
||||
payload.mentionedUserIds = params.mentionedUserIds
|
||||
}
|
||||
const envelope = new WebSocketEnvelope()
|
||||
envelope.destination = '/app/chat.send'
|
||||
envelope.payload = payload
|
||||
|
||||
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
|
||||
if (_err) {
|
||||
this.delegate?.onError?.(_err.message)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
return outgoing
|
||||
}
|
||||
|
||||
@ -92,15 +193,14 @@ export class ImClient {
|
||||
if (!this.ws) {
|
||||
throw new Error('WebSocket not connected')
|
||||
}
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
destination: '/app/chat.revoke',
|
||||
payload: { msgId },
|
||||
}),
|
||||
(_err: Error) => {
|
||||
const payload = new RevokeEnvelopePayload()
|
||||
payload.msgId = msgId
|
||||
const envelope = new WebSocketEnvelope()
|
||||
envelope.destination = '/app/chat.revoke'
|
||||
envelope.payload = payload
|
||||
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
|
||||
if (_err) this.delegate?.onError?.(_err.message)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fetchHistory(
|
||||
@ -109,19 +209,8 @@ export class ImClient {
|
||||
size: number = 20,
|
||||
query: HistoryQuery = {},
|
||||
): Promise<PageResult<ImMessage>> {
|
||||
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/history/${encodeURIComponent(toId)}`, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
page,
|
||||
size,
|
||||
msgType: query.msgType,
|
||||
keyword: query.keyword,
|
||||
startTime: query.startTime instanceof Date
|
||||
? this.formatDateTime(query.startTime)
|
||||
: query.startTime,
|
||||
endTime: query.endTime instanceof Date
|
||||
? this.formatDateTime(query.endTime)
|
||||
: query.endTime,
|
||||
})
|
||||
const queryString = this.buildHistoryQuery(page, size, query)
|
||||
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/history/' + encodeURIComponent(toId), queryString)
|
||||
}
|
||||
|
||||
async fetchGroupHistory(
|
||||
@ -130,55 +219,109 @@ export class ImClient {
|
||||
size: number = 50,
|
||||
query: HistoryQuery = {},
|
||||
): Promise<PageResult<ImMessage>> {
|
||||
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/group-history/${encodeURIComponent(groupId)}`, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
page,
|
||||
size,
|
||||
msgType: query.msgType,
|
||||
keyword: query.keyword,
|
||||
startTime: query.startTime instanceof Date
|
||||
? this.formatDateTime(query.startTime)
|
||||
: query.startTime,
|
||||
endTime: query.endTime instanceof Date
|
||||
? this.formatDateTime(query.endTime)
|
||||
: query.endTime,
|
||||
})
|
||||
const queryString = this.buildHistoryQuery(page, size, query)
|
||||
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString)
|
||||
}
|
||||
|
||||
async listConversations(size: number = 20): Promise<ConversationData[]> {
|
||||
return HttpClient.get<ConversationData[]>('/api/im/conversations', {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
page: 0,
|
||||
size,
|
||||
})
|
||||
return HttpClient.get<ConversationData[]>('/api/im/conversations', this.buildConversationQuery(size))
|
||||
}
|
||||
|
||||
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
|
||||
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, undefined, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
chatType,
|
||||
})
|
||||
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType))
|
||||
}
|
||||
|
||||
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
|
||||
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, undefined, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
chatType,
|
||||
draft,
|
||||
})
|
||||
const params = new DraftBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
params.chatType = chatType
|
||||
params.draft = draft
|
||||
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/draft', params)
|
||||
}
|
||||
|
||||
async setConversationPinned(targetId: string, chatType: ChatType, pinned: boolean): Promise<void> {
|
||||
const params = new PinBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
params.chatType = chatType
|
||||
params.pinned = pinned
|
||||
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/pinned', params)
|
||||
}
|
||||
|
||||
async setConversationMuted(targetId: string, chatType: ChatType, muted: boolean): Promise<void> {
|
||||
const params = new MuteBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
params.chatType = chatType
|
||||
params.muted = muted
|
||||
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/muted', params)
|
||||
}
|
||||
|
||||
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
|
||||
await HttpClient.delete<void>(`/api/im/conversations/${encodeURIComponent(targetId)}`, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
chatType,
|
||||
})
|
||||
await HttpClient.delete<void>('/api/im/conversations/' + encodeURIComponent(targetId), this.buildConversationActionQuery(chatType))
|
||||
}
|
||||
|
||||
async listFriends(): Promise<string[]> {
|
||||
return HttpClient.get<string[]>('/api/im/friends', this.buildAppQuery())
|
||||
}
|
||||
|
||||
async listGroups(): Promise<ImGroup[]> {
|
||||
return HttpClient.get<ImGroup[]>('/api/im/groups', this.buildAppQuery())
|
||||
}
|
||||
|
||||
async getGroupInfo(groupId: string): Promise<ImGroup> {
|
||||
return HttpClient.get<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery())
|
||||
}
|
||||
|
||||
async sendFriendRequest(toUserId: string, remark: string | null = null): Promise<FriendRequest> {
|
||||
const params = new FriendRequestBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
params.toUserId = toUserId
|
||||
if (remark !== null && remark !== '') {
|
||||
params.remark = remark
|
||||
}
|
||||
return HttpClient.post<FriendRequest>('/api/im/friend-requests', params)
|
||||
}
|
||||
|
||||
async listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise<FriendRequest[]> {
|
||||
return HttpClient.get<FriendRequest[]>('/api/im/friend-requests', this.buildFriendRequestQuery(direction))
|
||||
}
|
||||
|
||||
async acceptFriendRequest(requestId: string): Promise<FriendRequest> {
|
||||
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/accept', this.buildAppBody())
|
||||
}
|
||||
|
||||
async rejectFriendRequest(requestId: string): Promise<FriendRequest> {
|
||||
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/reject', this.buildAppBody())
|
||||
}
|
||||
|
||||
async sendGroupJoinRequest(groupId: string, remark: string | null = null): Promise<GroupJoinRequest> {
|
||||
const params = new GroupJoinRequestBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
if (remark !== null && remark !== '') {
|
||||
params.remark = remark
|
||||
}
|
||||
return HttpClient.post<GroupJoinRequest>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', params)
|
||||
}
|
||||
|
||||
async listGroupJoinRequests(groupId: string): Promise<GroupJoinRequest[]> {
|
||||
return HttpClient.get<GroupJoinRequest[]>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', this.buildAppQuery())
|
||||
}
|
||||
|
||||
async acceptGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
|
||||
return HttpClient.post<GroupJoinRequest>(
|
||||
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/accept',
|
||||
this.buildAppBody(),
|
||||
)
|
||||
}
|
||||
|
||||
async rejectGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
|
||||
return HttpClient.post<GroupJoinRequest>(
|
||||
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/reject',
|
||||
this.buildAppBody(),
|
||||
)
|
||||
}
|
||||
|
||||
async getProfile(userId: string): Promise<UserProfile> {
|
||||
return HttpClient.get<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
})
|
||||
return HttpClient.get<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery())
|
||||
}
|
||||
|
||||
async updateProfile(
|
||||
@ -187,12 +330,62 @@ export class ImClient {
|
||||
avatar: string | null = null,
|
||||
gender: string | null = null,
|
||||
): Promise<UserProfile> {
|
||||
return HttpClient.put<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, undefined, {
|
||||
appId: SDKContext.getConfig().appKey,
|
||||
...(nickname !== null ? { nickname } : {}),
|
||||
...(avatar !== null ? { avatar } : {}),
|
||||
...(gender !== null ? { gender } : {}),
|
||||
})
|
||||
const params = new UpdateProfileBody()
|
||||
params.appId = SDKContext.getConfig().appKey
|
||||
if (nickname !== null) {
|
||||
params.nickname = nickname
|
||||
}
|
||||
if (avatar !== null) {
|
||||
params.avatar = avatar
|
||||
}
|
||||
if (gender !== null) {
|
||||
params.gender = gender
|
||||
}
|
||||
return HttpClient.put<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), params)
|
||||
}
|
||||
|
||||
private buildAppBody(): AppBody {
|
||||
const body = new AppBody()
|
||||
body.appId = SDKContext.getConfig().appKey
|
||||
return body
|
||||
}
|
||||
|
||||
private buildAppQuery(): string {
|
||||
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey)
|
||||
}
|
||||
|
||||
private buildConversationActionQuery(chatType: ChatType): string {
|
||||
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&chatType=' + encodeURIComponent(chatType)
|
||||
}
|
||||
|
||||
private buildConversationQuery(size: number): string {
|
||||
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&page=0&size=' + encodeURIComponent(size)
|
||||
}
|
||||
|
||||
private buildFriendRequestQuery(direction: 'incoming' | 'outgoing'): string {
|
||||
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&direction=' + encodeURIComponent(direction)
|
||||
}
|
||||
|
||||
private buildHistoryQuery(page: number, size: number, query: HistoryQuery): string {
|
||||
const parts: string[] = []
|
||||
parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey))
|
||||
parts.push('page=' + encodeURIComponent(page))
|
||||
parts.push('size=' + encodeURIComponent(size))
|
||||
if (query.msgType !== undefined) {
|
||||
parts.push('msgType=' + encodeURIComponent(query.msgType))
|
||||
}
|
||||
if (query.keyword !== undefined && query.keyword !== '') {
|
||||
parts.push('keyword=' + encodeURIComponent(query.keyword))
|
||||
}
|
||||
if (query.startTime !== undefined) {
|
||||
const startValue = query.startTime instanceof Date ? this.formatDateTime(query.startTime) : String(query.startTime)
|
||||
parts.push('startTime=' + encodeURIComponent(startValue))
|
||||
}
|
||||
if (query.endTime !== undefined) {
|
||||
const endValue = query.endTime instanceof Date ? this.formatDateTime(query.endTime) : String(query.endTime)
|
||||
parts.push('endTime=' + encodeURIComponent(endValue))
|
||||
}
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
@ -208,7 +401,7 @@ export class ImClient {
|
||||
private scheduleReconnect(): void {
|
||||
if (this.destroyed) return
|
||||
const delay = this.reconnectDelay
|
||||
if (SDKContext.getConfig().debug) console.log(`[ImClient] reconnect in ${delay}ms`)
|
||||
if (SDKContext.getConfig().debug) console.log('[ImClient] reconnect in ' + delay + 'ms')
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect()
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
||||
@ -219,7 +412,7 @@ export class ImClient {
|
||||
const messageId = params.messageId ?? this.generateMessageId()
|
||||
const userId = SDKContext.getUserId() ?? ''
|
||||
const appId = SDKContext.getConfig().appKey
|
||||
return {
|
||||
const message: ImMessage = {
|
||||
id: messageId,
|
||||
appId,
|
||||
fromUserId: userId,
|
||||
@ -234,25 +427,55 @@ export class ImClient {
|
||||
revoked: false,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
private markFailed(message: ImMessage): ImMessage {
|
||||
const failed: ImMessage = {
|
||||
id: message.id,
|
||||
appId: message.appId,
|
||||
fromUserId: message.fromUserId,
|
||||
fromId: message.fromId,
|
||||
toId: message.toId,
|
||||
chatType: message.chatType,
|
||||
msgType: message.msgType,
|
||||
content: message.content,
|
||||
status: 'FAILED',
|
||||
mentionedUserIds: message.mentionedUserIds,
|
||||
groupReadCount: message.groupReadCount,
|
||||
revoked: message.revoked,
|
||||
createdAt: message.createdAt,
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
private normalizeMessage(message: ImMessage): ImMessage {
|
||||
return {
|
||||
...message,
|
||||
fromId: message.fromId ?? message.fromUserId,
|
||||
revoked: message.revoked ?? message.status === 'REVOKED',
|
||||
appId: message.appId ?? SDKContext.getConfig().appKey,
|
||||
const normalized: ImMessage = {
|
||||
id: message.id,
|
||||
appId: message.appId || SDKContext.getConfig().appKey,
|
||||
fromUserId: message.fromUserId,
|
||||
fromId: message.fromId || message.fromUserId,
|
||||
toId: message.toId,
|
||||
chatType: message.chatType,
|
||||
msgType: message.msgType,
|
||||
content: message.content,
|
||||
status: message.status,
|
||||
mentionedUserIds: message.mentionedUserIds,
|
||||
groupReadCount: message.groupReadCount,
|
||||
revoked: message.revoked || message.status === 'REVOKED',
|
||||
createdAt: message.createdAt,
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
private generateMessageId(): string {
|
||||
const cryptoId = globalThis.crypto?.randomUUID?.()
|
||||
if (cryptoId) return cryptoId
|
||||
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16)
|
||||
}
|
||||
|
||||
private formatDateTime(value: Date): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const pad = (n: number): string => {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
return [
|
||||
value.getFullYear(),
|
||||
'-',
|
||||
@ -267,4 +490,14 @@ export class ImClient {
|
||||
pad(value.getSeconds()),
|
||||
].join('')
|
||||
}
|
||||
|
||||
private toStringValue(value: Date | string | number | undefined): string | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return this.formatDateTime(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import { HttpClient } from '../core/HttpClient'
|
||||
import type { PushTokenInfo } from '../core/Types'
|
||||
|
||||
class PushRegisterBody {
|
||||
vendor: string = 'HARMONY'
|
||||
token: string = ''
|
||||
platform: string = 'harmony'
|
||||
imUserId: string | null = null
|
||||
}
|
||||
|
||||
class PushUnregisterBody {
|
||||
vendor: string = 'HARMONY'
|
||||
token: string = ''
|
||||
platform: string = 'harmony'
|
||||
}
|
||||
|
||||
export class PushSDK {
|
||||
/**
|
||||
@ -8,21 +20,20 @@ export class PushSDK {
|
||||
* vendor should be 'HARMONY' for HarmonyOS devices.
|
||||
*/
|
||||
static async registerToken(token: string, imUserId?: string): Promise<void> {
|
||||
const body: PushTokenInfo = {
|
||||
vendor: 'HARMONY',
|
||||
token,
|
||||
platform: 'harmony',
|
||||
const body = new PushRegisterBody()
|
||||
body.token = token
|
||||
if (imUserId !== undefined) {
|
||||
body.imUserId = imUserId
|
||||
}
|
||||
await HttpClient.post<void>('/api/v1/push/register', {
|
||||
...body,
|
||||
imUserId: imUserId ?? null,
|
||||
})
|
||||
await HttpClient.post<void>('/api/v1/push/register', body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister push token on logout.
|
||||
*/
|
||||
static async unregisterToken(token: string): Promise<void> {
|
||||
await HttpClient.post<void>('/api/v1/push/unregister', { token, platform: 'harmony' })
|
||||
const body = new PushUnregisterBody()
|
||||
body.token = token
|
||||
await HttpClient.post<void>('/api/v1/push/unregister', body)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "xuqm-sdk",
|
||||
"name": "xuqmSdk",
|
||||
"type": "har",
|
||||
"deviceTypes": ["phone", "tablet"]
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户