更新云信相关代码

This commit is contained in:
XiuYun CHEN 2025-07-10 08:57:32 +08:00
parent a297e0452f
commit 5954f51701
523 changed files with 61549 additions and 1491 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,15 @@
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "/Users/gandanxiangzhao/.ohos/config/default_harmony_0p9qf45UN48IqoSyKhLtcLhTMwMj81GJRnBkhg7Axm4=.cer",
"keyAlias": "debugKey",
"keyPassword": "0000001AA50378305356D7D7EEDE463DB4E4B1B23C35A2615C29ED413C2E2DC30778DFE346C51C100F7D",
"profile": "/Users/gandanxiangzhao/.ohos/config/default_harmony_0p9qf45UN48IqoSyKhLtcLhTMwMj81GJRnBkhg7Axm4=.p7b",
"storeFile": "D:/202076work/gdxzExport.p12",
"storePassword": "00000018C50E504AB8CC43CCB52E64D656B0131573F764452F4F1FED54B55C3ECC0010D2BD946612",
"keyAlias": "gdxz",
"keyPassword": "00000018EB40FA6E3F3E03EBCA9DCC01DFAB5A57692D48BC16DDF2182CD2CD08E1E583F524397C53",
"signAlg": "SHA256withECDSA",
"storeFile": "/Users/gandanxiangzhao/.ohos/config/default_harmony_0p9qf45UN48IqoSyKhLtcLhTMwMj81GJRnBkhg7Axm4=.p12",
"storePassword": "0000001A29D969B0C08E79F7AC25E39AEB74F09540DED4C7BEF6ABF0671F85B598C8F911C2943D3B741C"
"profile": "D:/202076work/profile测试Debug.p7b",
"certpath": "D:/202076work/鸿蒙专家端测试证书.cer"
// "profile": "D:/202076work/release鸿蒙Release.p7b",
// "certpath": "D:/202076work/鸿蒙专家端发布证书.cer"
}
}
],
@ -30,9 +32,9 @@
}
],
"buildModeSet": [
{
"name": "debug"
},
// {
// "name": "debug"
// },
{
"name": "release"
}
@ -74,7 +76,35 @@
{
"name": "scene_single_video",
"srcPath": "./scene_single_video"
}
},
{
"name": "corekit",
"srcPath": "./corekit"
},
{
"name": "chatkit",
"srcPath": "./chatkit"
},
{
"name": "chatkit_ui",
"srcPath": "./chatkit_ui"
},
{
"name": "netease",
"srcPath": "./features/netease"
},
{
"name": "conversationkit_ui",
"srcPath": "./conversationkit_ui"
},
{
"name": "common",
"srcPath": "./common"
},
{
"name": "localconversationkit_ui",
"srcPath": "./localconversationkit_ui"
}
]
}

6
chatkit/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test

17
chatkit/BuildProfile.ets Normal file
View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '10.1.0';
export const BUILD_MODE_NAME = 'release';
export const DEBUG = false;
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;
}

78
chatkit/Index.ets Normal file
View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export { ChatKitClient, currentConversationChanged } from './src/main/ets/ChatKitClient'
export { ChatRepo } from './src/main/ets/repo/ChatRepo'
export { ContactRepo } from './src/main/ets/repo/ContactRepo'
export { TeamRepo } from './src/main/ets/repo/TeamRepo'
export { SettingRepo } from './src/main/ets/repo/SettingRepo'
export { StorageRepo } from './src/main/ets/repo/StorageRepo'
export { NEUserWithFriend } from './src/main/ets/model/NEUserWithFriend'
export { NEFriendUserCache } from './src/main/ets/NEFriendUserCache'
export { ConversationRepo } from './src/main/ets/repo/ConversationRepo'
export { LocalConversationRepo } from './src/main/ets/repo/LocalConversationRepo'
export { IsDiscussion } from './src/main/ets/utils/Utils'
export { ErrorUtils } from './src/main/ets/utils/ErrorUtils'
export { TeamMemberWithUser, TeamMemberResult } from './src/main/ets/model/TeamMemberWithUser'
export { PersonSelectParam } from './src/main/ets/model/PersonSelectParam'
export { ConversationSelectParam } from './src/main/ets/model/ConversationSelectParam'
export { ConversationSelectedParam } from './src/main/ets/model/ConversationSelectedParam'
export { CustomMessageUtils } from './src/main/ets/utils/CustomMessageUtils'
export { MergedMessageAttachment,
MergeMessageAbstract,
MessageUploadInfo } from './src/main/ets/model/CustomMessageAttachment'
export { mergedMessageNickKey,
mergedMessageAvatarKey,
multiForwardFileName,
mergedMessageMaxDepth,
mergedMessageLimitCount,
singleMessageLimitCount,
deleteMessagesLimitCount,
mergedMessageCustomType,
mergedMessageCellHeight,
conversationSelectLimitCount,
keyExtensionLastOptType,
keyExtensionAtAll,
typeExtensionAllowAll,
typeExtensionAllowManager,
collectionTypeOffset,
keyReplyMsgKey
} from './src/main/ets/constant/Constant'
export { TeamMemberCache } from './src/main/ets/cache/TeamMemberCache'
export { AitModel, accountAll, aitKey, getAitModelFromJson, YxAitMsg } from './src/main/ets/model/ait/AitModel'
export { AitMessage } from './src/main/ets/model/ait/AitMessage'
export { AitSegment } from './src/main/ets/model/ait/AitSegment'
export { AitAllPermission } from './src/main/ets/model/ait/AitAllPermission'
export { KitLogger as Logger } from './src/main/ets/logger/AppLogger'
export { IMKitConfigCenter } from './src/main/ets/IMKitConfigCenter'
export { TeamSettingParam } from './src/main/ets/model/TeamSettingParam'

View File

@ -0,0 +1,31 @@
{
"apiType": "stageMode",
"buildOption": {
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
},
"consumerFiles": [
"./consumer-rules.txt"
]
}
},
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest"
}
]
}

View File

13
chatkit/hvigorfile.ts Normal file
View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}

View File

@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation

View File

@ -0,0 +1,132 @@
{
"meta": {
"stableOrder": true
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@nimkit/corekit@../corekit": "@nimkit/corekit@../corekit",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+conversation@10.9.10/oh_modules/@nimsdk/conversation/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+friend@10.9.10/oh_modules/@nimsdk/friend/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+message@10.9.10/oh_modules/@nimsdk/message/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+team@10.9.10/oh_modules/@nimsdk/team/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+user@10.9.10/oh_modules/@nimsdk/user/libs/base.har": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/base@10.9.10": "@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"@nimsdk/conversation@10.9.10": "@nimsdk/conversation@10.9.10",
"@nimsdk/friend@10.9.10": "@nimsdk/friend@10.9.10",
"@nimsdk/message@10.9.10": "@nimsdk/message@10.9.10",
"@nimsdk/nim@10.9.10": "@nimsdk/nim@10.9.10",
"@nimsdk/team@10.9.10": "@nimsdk/team@10.9.10",
"@nimsdk/user@10.9.10": "@nimsdk/user@10.9.10",
"@nimsdk/vendor@1.0.0": "@nimsdk/vendor@1.0.0",
"class-transformer@^0.5.1": "class-transformer@0.5.1",
"reflect-metadata@^0.1.13": "reflect-metadata@0.2.1"
},
"packages": {
"@nimkit/corekit@../corekit": {
"name": "@nimkit/corekit",
"version": "1.1.0",
"resolved": "../corekit",
"registryType": "local"
},
"@nimsdk/base@../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har": {
"name": "@nimsdk/base",
"version": "10.9.10",
"resolved": "../oh_modules/.ohpm/@nimsdk+nim@10.9.10/oh_modules/@nimsdk/nim/libs/base.har",
"registryType": "local",
"dependencies": {
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/conversation@10.9.10": {
"name": "@nimsdk/conversation",
"version": "10.9.10",
"integrity": "sha512-1HLvs19/GJAHeIOCN0OiKlowkg6dzZwvZK0Jqu7tAcYGcLl4+G/Z3pwsGHhv+E2Tzs8FHZCqbESMgSh+LNyt/g==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/conversation/-/conversation-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/friend@10.9.10": {
"name": "@nimsdk/friend",
"version": "10.9.10",
"integrity": "sha512-JVACpT8xqLLaN8D26YHmwfsS1dHFQvBnP3Jyk9El89P2trn/2ZFLvnQjxzyBDsqJRUtNFfIrN+TK7Idmud4ACQ==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/friend/-/friend-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/message@10.9.10": {
"name": "@nimsdk/message",
"version": "10.9.10",
"integrity": "sha512-f59rWiM4SjhhxNftRUt9vg7lIwkGycV/aL8J3omH+Te4SMbUGolwDGErDr7adtZ3tDUThtxxgU8n5tD28TBRtA==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/message/-/message-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/nim@10.9.10": {
"name": "@nimsdk/nim",
"version": "10.9.10",
"integrity": "sha512-WpT8vBTld92ExtH30Ffsm+xq6BW6/UFj8SuhJrcQaZY3AYf9sg+d+euqx/dFzjZin5cWRxd/yoodBiVcGfsM4w==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/nim/-/nim-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/team@10.9.10": {
"name": "@nimsdk/team",
"version": "10.9.10",
"integrity": "sha512-T4YSN395VXQr1TDX2B24DmGYuvUgUqE7wndbleR980wEyki9IfhC2VxxJ1yajhxVlVkfmuBjCB/eKWL0zLzu5A==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/team/-/team-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/user@10.9.10": {
"name": "@nimsdk/user",
"version": "10.9.10",
"integrity": "sha512-KyWVDDPbymj3qoC8Y0mB8umgvLg89Y2cB02tM35oSG8IW95C936v5ogip2Jk7qAfabXxI/XTyy5wQoW1z950JA==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/user/-/user-10.9.10.har",
"registryType": "ohpm",
"dependencies": {
"@nimsdk/base": "file:./libs/base.har",
"@nimsdk/vendor": "1.0.0"
}
},
"@nimsdk/vendor@1.0.0": {
"name": "@nimsdk/vendor",
"version": "1.0.0",
"integrity": "sha512-q49MJM6PfucNs8jvLP56a2etyqRfZCeJaMa1BT9vO4sIgwt15bin+hpUWZ1qkflBs9YkDb2nMIX5O8zt556muw==",
"resolved": "https://repo.harmonyos.com/ohpm/@nimsdk/vendor/-/vendor-1.0.0.har",
"registryType": "ohpm"
},
"class-transformer@0.5.1": {
"name": "class-transformer",
"version": "0.5.1",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
"resolved": "https://repo.harmonyos.com/ohpm/class-transformer/-/class-transformer-0.5.1.tgz",
"shasum": "24147d5dffd2a6cea930a3250a677addf96ab336",
"registryType": "ohpm"
},
"reflect-metadata@0.2.1": {
"name": "reflect-metadata",
"version": "0.2.1",
"integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==",
"resolved": "https://repo.harmonyos.com/ohpm/reflect-metadata/-/reflect-metadata-0.2.1.tgz",
"shasum": "8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74",
"registryType": "ohpm"
}
}
}

21
chatkit/oh-package.json5 Normal file
View File

@ -0,0 +1,21 @@
{
"name": "@nimkit/chatkit",
"version": "10.1.0",
"description": "Please describe the basic information.",
"main": "Index.ets",
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@nimsdk/conversation": "10.9.10",
"@nimsdk/message": "10.9.10",
"@nimsdk/team": "10.9.10",
"@nimsdk/user": "10.9.10",
"@nimsdk/friend": "10.9.10",
"@nimsdk/nim": "10.9.10",
"@nimsdk/base": "10.9.10",
"@nimkit/corekit": "file:../corekit",
"class-transformer": "^0.5.1",
"reflect-metadata": "^0.1.13",
// 用于嵌套对象@Type反射
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { CoreKitClient } from '@nimkit/corekit';
import {
IM_SDK_VERSION,
NIM,
NIMInterface,
V2NIMConnectStatus,
V2NIMDataSyncState,
V2NIMDataSyncType,
V2NIMEnableServiceType,
V2NIMError,
V2NIMLoginStatus,
V2NIMMessageRevokeNotification
} from '@nimsdk/base';
import { ContactRepo } from '../../../Index';
import { KitLogger } from './logger/AppLogger';
import { LoggerKitImpl } from './logger/LoggerKitImpl';
import { ChatRepo } from './repo/ChatRepo';
import { saveLocalRevokeMessageFormOther } from './utils/MessageUtils';
export const currentConversationChanged: string = 'CurrentConversationChanged'
export class ChatKitClient {
declare static nim: NIMInterface
static haveSyncedConversation: boolean = false
//是否主动离开群
static selfLeaveTeam = false
static hasInitListener = false
static currentConversationId: string = ''
static networkAvailable: boolean = true
static logger: KitLogger | undefined = undefined
static init(nimSdk: NIMInterface, appKey: string, disableLog?: boolean) {
ChatKitClient.nim = nimSdk
if (disableLog !== true) {
if (nimSdk instanceof NIM) {
let logger = new LoggerKitImpl(nimSdk.context.cacheDir)
ChatKitClient.logger = new KitLogger(logger)
}
}
ChatKitClient.haveSyncedConversation = false
ChatKitClient.initListener()
CoreKitClient.init({
appKey: appKey,
imVersion: IM_SDK_VERSION,
})
}
/**
* 长连接是否断开
* 可用于判断网络是否断开
*/
static connectBroken() {
return ChatKitClient.nim.loginService.getConnectStatus() !== V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
}
/**
* 设置当前会话id
* @param conversationId 当前会话id
*/
static setCurrentConversationId(conversationId: string) {
ChatKitClient.currentConversationId = conversationId
getContext().eventHub.emit(currentConversationChanged, conversationId)
}
/**
* 清除当前会话id
*/
static clearCurrentConversationId() {
ChatKitClient.currentConversationId = ''
getContext().eventHub.emit(currentConversationChanged, '')
}
/**
* 获取当前会话id
* @returns 当前会话id
*/
static getCurrentConversationId(): string {
return ChatKitClient.currentConversationId
}
/**
* 是否登录
* @returns
*/
static hasLogin(): boolean {
return ChatKitClient.nim != null && ChatKitClient.nim.loginService.getLoginUser() != null
}
static getLoginUserId(): string {
return ChatKitClient.nim.loginService.getLoginUser()
}
/**
* IM 主数据是否同步完成
* @returns
*/
static isMainDataSynced(): boolean {
let dataSync = ChatKitClient.nim.loginService.getDataSync()
if (dataSync != null) {
for (const item of dataSync) {
if (item.type === V2NIMDataSyncType.V2NIM_DATA_SYNC_TYPE_MAIN &&
item.state === V2NIMDataSyncState.V2NIM_DATA_SYNC_STATE_COMPLETED) {
return true
}
}
}
return false
}
static isLocalConversation(): boolean {
return ChatKitClient.nim.isServiceEnable(V2NIMEnableServiceType.LOCAL_CONVERSATION)
}
/**
* 等待登录后执行
*/
static runAfterLoggedIn(fn: Function) {
if (ChatKitClient.nim.loginService?.getLoginStatus() === V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED) {
fn()
} else {
const onLoginStatusFunc = (status: V2NIMLoginStatus) => {
if (status === V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED) {
fn()
ChatKitClient.nim.loginService.off('onLoginStatus', onLoginStatusFunc)
}
}
ChatKitClient.nim.loginService.on('onLoginStatus', onLoginStatusFunc)
}
}
/*
* 销毁 清理监听
*/
static onDestroy() {
ChatKitClient.hasInitListener = false
ChatKitClient.haveSyncedConversation = false
ChatKitClient.selfLeaveTeam = false
ChatKitClient.currentConversationId = ''
ChatRepo.offRevokeMessage(ChatKitClient.onRevokeFun)
ChatKitClient.nim.conversationService?.off('onSyncFinished', ChatKitClient.onSyncFinishedFun)
ChatKitClient.nim.loginService.off('onDataSync', ChatKitClient.dataSyncFun)
}
/**
* 初始化监听
*/
private static initListener() {
if (ChatKitClient.hasInitListener) {
return
}
console.info("netease ChatKitClient initListener ");
// 数据同步监听
ChatKitClient.nim.conversationService?.on('onSyncFinished', ChatKitClient.onSyncFinishedFun)
ChatKitClient.nim.loginService.on('onDataSync', ChatKitClient.dataSyncFun)
// 消息撤回监听,消息撤回后,会收到通知
ChatRepo.onRevokeMessage(ChatKitClient.onRevokeFun)
ChatKitClient.hasInitListener = true
}
private static onSyncFinishedFun = () => {
console.debug(`Performance Test onSyncFinishedFun`)
ChatKitClient.haveSyncedConversation = true
}
private static dataSyncFun = (type: V2NIMDataSyncType, state: V2NIMDataSyncState, error?: V2NIMError) => {
if (state === V2NIMDataSyncState.V2NIM_DATA_SYNC_STATE_COMPLETED) {
ContactRepo.getFriendList()
}
}
private static onRevokeFun = (messages: V2NIMMessageRevokeNotification[]) => {
messages.forEach((msg, index, messages) => {
if (msg.messageRefer.conversationId !== ChatKitClient.currentConversationId) {
saveLocalRevokeMessageFormOther(msg.messageRefer.conversationId, msg, false)
}
})
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
@ObservedV2
export class IMKitConfigCenter {
// 是否使用本地会话列表
@Trace static enableLocalConversation: boolean = true;
}

View File

@ -0,0 +1,194 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatKitClient } from './ChatKitClient';
import { NEUserWithFriend } from './model/NEUserWithFriend';
import { V2NIMFriend, V2NIMUser } from '@nimsdk/base';
import { ContactRepo } from './repo/ContactRepo';
interface updateFriendInfoParams {
user?: V2NIMUser;
friend?: V2NIMFriend;
friendUser?: NEUserWithFriend;
}
/// 好友信息缓存,只缓存好友
@ObservedV2
export class NEFriendUserCache {
@Trace public static mineUserCache?: NEUserWithFriend
// 黑名单账号集合
public blockAccountList: string[] = []
// 好友列表,包括黑名单中好友
public friendCache: Map<string, NEUserWithFriend> = new Map<string, NEUserWithFriend>()
private constructor() {
ContactRepo.addListener()
ContactRepo.getFriendList()
ContactRepo.getUserListFromCloud([ChatKitClient.getLoginUserId()])
ContactRepo.getBlockList()
}
public static getInstance() {
if (!AppStorage.get<NEFriendUserCache>(NEFriendUserCache.name)) {
let instance = new NEFriendUserCache()
AppStorage.setOrCreate(NEFriendUserCache.name, instance);
}
return AppStorage.get<NEFriendUserCache>(NEFriendUserCache.name) as NEFriendUserCache;
}
/// 是否是自己
public static isMe(accountId: string): Boolean {
return NEFriendUserCache.mineUserCache?.user?.accountId === accountId
}
/// 好友缓存是否为空
public isEmpty(): Boolean {
if (this.friendCache == undefined || this.friendCache.size === 0) {
return true
}
return false
}
/// 是否是好友
public isFriend(accountId: string): Boolean {
return this.friendCache.has(accountId) && !this.blockAccountList.includes(accountId)
}
/// 添加(更新)好友信息
public updateFriendInfo(params: updateFriendInfoParams) {
let accountId = ""
if (params.user) {
accountId = params.user.accountId
let friendUser = this.friendCache.get(accountId)
if (friendUser) {
friendUser.user = params.user
} else {
this.friendCache.set(accountId, new NEUserWithFriend({
user: params.user
}))
}
}
if (params.friend) {
accountId = params.friend.accountId
let friendUser = this.friendCache.get(accountId)
if (friendUser) {
friendUser.friend = params.friend
friendUser.user = params.friend.userProfile
} else {
this.friendCache.set(accountId, new NEUserWithFriend({
friend: params.friend
}))
}
}
if (params.friendUser && params.friendUser.user?.accountId) {
accountId = params.friendUser.user.accountId
this.friendCache.set(accountId, params.friendUser)
}
}
/// 使用好友列表初始化缓存
public loadFriendList(friends: V2NIMFriend[]) {
friends.forEach((friend: V2NIMFriend) => {
this.updateFriendInfo({
friend: friend
})
})
ContactRepo.listener.emit('loadFriendCache')
}
/// 获取缓存的好友信息
public getFriendById(accountId: string) {
return this.friendCache.get(accountId)
}
/**
* 通过用户ID列表获取用户信息
* @param accIds
* @returns
*/
public getFriendsByIds(accIds: string[]) {
let result: NEUserWithFriend[] = []
accIds.forEach((accId) => {
let user = this.friendCache.get(accId)
if (user) {
result.push(user)
}
})
return result
}
/// 获取缓存的好友信息列表,包含黑名单中的好友
public getFriendList() {
return Array.from(this.friendCache.values())
}
/// 获取缓存的好友信息列表,不包含黑名单中的好友
public getFriendListNotInBlocklist() {
let friends: Map<string, NEUserWithFriend> = new Map<string, NEUserWithFriend>()
this.friendCache.forEach((value, key) => {
if (!this.blockAccountList.includes(key)) {
friends.set(key, value)
}
})
return Array.from(friends.values())
}
/// 获取缓存的黑名单列表
public getBlocklist(): string[] {
return this.blockAccountList
}
/// 删除好友信息缓存
public removeFriendInfo(accountId: string) {
this.friendCache.delete(accountId)
ContactRepo.listener.emit('removeFriendInfo', accountId)
}
/// 删除所有好友信息缓存
public removeAllFriendInfo() {
this.friendCache.clear()
this.blockAccountList = []
ContactRepo.removeListener()
AppStorage.delete(NEFriendUserCache.name)
}
/// 初始化黑名单
public initBlockAccountSet(blockList: string[]) {
this.blockAccountList = blockList
}
/// 是否是黑名单账号
public isBlockAccount(accountId: string) {
return this.blockAccountList.includes(accountId)
}
/// 更新黑名单
public addBlockAccount(accountId: string) {
if (!this.blockAccountList.includes(accountId)) {
this.blockAccountList.push(accountId)
ContactRepo.listener.emit('addBlockAccount', accountId)
}
}
/// 移除黑名单账号
public removeBlockAccount(accountId: string) {
if (this.blockAccountList.includes(accountId)) {
for (let index = 0; index < this.blockAccountList.length; index++) {
if (this.blockAccountList[index] === accountId) {
this.blockAccountList.splice(index, 1)
ContactRepo.listener.emit('removeBlockAccount', accountId)
break
}
}
}
}
}

View File

@ -0,0 +1,379 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMError,
V2NIMFriend,
V2NIMQueryDirection,
V2NIMTeam,
V2NIMTeamMember,
V2NIMTeamMemberRole,
V2NIMTeamType,
V2NIMUser
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
import { TeamMemberWithUser } from '../model/TeamMemberWithUser';
import { TeamRepo } from '../repo/TeamRepo';
@ObservedV2
export class TeamMemberCache {
static logTag = 'TeamMemberCache'
//全成员列表
@Trace membersMap: Map<string, TeamMemberWithUser> = new Map()
//自己的群身份
@Trace mineTeamMember: V2NIMTeamMember | undefined = undefined
//群组Id
teamId: string = ''
//群组
team?: V2NIMTeam = undefined
//下次拉去的token
nextToken = ''
//是否拉完了
isFinish = false
private static instance: TeamMemberCache
private constructor() {
}
public static getInstance(): TeamMemberCache {
if (!TeamMemberCache.instance) {
TeamMemberCache.instance = new TeamMemberCache()
}
return TeamMemberCache.instance
}
/**
* 初始化信息
* @param teamId
* @param team
*/
intTeamInfo(teamId: string, team?: V2NIMTeam) {
if (this.teamId !== teamId) {
this.membersMap.clear()
this.mineTeamMember = undefined
this.teamId = teamId
this.team = team
this.isFinish = false
this.initMemberChangeListener()
}
}
/**
* 是否为空
* @returns
*/
isEmpty(): boolean {
return this.membersMap.size <= 0
}
/**
* 是否需要拉取
* @returns
*/
needFetchMember(): boolean {
if (this.membersMap.size <= 0) {
return true
}
if (this.membersMap.size < 100 && this.isFinish === false) {
return true
}
return false
}
/**
* 好友信息变更
* @param friend
*/
onFriendInfoChanged(friend: V2NIMFriend) {
let member = this.membersMap.get(friend.accountId)
if (member) {
member.friendInfo = friend
this.membersMap.set(friend.accountId, member)
}
}
/**
* 群乘以信息变更
* @param members
*/
onTeamMemberInfoUpdated(members: V2NIMTeamMember[]) {
if (members[0].teamId === this.teamId) {
members.forEach((m) => {
let member = this.membersMap.get(m.accountId)
if (member) {
member.teamMember = m
this.membersMap.set(m.accountId, member)
if (m.accountId === ChatKitClient.getLoginUserId()) {
this.mineTeamMember = m
}
}
})
}
}
/**
* 离开群
* @param members
*/
onTeamMemberLeft(members: V2NIMTeamMember[]) {
if (members[0].teamId === this.teamId) {
let removeIds: string[] = members.map(member => member.accountId)
removeIds.forEach((e) => {
this.membersMap.delete(e)
})
}
}
/**
* 踢人
* @param accId
* @param members
*/
onTeamMemberKicked(accId: string, members: V2NIMTeamMember[]) {
if (members[0].teamId === this.teamId) {
let removeIds: string[] = members.map(member => member.accountId)
removeIds.forEach((e) => {
this.membersMap.delete(e)
})
}
}
/**
* 成员加入
* @param members
*/
onTeamMemberJoined(members: V2NIMTeamMember[]) {
if (members[0].teamId === this.teamId) {
let accIds = members.map(m => m.accountId)
TeamRepo.getTeamMembersByIds(this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL,
accIds).then((newMembers) => {
newMembers.forEach((m) => {
this.membersMap.set(m.getAccId(), m)
})
})
}
}
/**
* 获取所有成员的accId
* @returns
*/
getAllMemberAccounts(): string[] {
return Array.from(this.membersMap.keys())
}
/**
* 获取所有的成员列表
* @returns
*/
getAllMembers(): TeamMemberWithUser[] {
return Array.from(this.membersMap.values()).sort((a, b) => this.sortTeamMember(a, b))
}
// 群更新
onTeamInfoUpdated = (team: V2NIMTeam) => {
if (team.teamId === this.teamId) {
this.team = team
}
}
/**
* 刷新群信息
*/
onSyncFinished = () => {
if (this.teamId) {
TeamRepo.getTeamInfo(this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL)
.then((team) => {
this.team = team
})
TeamRepo.getTeamMembersByIds(
this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL, [ChatKitClient.getLoginUserId()]
)
.then((members: TeamMemberWithUser[]) => {
if (members.length > 0) {
this.mineTeamMember = members[0].teamMember
}
})
}
}
/**
* 用户信息变更
* @param users
*/
onUserProfileChanged(users: V2NIMUser[]) {
users.forEach((user) => {
let member = this.membersMap.get(user.accountId)
if (member) {
member.userInfo = user
this.membersMap.set(user.accountId, member)
}
})
}
/**
* 初始化成员信息变化回调
*/
initMemberChangeListener() {
//用户信息变更
ChatKitClient.nim.userService?.on('onUserProfileChanged', this.onUserProfileChanged.bind(this))
//好友信息变更
ChatKitClient.nim.friendService?.on('onFriendInfoChanged', this.onFriendInfoChanged.bind(this))
//群成员信息变更
ChatKitClient.nim.teamService?.on('onTeamMemberInfoUpdated', this.onTeamMemberInfoUpdated.bind(this))
ChatKitClient.nim.teamService?.on('onTeamMemberLeft', this.onTeamMemberLeft.bind(this))
ChatKitClient.nim.teamService?.on('onTeamMemberKicked', this.onTeamMemberKicked.bind(this))
ChatKitClient.nim.teamService?.on('onTeamMemberJoined', this.onTeamMemberJoined.bind(this))
//群信息变更
ChatKitClient.nim.teamService?.on('onTeamInfoUpdated', this.onTeamInfoUpdated.bind(this))
//断网
ChatKitClient.nim.teamService?.on('onSyncFinished', this.onSyncFinished.bind(this))
}
/**
* 初始化群信息
*/
async getTeam(): Promise<V2NIMTeam | undefined> {
if (!this.team) {
this.team = await TeamRepo.getTeamInfo(this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL)
}
return this.team
}
/**
* 拉取全量成员
*/
async loadAllTeamMember() {
if (this.team) {
while (!this.isFinish) {
await this.getMemberList()
}
}
}
async getMoreMemberList() {
if (!this.isFinish) {
await this.getMemberList()
}
}
async getMemberList() {
let result = await TeamRepo.getTeamMembers(this.teamId,
V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL, {
nextToken: this.nextToken,
direction: V2NIMQueryDirection.V2NIM_QUERY_DIRECTION_ASC
}).catch((e: V2NIMError) => {
console.debug(`${TeamMemberCache.logTag} getTeamMembers error code = ${e.code}`)
})
if (result) {
this.isFinish = result.finished
this.nextToken = result.nextToken
result.memberList.forEach((e) => {
this.membersMap.set(e.getAccId(), e)
})
let mineMember = result.memberList.find(member => member.getAccId() === ChatKitClient.getLoginUserId())
if (mineMember) {
this.mineTeamMember = mineMember.teamMember
}
}
}
//群成员排序
sortTeamMember(a: TeamMemberWithUser, b: TeamMemberWithUser): number {
if (a.teamMember.memberRole === b.teamMember.memberRole) {
return a.teamMember.joinTime - b.teamMember.joinTime
}
if (a.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER) {
return -1
}
if (b.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER) {
return 1
}
if (a.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER) {
return -1
}
if (b.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER) {
return 1
}
return a.teamMember.joinTime - b.teamMember.joinTime
}
/**
* 获取自己的成员信息
* @returns
*/
async getMineMember(): Promise<V2NIMTeamMember | undefined> {
if (!this.mineTeamMember) {
let mineMember = (await TeamRepo.getTeamMembersByIds(
this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL, [ChatKitClient.getLoginUserId()]
)
).find(m => m.getAccId() === ChatKitClient.getLoginUserId())
if (mineMember) {
this.membersMap.set(mineMember.getAccId(), mineMember)
this.mineTeamMember = mineMember.teamMember
}
}
return this.mineTeamMember
}
/**
* 根据accId 获取具体的成员
* @param account
* @returns
*/
async getMemberById(account: string): Promise<TeamMemberWithUser | undefined> {
let member = this.membersMap.get(account)
if (member) {
return member
} else {
let result = (await TeamRepo.getTeamMembersByIds(
this.teamId, V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL, [account]
))
if (result.length > 0) {
const m = result[0]
this.membersMap.set(m.getAccId(), m)
return m
}
}
return undefined
}
clear() {
this.membersMap.clear()
this.mineTeamMember = undefined
this.teamId = ''
this.team = undefined
this.isFinish = false
this.nextToken = ''
//用户信息变更
ChatKitClient.nim.userService?.off('onUserProfileChanged', this.onUserProfileChanged.bind(this))
//好友信息变更
ChatKitClient.nim.friendService?.off('onFriendInfoChanged', this.onFriendInfoChanged.bind(this))
//群成员信息变更
ChatKitClient.nim.teamService?.off('onTeamMemberInfoUpdated', this.onTeamMemberInfoUpdated.bind(this))
ChatKitClient.nim.teamService?.off('onTeamMemberLeft', this.onTeamMemberLeft.bind(this))
ChatKitClient.nim.teamService?.off('onTeamMemberKicked', this.onTeamMemberKicked.bind(this))
ChatKitClient.nim.teamService?.off('onTeamMemberJoined', this.onTeamMemberJoined.bind(this))
//群信息变更
ChatKitClient.nim.teamService?.off('onTeamInfoUpdated', this.onTeamInfoUpdated.bind(this))
ChatKitClient.nim.teamService?.off('onSyncFinished', this.onSyncFinished.bind(this))
}
/**
* 是否已经加载了所有成员
* @returns
*/
haveLoadAllMember(): boolean {
return this.isFinish
}
}

View File

@ -0,0 +1,47 @@
// 用于显示合并后的消息的发送方Nick的key
export const mergedMessageNickKey: string = 'mergedMessageNickKey';
// 用于显示合并后的消息的发送方avatar的key
export const mergedMessageAvatarKey: string = 'mergedMessageAvatarKey';
// 合并转发消息文件名前缀
export const multiForwardFileName = "multiForward"
/// 合并转发消息最大层数(深度): 3
export let mergedMessageMaxDepth = 3
/// 合并转发消息限制条数: 100
export let mergedMessageLimitCount = 100
/// 逐条转发消息限制条数: 10
export let singleMessageLimitCount = 10
/// 批量删除消息限制条数: 50
export let deleteMessagesLimitCount = 50
/// 合并转发自定义消息 type: 101
export let mergedMessageCustomType = 101
/// 合并转发自定义消息 cellHeight: 130
export let mergedMessageCellHeight: number = 130
/// 会话选择器最大选择数量: 9
export let conversationSelectLimitCount: number = 9
// 最新操作的类型
export let keyExtensionLastOptType = "lastOpt"
// 群自定义配置参数,用于是否群中所有人都可以@所有人配置的KEY值
export let keyExtensionAtAll = "yxAllowAt"
// 群自定义配置参数,用于是否群中管理员可以@所有人,允许所有群成员@所有人
export let typeExtensionAllowAll = "all"
// 群自定义配置参数,用于是否群中管理员可以@所有人,只允许管理员@所有人
export let typeExtensionAllowManager = "manager"
/// 收藏类型与消息类型映射(在类型基础上+1000)
export let collectionTypeOffset = 1000
/// 回复消息key, 用于不使用 thread 的消息回复方案
export let keyReplyMsgKey = "yxReplyMsg"

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { LogLevel } from '@nimsdk/base'
import { LoggerKitImpl } from './LoggerKitImpl'
export class KitLogger {
private core: LoggerKitImpl
public constructor(core: LoggerKitImpl) {
this.core = core
}
public async debug(label?: string, message?: string, ...args: string[]): Promise<void> {
try {
label = label ? label : 'Debug'
message = message ? message : 'logMessage'
const level = LogLevel.Debug
this.core.write(level, label, message, args)
} catch (e) {
this.core.write(LogLevel.Error, label ?? '', 'debug', ['write error'])
}
}
public async info(label?: string, message?: string, ...args: string[]): Promise<void> {
try {
label = label ? label : 'Info'
message = message ? message : 'logMessage'
const level = LogLevel.Info
this.core.write(level, label, message, args)
} catch (e) {
this.core.write(LogLevel.Error, label ?? '', 'debug', ['write error'])
}
}
public async warn(label?: string, message?: string, ...args: string[]): Promise<void> {
try {
label = label ? label : 'Warn'
message = message ? message : 'logMessage'
const level = LogLevel.Warn
this.core.write(level, label, message, args)
} catch (e) {
this.core.write(LogLevel.Error, label ?? '', 'debug', ['write error'])
}
}
public async error(label?: string, message?: string, ...args: string[]): Promise<void> {
try {
label = label ? label : 'Error'
message = message ? message : 'logMessage'
const level = LogLevel.Error
this.core.write(level, label, message, args)
} catch (e) {
this.core.write(LogLevel.Error, label ?? '', 'debug', ['write error'])
}
}
}

View File

@ -0,0 +1,248 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { taskpool } from '@kit.ArkTS';
import { LogLevel, NetWorkingInStance, NIMEStrAnyObj, V2NIMErrorImpl } from '@nimsdk/base';
import fs from '@ohos.file.fs';
@Concurrent
async function writeLogger(filePath: string, level: string, label: string, message: string, state: string,
...args: NIMEStrAnyObj[]) {
// CELLULAR = 0, WIFI = 1, ETHERNET = 3, VPN = 4
const netInfo = NetWorkingInStance.getInstance().getNetInfoSync()
const customInfo = `${state}_${netInfo.netType}_${netInfo.isConnected}`
// date formatter
const date = new Date()
const milliseconds: number = date.getMilliseconds()
const formattedMilliseconds: string = ("00" + milliseconds).slice(-3);
const dateStr = `${date.getMonth() +
1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${formattedMilliseconds}`
const prefix = `[${dateStr}]: ${level}: ${customInfo}: ${label}: ${message} `
// args formatter
let tempStr = args.map((item) => {
if (item instanceof V2NIMErrorImpl) {
let desc = `${item.name}\n code: ${item.code}\n message: "${item.message}"\n detail: ${item.detail ?
JSON.stringify(item.detail) : ''}`
if (item?.detail?.rawError) {
desc += `\n rawError: ${item.detail.rawError.message}`
}
return desc
} else if (item instanceof Error) {
return item && item.message ? item.message : item
} else if (typeof item === 'object') {
return JSON.stringify(item).replace(/^\[|\]$/g, "").replace(/^\[|\]$/g, "")
} else {
return item
}
}).join(' ')
tempStr = tempStr.replace(/^"|"$/g, '')
const logMessage = tempStr ? `${prefix + ': ' + tempStr}\n` : `${prefix}\n`
const file: fs.File = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.APPEND)
fs.writeSync(file.fd, logMessage)
fs.closeSync(file)
}
const TAG = '[LoggerServiceImpl]'
export class LoggerKitImpl {
// 本地日志,默认保存 15 天
private static readonly LoggerFileEffectivePeriod: number = 15
logDir: string = ''
// core: NIM
sequenceRunner: taskpool.SequenceRunner
logLevel: LogLevel
isOpenConsoleLog: boolean
private currentLogPath: string
public constructor(cacheDir: string, logLevel?: LogLevel, isOpenConsoleLog?: boolean) {
// core.loggerService = this
// this.core = core
this.logDir = cacheDir + '/nim_kit_log'
this.checkLoggerEffectivePeriod(this.logDir)
const formattedDate = this.formattedDate()
this.currentLogPath = this.logDir + `/nim_kit_${formattedDate}.txt`
// this.core.eventBus.on('LoggerServiceImpl/onUploadLogFiles', () => {
// this.uploadZipLogFile(false)
// })
this.sequenceRunner = new taskpool.SequenceRunner()
this.logLevel = logLevel ?? LogLevel.Debug
this.isOpenConsoleLog = isOpenConsoleLog ?? false
}
async write(level: LogLevel, label: string, message: string, ...args: NIMEStrAnyObj[]): Promise<void> {
let state = 'U'
// const isForeground = this.core.settingService?.v2IGetIsForeground()
// if (typeof isForeground !== 'undefined') {
// state = isForeground ? 'F' : 'B'
// }
if (this.isWriteLog(level)) {
const filePath = this.currentLogPath
const task = new taskpool.Task(writeLogger, filePath, level, label, message, state, args)
this.sequenceRunner.execute(task)
}
this.consoleLog(level, label, message, args)
}
isWriteLog(level: LogLevel): boolean {
switch (this.logLevel) {
case LogLevel.Debug:
return true;
case LogLevel.Info:
return level !== LogLevel.Debug;
case LogLevel.Warn:
return level !== LogLevel.Debug && level !== LogLevel.Info;
case LogLevel.Error:
return level === LogLevel.Error;
default:
return true;
}
}
consoleLog(level: LogLevel, label: string, message: string, args: NIMEStrAnyObj[]) {
// console log
message = message.slice(0, 2000)
if (this.isOpenConsoleLog) {
if (level === LogLevel.Debug) {
console.debug(label, message, args[0].length === 0 ? '' : JSON.stringify(args[0]))
} else if (level === LogLevel.Info) {
console.info(label, message, args[0].length === 0 ? '' : JSON.stringify(args[0]))
} else if (level === LogLevel.Error) {
console.error(label, message, args[0].length === 0 ? '' : JSON.stringify(args[0]))
} else if (level === LogLevel.Warn) {
console.warn(label, message, args[0].length === 0 ? '' : JSON.stringify(args[0]))
}
}
}
// async uploadSDKLogs(isActive: boolean): Promise<string> {
// try {
// this.core.logger.info(TAG, 'uploadSDKLogs', isActive)
// return await this.uploadZipLogFile(isActive)
// } catch (e) {
// this.core.logger.error(TAG, 'uploadSDKLogs', isActive, e)
// if (e instanceof V2NIMErrorImpl || e.name === 'V2NIMError') {
// throw e as V2NIMErrorImpl
// } else {
// throw new V2NIMErrorImpl({
// code: V2NIMErrorCode.V2NIM_ERROR_CODE_FILE_UPLOAD_FAILED,
// detail: {
// reason: 'upload log file: error: ' + `${JSON.stringify(e)}`
// }
// })
// }
// }
// }
public getLogDirectory(): string {
return this.logDir
}
public getLogFilePath(): string {
return this.currentLogPath
}
async checkLoggerEffectivePeriod(fileDir: string): Promise<void> {
try {
// 获取log list
if (!fs.accessSync(this.logDir)) {
fs.mkdirSync(this.logDir, true);
}
let fileList: Array<string> = await fs.listFile(fileDir, { recursion: false, listNum: 0 })
// 大于等于 15 时,淘汰最久的
if (fileList.length >= LoggerKitImpl.LoggerFileEffectivePeriod) {
fileList.sort((a, b) => a.localeCompare(b))
let oldestName: string = fileList[0]
let oldestFilePath = `${fileDir} + ${oldestName}`
await fs.unlink(oldestFilePath)
}
} catch (e) {
this.write(LogLevel.Error, 'checkLoggerEffectivePeriod', `fail:${JSON.stringify(e)}, filename ${fileDir}`)
}
}
// async uploadZipLogFile(isActive: boolean): Promise<string> {
// let outFile = this.getOutputZipFilePath()
// try {
// this.core.logger.info(TAG, 'uploadZipLogFile', isActive)
// // zip log file
// const fileList: Array<string> = await fs.listFile(this.logDir, { recursion: false, listNum: 0 })
// const zipList = fileList.map(item => this.logDir + '/' + item)
// await zlib.compressFiles(zipList, outFile, {
// level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION,
// memLevel: zlib.MemLevel.MEM_LEVEL_MAX,
// strategy: zlib.CompressStrategy.COMPRESS_STRATEGY_HUFFMAN_ONLY
// })
//
// const result: UploadFileResult = await this.core.storageService.uploadFileTask({
// taskId: guid(),
// uploadParams: {
// filePath: outFile,
// sceneName: V2NIMStorageSceneConfig.DEFAULT_SYSTEM().sceneName,
// } as V2NIMUploadFileParams
// })
// // 上传成功删除本地zip 文件
// await this.uploadSendLog(result.url, isActive)
// await fs.unlink(outFile)
// this.core.logger.info(TAG, 'uploadZipLogFile succeed')
// return result.url
// } catch (e) {
// await fs.unlink(outFile)
// this.core.logger.error(TAG, 'uploadZipLogFile', isActive, e)
// if (e instanceof V2NIMErrorImpl || e.name === 'V2NIMError') {
// throw e as V2NIMErrorImpl
// } else {
// throw new V2NIMErrorImpl({
// code: V2NIMErrorCode.V2NIM_ERROR_CODE_FILE_UPLOAD_FAILED,
// detail: {
// reason: 'upload log file: error: ' + `${JSON.stringify(e)}`
// }
// })
// }
// }
// }
// private async uploadSendLog(url: string, isActive: boolean): Promise<void> {
// try {
// this.core.logger.info(TAG, 'uploadSendLog', url, isActive)
// const activeType = isActive ? 1 : 0
// const uploadLogUrlRequest: UploadLogUrlRequest = new UploadLogUrlRequest(
// url,
// new UploadLogUrlParams(activeType)
// )
// await this.core.sendCmd('uploadLogUrl', uploadLogUrlRequest)
// } catch (e) {
// this.core.logger.error(TAG, 'uploadSendLog', url, isActive, e)
// }
// }
private getOutputZipFilePath(): string {
const filepath = this.logDir + `/nim_log.zip`
if (fs.accessSync(filepath)) {
fs.unlinkSync(filepath)
}
return filepath
}
private formattedDate(): string {
const date = new Date()
const formattedDate = Intl.DateTimeFormat("en-US",
{ year: "numeric", month: "numeric", day: "numeric" }
).format(date)
.replace(/\//g, '_');
return formattedDate
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
/**
* Request params of protocol:
* uploadLogUrl
*/
export class UploadLogUrlRequest {
public url: string
public info: UploadLogUrlParams
public constructor(url: string, info: UploadLogUrlParams) {
this.url = url
this.info = info
}
}
/**
* Params of 'UploadLogUrlRequest'
*/
export class UploadLogUrlParams {
public sdklogUploadType: number
public constructor(sdklogUploadType: number) {
this.sdklogUploadType = sdklogUploadType
}
}

View File

@ -0,0 +1,14 @@
// 转发选择页面数据模型
@ObservedV2
export class ConversationSelectModel {
// 会话id
conversationId?: string
// 会话名称
name?: string
// 会话头像
avatar?: ResourceStr
// 是否已选中
@Trace isSelected: boolean = false
// 会话人数,用于展示群人数(单聊默认为 0群聊为群人数
memberCount: number = 0
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ConversationSelectModel } from './ConversationSelectModel';
export class ConversationSelectParam {
filterList?: string[] = [];
selectLimit: number = 200;
onSureButtonClick?: (selectedList: ConversationSelectModel[]) => void = undefined
constructor(filterList: string[] = [],
selectLimit: number = 200,
onSureButtonClick?: (selectedList: ConversationSelectModel[]) => void
) {
this.filterList = filterList
this.selectLimit = selectLimit
this.onSureButtonClick = onSureButtonClick
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ConversationSelectModel } from './ConversationSelectModel';
export class ConversationSelectedParam {
selectedList?: ConversationSelectModel[] = [];
onRemoveButtonClick?: (item: ConversationSelectModel) => void = undefined // 移除按钮点击事件
constructor(selectedList?: ConversationSelectModel[],
onRemoveButtonClick?: (item: ConversationSelectModel) => void
) {
this.selectedList = selectedList
this.onRemoveButtonClick = onRemoveButtonClick
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
// 自定义消息附件
export interface CustomMessageAttachment {
// 自定义消息类型
type?: string;
}
// 合并消息
export interface MergedMessageAttachment extends CustomMessageAttachment {
// 会话id
sessionId?: string;
// 会话名称
sessionName?: string;
// 合并消息上传NOS后的url
url?: string;
// 合并消息文件的md5
md5?: string;
// 合并消息的深度
depth?: number;
// 合并消息的摘要,用于在消息列表展示,默认三条
abstracts?: MergeMessageAbstract[];
// 合并消息的id用于标识合并消息
// 通[NIMMessage.uuid]获取
messageId?: string;
}
// 合并转发消息的缩略
export interface MergeMessageAbstract {
// 消息展示的nick只取fromNick没有就accId
senderNick: string;
// 内容不是Text的显示缩略
content: string;
// 发送方的accId
userAccId: string;
}
// 消息上传后的信息
export interface MessageUploadInfo {
url: string;
md5: string;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { V2NIMFriend, V2NIMUser } from '@nimsdk/base';
interface NEUserWithFriendParams {
friend?: V2NIMFriend;
user?: V2NIMUser;
}
@ObservedV2
export class NEUserWithFriend {
@Trace user?: V2NIMUser
@Trace friend?: V2NIMFriend
constructor(params: NEUserWithFriendParams) {
this.friend = params.friend
if (params.user) {
this.user = params.user
} else {
this.user = params.friend?.userProfile
}
}
/// 获取显示名称
/// (备注) > 昵称 > accid
/// - Parameter showAlias: 是否优先显示备注
public showName(showAlias: boolean = true): string {
if (showAlias && this.friend?.alias && this.friend.alias.length > 0) {
return this.friend?.alias
}
if (this.user?.name && this.user.name.length > 0) {
return this.user.name
}
return this.user?.accountId ?? ""
}
/// 获取简称 (尾部截取)
/// - Parameter showAlias: 是否优先显示备注
/// - Parameter count: 尾部截取长度
public shortName(showAlias: boolean = true, count: number = 2): string {
let name = this.showName(showAlias)
if (name) {
const start: number = (name.length - count) > 0 ? name.length - count : 0
const end: number = name.length
return name.substring(start, end)
}
return ""
}
/**
* 获取好友备注
* @returns
*/
public getAlias() {
return this.friend?.alias
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { NEUserWithFriend } from './NEUserWithFriend';
export class PersonSelectParam {
filterList?: string[] = [];
selectLimit: number = 200;
onClickSureButton?: (selectedList: NEUserWithFriend[]) => void = undefined
constructor(onClickSureButton?: (selectedList: NEUserWithFriend[]) => void, filterList: string[] = [],
selectLimit: number = 200) {
this.filterList = filterList
this.selectLimit = selectLimit
this.onClickSureButton = onClickSureButton
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export interface RevokeMessageExtension {
revoke_message_local: boolean,
revoke_message_local_time: number,
revoke_message_local_edit: boolean,
revoke_message_local_content: string,
revoke_message_client_id: string,
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { V2NIMFriend, V2NIMTeamMember, V2NIMTeamMemberRole, V2NIMUser } from '@nimsdk/base'
/**
* 群成员,好友,用户信息的集合
*/
export class TeamMemberWithUser {
//群成员信息
teamMember: V2NIMTeamMember
//好友信息
friendInfo?: V2NIMFriend
//用户信息
userInfo?: V2NIMUser
constructor(teamMember: V2NIMTeamMember,
friendInfo?: V2NIMFriend, userInfo?: V2NIMUser) {
this.teamMember = teamMember
this.friendInfo = friendInfo
this.userInfo = userInfo
}
/**
* 获取头像
* @returns
*/
getAvatar(): string | undefined {
return this.userInfo?.avatar
}
/**
* 获取头像nick
* @returns
*/
getAvatarName(): string {
let avatarName = ''
if (this.friendInfo && this.friendInfo.alias &&
this.friendInfo.alias.length > 0) {
avatarName = this.friendInfo.alias
} else if (this.userInfo && this.userInfo.name && this.userInfo.name.length > 0) {
avatarName = this.userInfo.name
} else {
avatarName = this.teamMember.accountId
}
return avatarName.length > 2 ? avatarName.substring(avatarName.length - 2, avatarName.length) : avatarName
}
/**
* 获取@的名称,不包含好友备注
*/
getAitName(): string {
//群昵称
if (this.teamMember.teamNick &&
this.teamMember.teamNick.length > 0) {
return this.teamMember.teamNick
}
//用户名
if (this.userInfo && this.userInfo.name && this.userInfo.name.length > 0) {
return this.userInfo.name
}
//最后accId
return this.teamMember.accountId
}
/**
* 是否是群主
* @returns
*/
isOwner(): boolean {
return this.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
}
/**
* 是否是管理员
* @returns
*/
isManager(): boolean {
return this.teamMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
}
/**
* 获取昵称
* @returns
*/
getNickname(): string {
//优先好友备注
if (this.friendInfo && this.friendInfo.alias &&
this.friendInfo.alias.length > 0) {
return this.friendInfo.alias
}
//其次群昵称
if (this.teamMember.teamNick &&
this.teamMember.teamNick.length > 0) {
return this.teamMember.teamNick
}
//再次用户名
if (this.userInfo && this.userInfo.name && this.userInfo.name.length > 0) {
return this.userInfo.name
}
//最后accId
return this.teamMember.accountId
}
/**
* 获取群成员Id
* @returns
*/
getAccId(): string {
return this.teamMember.accountId
}
}
/**
* 群成员请求返回结果
*/
export interface TeamMemberResult {
//是否结束
finished: boolean
//下次请求token
nextToken: string
//成员列表
memberList: TeamMemberWithUser[]
}

View File

@ -0,0 +1,4 @@
export interface TeamSettingParam {
teamId: string,
memberIds: string[]
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
/**
* @所有人的权限
*/
export interface AitAllPermission {
yxAllowAt: string
lastOpt: string
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { AitSegment } from './AitSegment'
import { Type } from 'class-transformer'
import 'reflect-metadata'
export class AitMessage {
/**
* 账号
*/
accountId: string = ""
//@的文本
text: string
//@的信息存储,一个文案可能被@多次
@Type(() => AitSegment)
segments: AitSegment[] = []
constructor(text: string, accountId: string) {
this.text = text
this.accountId = accountId
}
addSegment(start: number, end: number, broken?: boolean) {
let segment: AitSegment = {
start: start,
end: end,
broken: broken ?? false
}
this.segments.push(...[segment])
}
removeSegment(start: number, end: number) {
let index = this.segments.findIndex(e => e.start === start && e.end === end)
if (index >= 0) {
this.segments.splice(index, 1)
}
}
valid(): boolean {
if (this.segments.length < 0) {
return false
}
for (let segment of this.segments) {
if (!segment.broken) {
return true
}
}
return false
}
segmentToMap(segment: AitSegment): Map<string, number | boolean> {
let map = new Map<string, number | boolean>()
map.set('start', segment.start)
map.set('end', segment.end)
map.set('broken', segment.broken ?? false)
return map
}
}

View File

@ -0,0 +1,271 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { AitMessage } from './AitMessage'
import { plainToClass, Transform, Type } from 'class-transformer'
import 'reflect-metadata'
import { AitSegment } from './AitSegment'
import { JSON } from '@kit.ArkTS'
//@所有人的Account
export const accountAll: string = "ait_all"
//@key
export const aitKey: string = 'yxAitMsg'
/**
* 云信@功能集合
*/
export interface YxAitMsg {
yxAitMsg?: Map<string, AitMessage>
}
export class AitModel {
/**
* key 为AccountID
*/
@Type(() => Map)
@Transform((param) => {
const map = new Map<string, AitMessage>();
if (param.value instanceof Map) {
let params = param.value as Map<string, AitMessage>
params.forEach((
value, key
) => {
map.set(key, value)
})
}
return map;
})
aitBlocks: Map<string, AitMessage> = new Map()
reset() {
this.aitBlocks.clear()
}
/**
* 删除一个@的用户
* @param deletedText 文本
* @param endIndex 删除的文本的光标所在位置
* @param length 删除的文本的长度
* @returns
*/
deleteAitUser(deletedText: string, endIndex: number, length: number): AitMessage | null {
//如果deletedText为空直接返回
if (deletedText.length <= 0) {
return null
}
let len: number = deletedText.length;
//如果aitBlocks 中有Value值和deletedText不匹配则返回
let removedBlack: AitMessage | null = null;
for (let aitMsg of this.aitBlocks.values()) {
for (let segment of aitMsg.segments) {
if (endIndex < segment.start) {
segment.start -= length;
segment.end -= length;
continue;
}
if (len < segment.end + 1 ||
deletedText.substring(segment.start, segment.end + 1) !== aitMsg.text) {
removedBlack = new AitMessage(aitMsg.text, "");
removedBlack.addSegment(segment.start, segment.end);
}
}
}
if (removedBlack !== null) {
this.removeSegment(removedBlack, length);
}
return removedBlack;
}
/**
* 删除文本
* @param endIndex 删除后光标的位置
* @param length 删除的长度
*/
deleteText(endIndex: number, length: number) {
let removedBlack: AitMessage | null = null;
for (let aitMsg of this.aitBlocks.values()) {
let deletedSegment: AitSegment | null = null
for (let segment of aitMsg.segments) {
if (endIndex < segment.start) {
segment.start -= length;
segment.end -= length;
continue;
} else if (endIndex < segment.end) {
deletedSegment = segment
}
}
if (deletedSegment !== null) {
aitMsg.removeSegment(deletedSegment.start, deletedSegment.end)
}
if (aitMsg.segments.length <= 0) {
removedBlack = aitMsg
}
}
if (removedBlack !== null) {
this.removeSegment(removedBlack, length)
}
}
/**
* 删除aitMsg中的segment,如果aitMsg中的segment为空则删除aitMsg
* 同时处理其他aitMsg中的segment的位置
* @param removedBlack
* @param deletedLen 删除的文本的长度,如果为正数则其他后面的前移,如果为负数,则其他不变
*/
removeSegment(removedBlack: AitMessage, deletedLen: number): void {
// 删除aitMsg中的segment,如果aitMsg中的segment为空则删除aitMsg
let aitMsg: AitMessage | undefined = undefined
let removeKey: string = ''
this.aitBlocks.forEach((value, key) => {
if (value.text === removedBlack.text) {
aitMsg = value
removeKey = key
}
})
if (aitMsg) {
//artTs 语法要求,重新拷贝
const aitMessage: AitMessage = aitMsg
const start: number = removedBlack.segments[0].start;
const end: number = removedBlack.segments[0].end;
// 该段文字的长度,加上删除的长度,因为其在前面已经移了deletedLen位
const length = end - start + 1 - deletedLen;
aitMessage.removeSegment(start, end);
if (aitMessage.segments.length === 0) {
this.aitBlocks.delete(removeKey);
}
if (deletedLen > 0) {
for (const aitMsg of this.aitBlocks.values()) {
for (const segment of aitMsg.segments) {
if (end <= segment.start) {
segment.start -= length;
segment.end -= length;
continue;
}
}
}
}
}
}
/**
* 用户是否被@
* @param accId
* @returns
*/
isUserBeenAit(accId: string | null): boolean {
if (accId === null) {
return false;
}
for (let key of this.aitBlocks.keys()) {
if (key === accountAll) {
return true;
}
if (key === accId) {
return true;
}
}
return false;
}
/**
* 拷贝
* @param aitModel
*/
fork(aitModel: AitModel) {
this.aitBlocks.clear()
this.aitBlocks = aitModel.aitBlocks
}
/**
* 根据插入后的Text 文案, segment 移位或者删除。
* @param changedText
* @param endIndex
* @param length
*/
insertText(endIndex: number, length: number): void {
let removedBlack: AitMessage | null = null;
const start: number = endIndex - length;
for (let aitMsg of this.aitBlocks.values()) {
for (let segment of aitMsg.segments) {
if (start <= segment.start) {
segment.start += length;
segment.end += length;
continue;
}
if (endIndex > segment.start && endIndex <= segment.end) {
removedBlack = new AitMessage(aitMsg.text, "");
removedBlack.addSegment(segment.start, segment.end);
continue;
}
}
}
if (removedBlack !== null) {
this.removeSegment(removedBlack, -1);
}
}
/**
* 添加@成员
* @param account
* @param name
* @param start
*/
addAitMember(account: string, name: string, start: number): void {
for (let aitMsg of this.aitBlocks.values()) {
for (let segment of aitMsg.segments) {
if (start <= segment.start) {
segment.start += name.length;
segment.end += name.length;
continue;
}
}
}
let aitBlock: AitMessage | undefined = this.aitBlocks.get(account);
if (aitBlock === undefined) {
aitBlock = new AitMessage(name, account);
this.aitBlocks.set(account, aitBlock);
}
const end: number = start + name.length - 1;
aitBlock.addSegment(start, end);
}
}
/**
* Extension 解析获得AitModel
* @param extension
* @returns
*/
export function getAitModelFromJson(extension?: string): AitModel | undefined {
if (extension) {
try {
const obj: Record<string, Object> = JSON.parse(extension) as Record<string, Object>
for (let key of Object.entries(obj)) {
if (key[0] === aitKey) {
let trans: AitModelTrans = {
aitBlocks: key[1]
}
let aitModel: AitModel = plainToClass(AitModel, trans, { enableImplicitConversion: true })
if (aitModel.aitBlocks.size > 0) {
return aitModel
}
}
}
}catch (e) {
console.error('parse json error', JSON.stringify(e), JSON.stringify(extension))
return undefined
}
}
return undefined
}
interface AitModelTrans {
aitBlocks: object
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
/**
* 记录@消息的最小单位
*/
export class AitSegment {
//@消息的起点
start: number = 0;
//@消息的终点位置
end: number = 0;
//是否已经被破坏
broken: boolean = false
}

View File

@ -0,0 +1,514 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMAddCollectionParams,
V2NIMCollection,
V2NIMCollectionOption,
V2NIMMessage,
V2NIMMessageDeletedNotification,
V2NIMMessageListOption,
V2NIMMessagePin,
V2NIMMessagePinNotification,
V2NIMMessageRefer,
V2NIMMessageRevokeNotification,
V2NIMMessageRevokeParams,
V2NIMMessageSearchParams,
V2NIMP2PMessageReadReceipt,
V2NIMSendMessageParams,
V2NIMSendMessageResult,
V2NIMTeamMessageReadReceipt,
V2NIMTeamMessageReadReceiptDetail
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
export class ChatRepo {
/**
* 发送消息
* @param sendFn 消息接受处理方法
*/
static onSendMessage(sendFn: (message: V2NIMMessage) => void) {
ChatKitClient.nim.messageService?.on('onSendMessage', sendFn);
}
/**
* 发送消息
* @param receiveFn 消息接受处理方法
*/
static offSendMessage(receiveFn: (message: V2NIMMessage) => void) {
ChatKitClient.nim.messageService?.off('onSendMessage', receiveFn);
}
/**
* 接受消息
* @param receiveFn 消息接受处理方法
*/
static onReceiverMessage(receiveFn: (messageList: V2NIMMessage[]) => void) {
ChatKitClient.nim.messageService?.on('onReceiveMessages', receiveFn);
}
/**
* 接受消息
* @param receiveFn 消息接受处理方法
*/
static offReceiverMessage(receiveFn: (messageList: V2NIMMessage[]) => void) {
ChatKitClient.nim.messageService?.off('onReceiveMessages', receiveFn);
}
/**
* 删除消息
* @param receiveFn 消息接受处理方法
*/
static onDeleteMessage(deleteFn: (messageList: V2NIMMessageDeletedNotification[]) => void) {
ChatKitClient.nim.messageService?.on('onMessageDeletedNotifications', deleteFn);
}
/**
* 删除消息
* @param receiveFn 消息接受处理方法
*/
static offDeleteMessage(deleteFn: (messageList: V2NIMMessageDeletedNotification[]) => void) {
ChatKitClient.nim.messageService?.off('onMessageDeletedNotifications', deleteFn);
}
/**
* 注册撤回消息监听
* @param receiveFn 消息接受处理方法
*/
static onRevokeMessage(revokeFn: (messageList: V2NIMMessageRevokeNotification[]) => void) {
ChatKitClient.nim.messageService?.on('onMessageRevokeNotifications', revokeFn);
}
/**
* 取消注册撤回消息监听
* @param receiveFn 消息接受处理方法
*/
static offRevokeMessage(revokeFn: (messageList: V2NIMMessageRevokeNotification[]) => void) {
ChatKitClient.nim.messageService?.off('onMessageRevokeNotifications', revokeFn);
}
/**
* 注册单聊消息已读回执监听
* @param receiptsFn 单聊消息已读回执处理方法
*/
static onP2PMessageReadReceipts(receiptsFn: (messageList: V2NIMP2PMessageReadReceipt[]) => void) {
ChatKitClient.nim.messageService?.on('onReceiveP2PMessageReadReceipts', receiptsFn);
}
/**
* 取消注册单聊消息已读回执监听
* @param receiptsFn 单聊消息已读回执处理方法
*/
static offP2PMessageReadReceipts(receiptsFn: (messageList: V2NIMP2PMessageReadReceipt[]) => void) {
ChatKitClient.nim.messageService?.off('onReceiveP2PMessageReadReceipts', receiptsFn);
}
/**
* 注册群消息已读回执监听
* @param receiptsFn 群消息已读回执处理方法
*/
static onReceiveTeamMessageReadReceipts(receiptsFn: (messageList: V2NIMTeamMessageReadReceipt[]) => void) {
ChatKitClient.nim.messageService?.on('onReceiveTeamMessageReadReceipts', receiptsFn);
}
/**
* 取消注册群消息已读回执监听
* @param receiptsFn 群消息已读回执处理方法
*/
static offReceiveTeamMessageReadReceipts(receiptsFn: (messageList: V2NIMTeamMessageReadReceipt[]) => void) {
ChatKitClient.nim.messageService?.off('onReceiveTeamMessageReadReceipts', receiptsFn);
}
/**
* 注册PIN消息监听
* @param pinFn PIN消息监听处理方法
*/
static onMessagePinNotification(pinFn: (notification: V2NIMMessagePinNotification) => void) {
ChatKitClient.nim.messageService?.on('onMessagePinNotification', pinFn);
}
/**
* 取消注册PIN消息监听
* @param pinFn PIN消息监听处理方法
*/
static offMessagePinNotification(pinFn: (notification: V2NIMMessagePinNotification) => void) {
ChatKitClient.nim.messageService?.off('onMessagePinNotification', pinFn);
}
/**
* 注册更新消息监听
* @param modifyFn PIN消息监听处理方法
*/
static onReceiveMessagesModified(modifyFn: (messageList: V2NIMMessage[]) => void) {
ChatKitClient.nim.messageService?.on('onReceiveMessagesModified', modifyFn);
}
/**
* 取消更新消息监听
* @param modifyFn PIN消息监听处理方法
*/
static offReceiveMessagesModified(modifyFn: (messageList: V2NIMMessage[]) => void) {
ChatKitClient.nim.messageService?.off('onReceiveMessagesModified', modifyFn);
}
/**
* 撤回消息
* @param receiveFn 消息接受处理方法
*/
static removeAllListeners(revokeFn: (messageList: V2NIMMessageRevokeNotification[]) => void) {
ChatKitClient.nim.messageService?.removeAllListeners('onMessageRevokeNotifications');
}
/**
* 查询消息
* @param option
* @returns
*/
static async getMessageList(option: V2NIMMessageListOption): Promise<V2NIMMessage[]> {
let messageList = await ChatKitClient.nim.messageService!!.getMessageList(option)
return messageList;
}
/**
* 根据 messageRefers 批量查询消息
* @param option
* @returns
*/
static async getMessageListByRefers(messageRefers: V2NIMMessageRefer[]): Promise<V2NIMMessage[]> {
let messageList = await ChatKitClient.nim.messageService!!.getMessageListByRefers(messageRefers)
return messageList ?? [];
}
/**
* 检索云端的消息
*
* @param params 检索参数
*/
static async searchCloudMessages(params: V2NIMMessageSearchParams): Promise<V2NIMMessage[]> {
let messageList = await ChatKitClient.nim.messageService!!.searchCloudMessages(params)
return messageList ?? [];
}
/**
* 发送消息
*
* @param message 消息体, 由 V2NIMMessageCreator 的对应方法创建
* @param conversationId 会话 id
* @param params 发送消息相关配置参数
* @param progress 发送消息进度回调. 作用于需要上传的文件,图片,音视频消息
*/
static async sendMessage(
message: V2NIMMessage,
conversationId: string,
params?: V2NIMSendMessageParams,
progress?: (percentage: number) => void
): Promise<V2NIMSendMessageResult | undefined> {
return ChatKitClient.nim.messageService?.sendMessage(message, conversationId, params, progress);
}
/**
* 回复消息
*
* @param message 需要发送的消息体, 由 V2NIMMessageCreator 的对应方法创建
* @param replyMessage 被回复的消息
* @param params 发送消息相关配置参数
* @param progress 发送消息进度回调. 作用于需要上传的文件,图片,音视频消息
*/
static async replyMessage(message: V2NIMMessage, replyMessage: V2NIMMessage, params?: V2NIMSendMessageParams,
progress?: (percentage: number) => void): Promise<V2NIMSendMessageResult> {
const res = await ChatKitClient.nim.messageService?.replyMessage(message, replyMessage, params, progress);
return res!;
}
/**
* 插入一条本地消息, 该消息不会发送。该消息不会多端同步,只是本端显示
* @param message 需要插入的消息体
* @param conversationId 会话 ID
* @param senderId 消息发送者账号
* @param createTime 指定插入消息时间
* @returns 插入成功的 message
*/
static async insertMessageToLocal(message: V2NIMMessage, conversationId: string, senderId?: string,
createTime?: number): Promise<V2NIMMessage> {
const res = ChatKitClient.nim.messageService?.insertMessageToLocal(message, conversationId, senderId, createTime);
return res!;
}
/**
* 撤回消息
*
* @param message 需要撤回的消息
* @param params 撤回消息相关参数
*/
static async revokeMessage(message: V2NIMMessage, params?: V2NIMMessageRevokeParams): Promise<void> {
const res = await ChatKitClient.nim.messageService?.revokeMessage(message, params);
return res!;
}
/**
* 删除单条消息
*
* 注: 操作成功后, SDK 会抛出事件 {@link V2NIMMessageListener.onMessageDeletedNotifications | V2NIMMessageListener.onMessageDeletedNotifications}
*
* @param message 需要删除的消息
* @param serverExtension 扩展字段
*/
static async deleteMessage(message: V2NIMMessage, serverExtension?: string,
onlyDeleteLocal?: boolean): Promise<void> {
const res = await ChatKitClient.nim.messageService?.deleteMessage(message, serverExtension, onlyDeleteLocal);
return res!;
}
/**
* 批量删除消息
*
* 注: 操作成功后, SDK 会抛出事件 {@link V2NIMMessageListener.onMessageDeletedNotifications | V2NIMMessageListener.onMessageDeletedNotifications} <br/>
* - 所删除的消息必须是同一会话的消息 <br/>
* - 限制一次最多删除 50 条消息
*
* @param messages 需要删除的消息
* @param serverExtension 扩展字段
*/
static async deleteMessages(messages: V2NIMMessage[], serverExtension?: string,
onlyDeleteLocal?: boolean): Promise<void> {
const res = await ChatKitClient.nim.messageService?.deleteMessages(messages, serverExtension, onlyDeleteLocal);
return res!;
}
/**
* pin 一条消息
*
* @param message 需要被 pin 的消息体
* @param serverExtension 扩展字段
*
* 注: 操作成功后, SDK 会抛出事件 {@link V2NIMMessageListener.onMessagePinNotification | V2NIMMessageListener.onMessagePinNotification}
*/
static async pinMessage(message: V2NIMMessage, serverExtension?: string): Promise<void> {
const res = await ChatKitClient.nim.messageService?.pinMessage(message, serverExtension);
return res!;
}
/**
* 取消一条Pin消息
*
* @param messageRefer 需要被取消 pin 的消息摘要
* @param serverExtension 扩展字段
*/
static async unpinMessage(messageRefer: V2NIMMessageRefer, serverExtension?: string): Promise<void> {
const res = await ChatKitClient.nim.messageService?.unpinMessage(messageRefer, serverExtension);
return res!;
}
/**
* 获取 pin 消息列表
*
* @param conversationId 会话 ID
*/
static async getPinnedMessageList(conversationId: string): Promise<V2NIMMessagePin[]> {
const res = ChatKitClient.nim.messageService?.getPinnedMessageList(conversationId);
return res!;
}
/**
* 添加一个收藏
*
* @param params 添加收藏的相关参数
*/
static async addCollection(params: V2NIMAddCollectionParams): Promise<V2NIMCollection> {
const res = await ChatKitClient.nim.messageService?.addCollection(params)
return res!;
}
/**
* 移除相关收藏
*
* @param collections 需要移除的相关收藏
*/
static async removeCollections(collections: V2NIMCollection[]): Promise<number> {
const res = await ChatKitClient.nim.messageService?.removeCollections(collections)
return res!;
}
/**
* 按条件分页获取收藏信息
*
* @param option 查询参数
*/
static async getCollectionListByOption(option: V2NIMCollectionOption): Promise<V2NIMCollection[]> {
const res = ChatKitClient.nim.messageService?.getCollectionListByOption(option)
return res!;
}
/**
* 构造文本消息
*
* @param text 文本内容. 不允许为空字符串
*
* @example
* ```
* const message = nim.V2NIMMessageCreator.createTextMessage('hello world')
* ```
*/
static createTextMessage(text: string): V2NIMMessage {
return ChatKitClient.nim.messageCreator.createTextMessage(text);
}
/**
* 构造图片消息
*
* @param imagePath 图片文件路径
* @param name 文件显示名称
* @param sceneName 场景名
* @param width 图片宽度.
* @param height 图片高度.
*/
static async createImageMessage(imagePath: string, name?: string, sceneName?: string, width?: number,
height?: number): Promise<V2NIMMessage> {
return ChatKitClient.nim.messageCreator.createImageMessage(imagePath, name, sceneName, width, height);
}
/**
* 构造语音消息
*
* @param audioPath 语音文件地址
* @param name 文件显示名称
* @param sceneName 场景名
* @param duration 音频时长
*/
static async createAudioMessage(audioPath: string, name?: string, sceneName?: string,
duration?: number): Promise<V2NIMMessage> {
return ChatKitClient.nim.messageCreator.createAudioMessage(audioPath, name, sceneName, duration);
}
/**
* 构造视频消息
*
* @param videoPath 视频文件地址
* @param name 文件显示名称
* @param sceneName 场景名
* @param duration 视频时长
* @param width 视频宽度
* @param height 视频高度
*/
static async createVideoMessage(videoPath: string, name?: string, sceneName?: string, duration?: number,
width?: number, height?: number): Promise<V2NIMMessage> {
return ChatKitClient.nim.messageCreator.createVideoMessage(videoPath, name, sceneName, duration, width, height);
}
/**
* 构造文件消息
*
* @param filePath 文件地址
* @param name 文件显示名称
* @param sceneName 场景名
*/
static async createFileMessage(filePath: string, name?: string, sceneName?: string): Promise<V2NIMMessage> {
return ChatKitClient.nim.messageCreator.createFileMessage(filePath, name, sceneName);
}
/**
* 构造地理位置消息
*
* @param latitude 纬度
* @param longitude 经度
* @param address 详细位置信息
*/
static createLocationMessage(latitude: number, longitude: number, address: string): V2NIMMessage {
return ChatKitClient.nim.messageCreator.createLocationMessage(latitude, longitude, address);
}
/**
* 构造自定义消息消息
*
* @param text 文本
* @param rawAttachment 自定义的附件内容
*/
static createCustomMessage(text: string, rawAttachment: string): V2NIMMessage {
return ChatKitClient.nim.messageCreator.createCustomMessage(text, rawAttachment);
}
/**
* 构造转发消息,消息内容与原消息一样
*
* https://overmind.hz.netease.com/206/requirement/issues/OMIM-50521
*
* 其它端怕抛 error 导致应用崩溃,所以这里不抛 error而是返回 null
*
* @param message 需要转发的消息体
*/
static createForwardMessage(message: V2NIMMessage): V2NIMMessage | null {
return ChatKitClient.nim.messageCreator.createForwardMessage(message);
}
/**
* 构造提示消息
*
* @param text 提示文本
*/
static createTipsMessage(text: string): V2NIMMessage {
return ChatKitClient.nim.messageCreator.createTipsMessage(text);
}
/**
* 插入一条本地消息, 该消息不会发送。该消息不会多端同步,只是本端显示
* @param message 需要插入的消息体
* @param conversationId 会话 ID
*/
static saveLocalMessage(msg: V2NIMMessage, conversationId: string, senderId: string, createTime: number) {
ChatKitClient.nim.messageService?.insertMessageToLocal(msg, conversationId, senderId, createTime)
}
/**
* 查询点对点消息已读回执
* @param conversationId 会话 id
*/
static async getP2PMessageReceipt(conversationId: string): Promise<V2NIMP2PMessageReadReceipt | undefined> {
return await ChatKitClient.nim.messageService?.getP2PMessageReceipt(conversationId)
}
/**
* 获取群消息已读回执状态详情
*
* @param msg 需要查询已读回执状态的消息
* @param memberAccountIds 查找指定的账号列表已读未读
* 为空表示查询全部
* 同时作用于已读未读账号列表和人数
*/
static getTeamMessageReceiptDetail(msg: V2NIMMessage,
memberAccountIds?: string[]): Promise<V2NIMTeamMessageReadReceiptDetail> {
return ChatKitClient.nim.messageService!!.getTeamMessageReceiptDetail(
msg, memberAccountIds
)
}
/**
* 发送消息已读回执
*
* @param message 点对点会话收到的对方最后一条消息
*/
static async sendP2PMessageReceipt(message: V2NIMMessage): Promise<void> {
return ChatKitClient.nim.messageService?.sendP2PMessageReceipt(message)
}
/**
* 获取群消息已读回执状态
*
* @param messages 获取群消息已读回执状态. 限制一批最多 50 个且所有消息必须属于同一个会话
*/
static async getTeamMessageReceipts(messages: V2NIMMessage[]): Promise<V2NIMTeamMessageReadReceipt[] | undefined> {
return ChatKitClient.nim.messageService?.getTeamMessageReceipts(messages)
}
/**
* 发送群消息已读回执
*
* @param messages 需要发送已读回执的消息列表. 限制一批最多 50 个且所有消息必须属于同一个会话
*/
static async sendTeamMessageReceipt(messages: V2NIMMessage[]): Promise<void> {
return ChatKitClient.nim.messageService?.sendTeamMessageReceipts(messages)
}
}

View File

@ -0,0 +1,338 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMCheckFriendResult,
V2NIMFriend,
V2NIMFriendAddApplication,
V2NIMFriendAddApplicationQueryOption,
V2NIMFriendAddApplicationResult,
V2NIMFriendAddParams,
V2NIMFriendDeleteParams,
V2NIMFriendDeletionType,
V2NIMFriendSearchOption,
V2NIMFriendSetParams,
V2NIMLoginStatus,
V2NIMUser,
V2NIMUserSearchOption,
V2NIMUserUpdateParams
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
import { NEUserWithFriend } from '../model/NEUserWithFriend';
import { NEFriendUserCache } from '../NEFriendUserCache';
export class ContactRepo {
public static listener = getContext(this).eventHub
static onBlockListAddedFun = async (user: V2NIMUser) => {
NEFriendUserCache.getInstance().addBlockAccount(user.accountId)
}
static onLoginStatusFun = (status: V2NIMLoginStatus) => {
switch (status) {
case V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGOUT:
NEFriendUserCache.getInstance().removeAllFriendInfo()
NEFriendUserCache.mineUserCache = undefined
break;
default:
break;
}
}
static onUserProfileChangedFun = async (users: V2NIMUser[]) => {
users.forEach((user) => {
if (NEFriendUserCache.getInstance().isFriend(user.accountId)) {
NEFriendUserCache.getInstance().updateFriendInfo({
user: user
})
}
if (NEFriendUserCache.isMe(user.accountId) && NEFriendUserCache.mineUserCache) {
NEFriendUserCache.mineUserCache.user = user
}
})
ContactRepo.listener.emit('updateUserInfo', users)
}
static onBlockListRemovedFun = (accountId: string) => {
NEFriendUserCache.getInstance().removeBlockAccount(accountId)
}
static onFriendAddedFun = async (friend: V2NIMFriend) => {
NEFriendUserCache.getInstance().updateFriendInfo({
friend: friend
})
ContactRepo.removeUserFromBlockList(friend.accountId)
ContactRepo.listener.emit('updateFriendInfo', friend)
// 更新 user 信息至最新
ContactRepo.getUserListFromCloud([friend.accountId])
}
static onFriendDeletedFun = (accountId: string, deletionType: V2NIMFriendDeletionType) => {
NEFriendUserCache.getInstance().removeFriendInfo(accountId)
}
static onFriendInfoChangedFun = (friend: V2NIMFriend) => {
NEFriendUserCache.getInstance().updateFriendInfo({
friend: friend
})
ContactRepo.listener.emit('updateFriendInfo', friend)
}
static addListener() {
ChatKitClient.nim.loginService.on('onLoginStatus', ContactRepo.onLoginStatusFun)
ChatKitClient.nim.userService?.on('onUserProfileChanged', ContactRepo.onUserProfileChangedFun)
ChatKitClient.nim.userService?.on('onBlockListAdded', ContactRepo.onBlockListAddedFun)
ChatKitClient.nim.userService?.on('onBlockListRemoved', ContactRepo.onBlockListRemovedFun)
ChatKitClient.nim.friendService?.on('onFriendAdded', ContactRepo.onFriendAddedFun)
ChatKitClient.nim.friendService?.on('onFriendDeleted', ContactRepo.onFriendDeletedFun)
ChatKitClient.nim.friendService?.on('onFriendInfoChanged', ContactRepo.onFriendInfoChangedFun)
}
static removeListener() {
ChatKitClient.nim.loginService.off('onLoginStatus', ContactRepo.onLoginStatusFun)
ChatKitClient.nim.userService?.off('onUserProfileChanged', ContactRepo.onUserProfileChangedFun)
ChatKitClient.nim.userService?.off('onBlockListAdded', ContactRepo.onBlockListAddedFun)
ChatKitClient.nim.userService?.off('onBlockListRemoved', ContactRepo.onBlockListRemovedFun)
ChatKitClient.nim.friendService?.off('onFriendAdded', ContactRepo.onFriendAddedFun)
ChatKitClient.nim.friendService?.off('onFriendDeleted', ContactRepo.onFriendDeletedFun)
ChatKitClient.nim.friendService?.off('onFriendInfoChanged', ContactRepo.onFriendInfoChangedFun)
}
/**
* 根据用户账号列表获取用户资料
*
* @param accountIds 用户 Id 列表。最大为 150 个
*/
static async getUserList(accountIds: string[]): Promise<V2NIMUser[]> {
return await ChatKitClient.nim.userService?.getUserList(accountIds) ?? []
}
/**
* 根据用户账号列表获取用户资料-从云端获取
*
* 注: 其结果会更新本地数据, 建议在需要实时感知用户最新的信息的场景下使用
*
* @param accountIds 用户 Id 列表。最大为 150 个
*/
static async getUserListFromCloud(accountIds: string[]): Promise<V2NIMUser[]> {
const userList = await ChatKitClient.nim.userService?.getUserListFromCloud(accountIds) ?? []
let friendList: V2NIMUser[] = []
userList.forEach((user) => {
const accountId = user.accountId
if (accountId === ChatKitClient.getLoginUserId()) {
NEFriendUserCache.mineUserCache = new NEUserWithFriend({
user: user
})
}
if (NEFriendUserCache.getInstance().isFriend(accountId)) {
friendList.push(user)
NEFriendUserCache.getInstance().updateFriendInfo({
user: user
})
}
})
if (friendList.length > 0) {
ContactRepo.listener.emit('updateUserInfo', friendList)
}
return userList
}
/**
* 更新自己的用户资料。调用该 API 后,会触发 onUserProfileChanged 事件
*
* @param updateParams 更新参数
*/
static async updateSelfUserProfile(updateParams: V2NIMUserUpdateParams): Promise<void> {
await ChatKitClient.nim.userService?.updateSelfUserProfile(updateParams)
}
/**
* 添加用户到黑名单中
*
* @param accountId 用户 Id
*/
static async addUserToBlockList(accountId: string): Promise<void> {
await ChatKitClient.nim.userService?.addUserToBlockList(accountId)
}
/**
* 从黑名单中移除用户
*
* @param accountId 用户 Id
*/
static async removeUserFromBlockList(accountId: string): Promise<void> {
await ChatKitClient.nim.userService?.removeUserFromBlockList(accountId)
}
/**
* 获取黑名单列表
*/
static async getBlockList(): Promise<string[]> {
const blockList = await ChatKitClient.nim.userService?.getBlockList() ?? []
NEFriendUserCache.getInstance().initBlockAccountSet(blockList)
return blockList
}
/**
* 根据关键词搜索用户信息
*
* @param option 搜索选项
*/
static async searchUserByOption(option: V2NIMUserSearchOption): Promise<V2NIMUser[]> {
return await ChatKitClient.nim.userService?.searchUserByOption(option) ?? []
}
/**
* 添加/申请好友
* @param accountId 好友 ID
* @param params 申请相关参数
*/
static async addFriend(accountId: string, params: V2NIMFriendAddParams): Promise<void> {
await ChatKitClient.nim.friendService?.addFriend(accountId, params)
}
/**
* 删除好友
*
* @param accountId 好友 ID
* @param params 删除相关参数
*/
static async deleteFriend(accountId: string, params: V2NIMFriendDeleteParams): Promise<void> {
await ChatKitClient.nim.friendService?.deleteFriend(accountId, params)
}
/**
* 接受好友申请
*
* @param accountId 好友 ID
*/
static async acceptAddApplication(application: V2NIMFriendAddApplication): Promise<void> {
await ChatKitClient.nim.friendService?.acceptAddApplication(application)
}
/**
* 拒绝好友申请
*
* @param accountId 好友 ID
* @param postscript 拒绝时的附言
*/
static async rejectAddApplication(application: V2NIMFriendAddApplication, postscript?: string): Promise<void> {
await ChatKitClient.nim.friendService?.rejectAddApplication(application, postscript)
}
/**
* 清空所有好友申请
*
*/
static async clearAllAddApplication(): Promise<void> {
await ChatKitClient.nim.friendService?.clearAllAddApplication()
}
/**
* 设置好友信息
*
* @param accountId 好友 ID
* @param params 设置好友信息参数
*/
static async setFriendInfo(accountId: string, params: V2NIMFriendSetParams): Promise<void> {
await ChatKitClient.nim.friendService?.setFriendInfo(accountId, params)
}
/**
* 获取好友列表
*/
static async getFriendList(): Promise<V2NIMFriend[]> {
let friendList = await ChatKitClient.nim.friendService?.getFriendList() ?? []
if (AppStorage.get<NEFriendUserCache>(NEFriendUserCache.name)) {
NEFriendUserCache.getInstance().loadFriendList(friendList)
}
return friendList
}
/**
* 根据账号 ID 获取好友信息
*
* @param accountIds 好友 ID 列表
*/
static async getFriendByIds(accountIds: string[]): Promise<V2NIMFriend[]> {
return await ChatKitClient.nim.friendService?.getFriendByIds(accountIds) ?? []
}
/**
* 根据账号 ID 获取用户信息(包含好友信息)
*
* @param accountIds 用户 ID 列表
*/
static async getUserWithFriendByIds(accountIds: string[]): Promise<NEUserWithFriend[]> {
let userWithFriends: NEUserWithFriend[] = []
const users = await ContactRepo.getUserListFromCloud(accountIds)
const friends = await ContactRepo.getFriendByIds(accountIds)
let friendsMap: Map<string, NEUserWithFriend> = new Map<string, NEUserWithFriend>()
for (const friend of friends) {
if (friend.accountId) {
friendsMap.set(friend.accountId, new NEUserWithFriend({
friend: friend
}))
}
}
for (const user of users) {
const friend = friendsMap.get(user.accountId)?.friend
let userWithFriend: NEUserWithFriend = new NEUserWithFriend({
friend: friend,
user: user
})
if (friend) {
NEFriendUserCache.getInstance().updateFriendInfo({
friendUser: userWithFriend
})
}
userWithFriends.push(userWithFriend)
}
return userWithFriends
}
/**
* 根据账号 ID 检查好友状态
* @param accountIds 好友 ID列表
*/
static async checkFriend(accountIds: string[]): Promise<V2NIMCheckFriendResult | undefined> {
return await ChatKitClient.nim.friendService?.checkFriend(accountIds)
}
/**
* 获取申请添加好友列表通知
*/
static async getAddApplicationList(option: V2NIMFriendAddApplicationQueryOption): Promise<V2NIMFriendAddApplicationResult | undefined> {
return await ChatKitClient.nim.friendService?.getAddApplicationList(option)
}
/**
* 设置好友申请已读
*/
static async setAddApplicationRead(): Promise<void> {
await ChatKitClient.nim.friendService?.setAddApplicationRead()
}
/**
* 获取未读的好友申请数量
*/
static async getAddApplicationUnreadCount(): Promise<number> {
return await ChatKitClient.nim.friendService?.getAddApplicationUnreadCount() ?? 0
}
/**
* 根据关键词搜索好友
* @param option 搜索好友的条件
*/
static async searchFriendByOption(option: V2NIMFriendSearchOption): Promise<V2NIMFriend[]> {
return await ChatKitClient.nim.friendService?.searchFriendByOption(option) ?? []
}
}

View File

@ -0,0 +1,282 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMConversation,
V2NIMConversationFilter,
V2NIMConversationOperationResult,
V2NIMConversationOption,
V2NIMConversationResult,
V2NIMConversationType,
V2NIMConversationUpdate
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
export class ConversationRepo {
/**
* 获取会话列表
*
* @param offset 分页偏移量. 首页应传 0, 其他页数据使用返回的 offset
* @param limit 分页拉取数量,不建议超过 100
*/
static async getConversationList(offset: number, limit: number): Promise<V2NIMConversationResult | null> {
return await ChatKitClient.nim.conversationService?.getConversationList(offset, limit) ?? null
}
/**
* 置顶会话
*
* 注: 在操作成功且是有效的操作时, 则触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* @param conversationId 会话 id
* @param stickTop 是否置顶. true: 置顶, false: 取消置顶.
*/
static async stickTopConversation(conversationId: string, stickTop: boolean): Promise<void> {
return await ChatKitClient.nim.conversationService?.stickTopConversation(conversationId, stickTop)
}
/**
* 删除会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationDeleted | V2NIMConversationListener.onConversationCreated}
*
* @param conversationId 会话 id
* @param clearMessage 是否删除会话对应的历史消息. 默认为 false
*/
static async deleteConversation(conversationId: string, clearMessage?: boolean): Promise<void> {
return await ChatKitClient.nim.conversationService?.deleteConversation(conversationId, clearMessage)
}
/**
* 获取会话列表. 可以指定筛选条件,按会话类型,未读等
*
* @param offset 会话标记. 首页应传 0, 其他页数据使用返回的 offset
* @param limit 分页拉取数量, 不建议超过100
* @param option 查询选项
*
*/
static async getConversationListByOption(offset: number, limit: number,
option: V2NIMConversationOption): Promise<V2NIMConversationResult | undefined> {
return await ChatKitClient.nim.conversationService?.getConversationListByOption(offset, limit, option)
}
/**
* 根据会话 id 获取单条会话
*
* @param conversationId 会话 id
*/
static async getConversation(conversationId: string): Promise<V2NIMConversation | undefined> {
return await ChatKitClient.nim.conversationService?.getConversation(conversationId)
}
/**
* 根据会话 id 获取会话列表
*
* @param conversationIds 会话 id 列表
*
*/
static async getConversationListByIds(conversationIds: string[]): Promise<V2NIMConversation[] | undefined> {
return await ChatKitClient.nim.conversationService?.getConversationListByIds(conversationIds)
}
/**
* 创建会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationCreated | V2NIMConversationListener.onConversationCreated}
*
* @param conversationId 会话 id
*
*/
static async createConversation(conversationId: string): Promise<V2NIMConversation | undefined> {
return await ChatKitClient.nim.conversationService?.createConversation(conversationId)
}
/**
* 批量删除会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationDeleted | V2NIMConversationListener.onConversationDeleted}
*
* @param conversationIds 会话 id 列表
* @param clearMessage 是否删除会话对应的历史消息. 默认为 false
* @returns 返回操作失败的列表,列表的对象包含会话 id 以及错误信息.
*/
static async deleteConversationListByIds(conversationIds: string[],
clearMessage?: boolean): Promise<V2NIMConversationOperationResult[] | undefined> {
return await ChatKitClient.nim.conversationService?.deleteConversationListByIds(conversationIds, clearMessage)
}
/**
* 更新会话
*
* 注: 在操作成功且是有效的操作时, 触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* @param conversationId 会话 id
* @param updateInfo 欲更新的信息
*
*/
static async updateConversation(conversationId: string, updateInfo: V2NIMConversationUpdate): Promise<void> {
return await ChatKitClient.nim.conversationService?.updateConversation(conversationId, updateInfo)
}
/**
* 更新会话的本地扩展字段
*
* 注: 在操作成功且是有效的操作时, 触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* 注2: 字段只能存在内存里, 不能持久化存储. 登出或者重新初始化后 localExtension 都会再次成为空字符串.
*
* @param conversationId 会话 id
* @param localExtension 本地扩展信息
*
*/
static async updateConversationLocalExtension(conversationId: string, localExtension: string): Promise<void> {
return await ChatKitClient.nim.conversationService?.updateConversationLocalExtension(conversationId, localExtension)
}
/**
* 获取全部会话的总的未读数
*
*/
static getTotalUnreadCount(): number | undefined {
return ChatKitClient.nim.conversationService?.getTotalUnreadCount()
}
/**
* 根据 id 列表获取会话的未读数
*
* @param conversationIds 会话 id 列表
*
*/
static async getUnreadCountByIds(conversationIds: string[]): Promise<number | undefined> {
return await ChatKitClient.nim.conversationService?.getUnreadCountByIds(conversationIds)
}
/**
* 根据过滤参数获取相应的未读信息
*
* @param filter 过滤条件
*/
static async getUnreadCountByFilter(filter: V2NIMConversationFilter): Promise<number | undefined> {
return await ChatKitClient.nim.conversationService?.getUnreadCountByFilter(filter)
}
/**
* 清空所有会话总的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*/
static async clearTotalUnreadCount(): Promise<void> {
return await ChatKitClient.nim.conversationService?.clearTotalUnreadCount()
}
/**
* 根据会话 id 列表清空相应会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param conversationIds 会话 id 列表
* @returns 返回操作失败结果的列表
*/
static async clearUnreadCountByIds(conversationIds: string[]): Promise<V2NIMConversationOperationResult[] | undefined> {
return await ChatKitClient.nim.conversationService?.clearUnreadCountByIds(conversationIds)
}
/**
* 清除对应指定分组下的会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param groupId 指定的会话分组 id
*/
static async clearUnreadCountByGroupId(groupId: string): Promise<void> {
return await ChatKitClient.nim.conversationService?.clearUnreadCountByGroupId(groupId)
}
/**
* 清除对应指定类型下的会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param types 指定的会话类型列表
*/
static async clearUnreadCountByTypes(types: V2NIMConversationType[]): Promise<void> {
return await ChatKitClient.nim.conversationService?.clearUnreadCountByTypes(types)
}
/**
* 订阅指定过滤条件的会话未读数变化
*
* 注1: 当订阅该条件后,该 filter 下的未读数发生变化时, 触发 {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter} 事件
*
* 注2: 同一种 filter 只能被订阅一次, 第二次的调用不会有任何效果
*
* @param filter 过滤条件
*/
static subscribeUnreadCountByFilter(filter: V2NIMConversationFilter) {
return ChatKitClient.nim.conversationService?.subscribeUnreadCountByFilter(filter)
}
/**
* 取消订阅指定过滤条件的会话未读变化
*
* @param filter 过滤条件
*/
static unsubscribeUnreadCountByFilter(filter: V2NIMConversationFilter) {
return ChatKitClient.nim.conversationService?.unsubscribeUnreadCountByFilter(filter)
}
/**
* 标记会话已读时间戳
*
* 注: 当该方法调用后SDK 可能给多端账户抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationReadTimeUpdated | V2NIMConversationListener.onConversationReadTimeUpdated} <br/>
*
*/
static async markConversationRead(conversationId: string): Promise<number | undefined> {
return await ChatKitClient.nim.conversationService?.markConversationRead(conversationId)
}
/**
* 获取会话已读时间戳。该时间包含多端已读时间戳
*/
static async getConversationReadTime(conversationId: string): Promise<number | undefined> {
return await ChatKitClient.nim.conversationService?.getConversationReadTime(conversationId)
}
/**
* 移除会话的所有监听
*/
static removeAllConversationListener() {
//移除
ChatKitClient.nim.conversationService?.removeAllListeners('onSyncFinished')
//会话创建
ChatKitClient.nim.conversationService?.removeAllListeners("onConversationCreated")
//会话删除
ChatKitClient.nim.conversationService?.removeAllListeners("onConversationDeleted")
//会话更新
ChatKitClient.nim.conversationService?.removeAllListeners("onConversationChanged")
}
}

View File

@ -0,0 +1,282 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMConversationType,
V2NIMLocalConversation,
V2NIMLocalConversationFilter,
V2NIMLocalConversationOperationResult,
V2NIMLocalConversationOption,
V2NIMLocalConversationResult,
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
export class LocalConversationRepo {
/**
* 获取会话列表
*
* @param offset 分页偏移量. 首页应传 0, 其他页数据使用返回的 offset
* @param limit 分页拉取数量,不建议超过 100
*/
static async getConversationList(offset: number, limit: number): Promise<V2NIMLocalConversationResult | null> {
return await ChatKitClient.nim.localConversationService?.getConversationList(offset, limit) ?? null
}
/**
* 置顶会话
*
* 注: 在操作成功且是有效的操作时, 则触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* @param conversationId 会话 id
* @param stickTop 是否置顶. true: 置顶, false: 取消置顶.
*/
static async stickTopConversation(conversationId: string, stickTop: boolean): Promise<void> {
return await ChatKitClient.nim.localConversationService?.stickTopConversation(conversationId, stickTop)
}
/**
* 删除会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationDeleted | V2NIMConversationListener.onConversationCreated}
*
* @param conversationId 会话 id
* @param clearMessage 是否删除会话对应的历史消息. 默认为 false
*/
static async deleteConversation(conversationId: string, clearMessage?: boolean): Promise<void> {
return await ChatKitClient.nim.localConversationService?.deleteConversation(conversationId, clearMessage)
}
/**
* 获取会话列表. 可以指定筛选条件,按会话类型,未读等
*
* @param offset 会话标记. 首页应传 0, 其他页数据使用返回的 offset
* @param limit 分页拉取数量, 不建议超过100
* @param option 查询选项
*
*/
static async getConversationListByOption(offset: number, limit: number,
option: V2NIMLocalConversationOption): Promise<V2NIMLocalConversationResult | undefined> {
return await ChatKitClient.nim.localConversationService?.getConversationListByOption(offset, limit, option)
}
/**
* 根据会话 id 获取单条会话
*
* @param conversationId 会话 id
*/
static async getConversation(conversationId: string): Promise<V2NIMLocalConversation | undefined> {
return await ChatKitClient.nim.localConversationService?.getConversation(conversationId)
}
/**
* 根据会话 id 获取会话列表
*
* @param conversationIds 会话 id 列表
*
*/
static async getConversationListByIds(conversationIds: string[]): Promise<V2NIMLocalConversation[] | undefined> {
return await ChatKitClient.nim.localConversationService?.getConversationListByIds(conversationIds)
}
/**
* 创建会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationCreated | V2NIMConversationListener.onConversationCreated}
*
* @param conversationId 会话 id
*
*/
static async createConversation(conversationId: string): Promise<V2NIMLocalConversation | undefined> {
return await ChatKitClient.nim.localConversationService?.createConversation(conversationId)
}
/**
* 批量删除会话
*
* 注: 在操作成功且是有效的操作时, 会抛出事件 {@link V2NIMConversationListener.onConversationDeleted | V2NIMConversationListener.onConversationDeleted}
*
* @param conversationIds 会话 id 列表
* @param clearMessage 是否删除会话对应的历史消息. 默认为 false
* @returns 返回操作失败的列表,列表的对象包含会话 id 以及错误信息.
*/
static async deleteConversationListByIds(conversationIds: string[],
clearMessage?: boolean): Promise<V2NIMLocalConversationOperationResult[] | undefined> {
return await ChatKitClient.nim.localConversationService?.deleteConversationListByIds(conversationIds, clearMessage)
}
/**
* 更新会话
*
* 注: 在操作成功且是有效的操作时, 触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* @param conversationId 会话 id
* @param updateInfo 欲更新的信息
*
*/
// static async updateConversation(conversationId: string, updateInfo: V2NIMLocalConversationUpdate): Promise<void> {
// return await ChatKitClient.nim.localConversationService?.updateConversation(conversationId, updateInfo)
// }
/**
* 更新会话的本地扩展字段
*
* 注: 在操作成功且是有效的操作时, 触发事件 {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged}
*
* 注2: 字段只能存在内存里, 不能持久化存储. 登出或者重新初始化后 localExtension 都会再次成为空字符串.
*
* @param conversationId 会话 id
* @param localExtension 本地扩展信息
*
*/
static async updateConversationLocalExtension(conversationId: string, localExtension: string): Promise<void> {
return await ChatKitClient.nim.localConversationService?.updateConversationLocalExtension(conversationId,
localExtension)
}
/**
* 获取全部会话的总的未读数
*
*/
static getTotalUnreadCount(): number | undefined {
return ChatKitClient.nim.localConversationService?.getTotalUnreadCount()
}
/**
* 根据 id 列表获取会话的未读数
*
* @param conversationIds 会话 id 列表
*
*/
static async getUnreadCountByIds(conversationIds: string[]): Promise<number | undefined> {
return await ChatKitClient.nim.localConversationService?.getUnreadCountByIds(conversationIds)
}
/**
* 根据过滤参数获取相应的未读信息
*
* @param filter 过滤条件
*/
static async getUnreadCountByFilter(filter: V2NIMLocalConversationFilter): Promise<number | undefined> {
return await ChatKitClient.nim.localConversationService?.getUnreadCountByFilter(filter)
}
/**
* 清空所有会话总的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*/
static async clearTotalUnreadCount(): Promise<void> {
return await ChatKitClient.nim.localConversationService?.clearTotalUnreadCount()
}
/**
* 根据会话 id 列表清空相应会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param conversationIds 会话 id 列表
* @returns 返回操作失败结果的列表
*/
static async clearUnreadCountByIds(conversationIds: string[]): Promise<V2NIMLocalConversationOperationResult[] | undefined> {
return await ChatKitClient.nim.localConversationService?.clearUnreadCountByIds(conversationIds)
}
/**
* 清除对应指定分组下的会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param groupId 指定的会话分组 id
*/
// static async clearUnreadCountByGroupId(groupId: string): Promise<void> {
// return await ChatKitClient.nim.localConversationService?.clearUnreadCountByGroupId(groupId)
// }
/**
* 清除对应指定类型下的会话的未读数
*
* 注: 当该方法调用后SDK 可能给开发者抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationChanged | V2NIMConversationListener.onConversationChanged} <br/>
* {@link V2NIMConversationListener.onTotalUnreadCountChanged | V2NIMConversationListener.onTotalUnreadCountChanged} <br/>
* {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter}
*
* @param types 指定的会话类型列表
*/
static async clearUnreadCountByTypes(types: V2NIMConversationType[]): Promise<void> {
return await ChatKitClient.nim.localConversationService?.clearUnreadCountByTypes(types)
}
/**
* 订阅指定过滤条件的会话未读数变化
*
* 注1: 当订阅该条件后,该 filter 下的未读数发生变化时, 触发 {@link V2NIMConversationListener.onUnreadCountChangedByFilter | V2NIMConversationListener.onUnreadCountChangedByFilter} 事件
*
* 注2: 同一种 filter 只能被订阅一次, 第二次的调用不会有任何效果
*
* @param filter 过滤条件
*/
static subscribeUnreadCountByFilter(filter: V2NIMLocalConversationFilter) {
return ChatKitClient.nim.localConversationService?.subscribeUnreadCountByFilter(filter)
}
/**
* 取消订阅指定过滤条件的会话未读变化
*
* @param filter 过滤条件
*/
static unsubscribeUnreadCountByFilter(filter: V2NIMLocalConversationFilter) {
return ChatKitClient.nim.localConversationService?.unsubscribeUnreadCountByFilter(filter)
}
/**
* 标记会话已读时间戳
*
* 注: 当该方法调用后SDK 可能给多端账户抛出以下的事件
*
* {@link V2NIMConversationListener.onConversationReadTimeUpdated | V2NIMConversationListener.onConversationReadTimeUpdated} <br/>
*
*/
static async markConversationRead(conversationId: string): Promise<number | undefined> {
return await ChatKitClient.nim.localConversationService?.markConversationRead(conversationId)
}
/**
* 获取会话已读时间戳。该时间包含多端已读时间戳
*/
static async getConversationReadTime(conversationId: string): Promise<number | undefined> {
return await ChatKitClient.nim.localConversationService?.getConversationReadTime(conversationId)
}
/**
* 移除会话的所有监听
*/
static removeAllConversationListener() {
//移除
ChatKitClient.nim.localConversationService?.removeAllListeners('onSyncFinished')
//会话创建
ChatKitClient.nim.localConversationService?.removeAllListeners("onConversationCreated")
//会话删除
ChatKitClient.nim.localConversationService?.removeAllListeners("onConversationDeleted")
//会话更新
ChatKitClient.nim.localConversationService?.removeAllListeners("onConversationChanged")
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { V2NIMDndConfig, V2NIMP2PMessageMuteMode, V2NIMTeamMessageMuteMode, V2NIMTeamType } from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
export class SettingRepo {
/**
* 获取会话消息免打扰状态
*
* 注: 若会话类型为群, 则群消息免打扰模式为 {@link V2NIMTeamMessageMuteMode.V2NIM_TEAM_MESSAGE_MUTE_MODE_OFF | V2NIMTeamMessageMuteMode.V2NIM_TEAM_MESSAGE_MUTE_MODE_OFF} 返回为false. 其他的返回 true.
*
* @param conversationId 会话 id
* @return mute 是否被免打扰
*/
static async getConversationMuteStatus(conversationId: string): Promise<boolean | undefined> {
return await ChatKitClient.nim.settingService?.getConversationMuteStatus(conversationId)
}
/**
* 设置群消息免打扰模式
*
* @param teamId 群组ID
* @param teamType 群组类型
* @param muteMode 群消息免打扰模式
*/
static async setTeamMessageMuteMode(teamId: string, teamType: V2NIMTeamType,
muteMode: V2NIMTeamMessageMuteMode): Promise<void> {
await ChatKitClient.nim.settingService?.setTeamMessageMuteMode(teamId, teamType, muteMode)
}
/**
* 获取群消息免打扰模式
*
* @param teamId 群组ID
* @param teamType 群组类型
* @return muteMode 群消息免打扰模式
*/
static async getTeamMessageMuteMode(teamId: string,
teamType: V2NIMTeamType): Promise<V2NIMTeamMessageMuteMode | undefined> {
return await ChatKitClient.nim.settingService?.getTeamMessageMuteMode(teamId, teamType)
}
/**
* 设置点对点消息免打扰模式
*
* @param accountId 目标账号 ID
* @param muteMode 设置用户的免打扰模式
*/
static async setP2PMessageMuteMode(accountId: string, muteMode: V2NIMP2PMessageMuteMode): Promise<void> {
await ChatKitClient.nim.settingService?.setP2PMessageMuteMode(accountId, muteMode)
}
/**
* 获取用户消息免打扰模式
*
* @param accountId 目标账号 ID
* @return muteMode p2p 类型消息免打扰模式
*/
static async getP2PMessageMuteMode(accountId: string): Promise<V2NIMP2PMessageMuteMode | undefined> {
return await ChatKitClient.nim.settingService?.getP2PMessageMuteMode(accountId)
}
/**
* 获取点对点消息免打扰列表。
*
* 返回 V2NIMP2PMessageMuteMode 为 V2NIM_P2P_MESSAGE_MUTE_MODE_ON 的 accountId 列表。
*/
static async getP2PMessageMuteList(): Promise<string[]> {
return await ChatKitClient.nim.settingService?.getP2PMessageMuteList() ?? []
}
/**
* 设置当桌面端在线时,移动端是否需要推送
*
* @param need 桌面端在线时,移动端是否需要推送
*/
static async setPushMobileOnDesktopOnline(need: boolean): Promise<void> {
await ChatKitClient.nim.settingService?.setPushMobileOnDesktopOnline(need)
}
/**
* 设置Apns免打扰与详情显示
*
* @param config 免打扰与详情配置参数
*/
static async setDndConfig(config: V2NIMDndConfig): Promise<void> {
await ChatKitClient.nim.settingService?.setDndConfig(config)
}
/**
* 获取Apns免打扰与详情显示
*
* @return 免打扰与详情配置参数
*/
static async getDndConfig(): Promise<V2NIMDndConfig | undefined> {
return await ChatKitClient.nim.settingService?.getDndConfig()
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMGetMediaResourceInfoResult,
V2NIMMessageAttachment,
V2NIMProgressCallback,
V2NIMSize,
V2NIMStorageScene,
V2NIMUploadFileParams,
V2NIMUploadFileTask
} from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
export class StorageRepo {
/**
* 设置自定义场景
*
* @param sceneName 自定义的场景名
* @param expireTime 文件过期时间. 单位秒. 数值要求大于等于 86400 秒, 即 1 天.
*/
static addCustomStorageScene(sceneName: string, expireTime: number): V2NIMStorageScene | undefined {
return ChatKitClient.nim.storageService?.addCustomStorageScene(sceneName, expireTime)
}
/**
* 查询存储场景列表
*/
static getStorageSceneList(): V2NIMStorageScene[] {
return ChatKitClient.nim.storageService?.getStorageSceneList() ?? []
}
/**
* 创建文件上传任务
*
* @param fileParams 上传文件参数
* @returns 上传任务
*/
static createUploadFileTask(fileParams: V2NIMUploadFileParams): V2NIMUploadFileTask | null {
return ChatKitClient.nim.storageService?.createUploadFileTask(fileParams) ?? null
}
/**
* 上传文件
*
* @param fileTask 上传任务createUploadTask 函数返回值
* @returns 文件的 url
*/
static async uploadFile(fileTask: V2NIMUploadFileTask, progress: V2NIMProgressCallback): Promise<string | undefined> {
return await ChatKitClient.nim.storageService?.uploadFile(fileTask, progress)
}
/**
* 取消文件上传
*
* @param fileTask 上传任务createUploadTask 函数返回值
*/
static async cancelUploadFile(fileTask: V2NIMUploadFileTask): Promise<void> {
return await ChatKitClient.nim.storageService?.cancelUploadFile(fileTask)
}
/**
* 下载文件
*
* @param url 下载文件 url
* @param filePath 文件下载存放的本地路径
* @returns 文件的 url
*/
static async downloadFile(url: string, filePath: string,
progress: V2NIMProgressCallback): Promise<string | undefined> {
return await ChatKitClient.nim.storageService?.downloadFile(url, filePath, progress)
}
/**
* 取消文件下载
*
* @param url 下载文件 url
* @returns
*/
static async cancelDownloadFile(url: string): Promise<void> {
return await ChatKitClient.nim.storageService?.cancelDownloadFile(url)
}
/**
* 短链接转长链接
*
* @param url 文件远程地址
* @returns 文件的 url
*/
static async shortUrlToLong(url: string): Promise<string | undefined> {
return await ChatKitClient.nim.storageService?.shortUrlToLong(url)
}
/**
* 生成图片缩略链接
*/
static async getImageThumbUrl(attachment: V2NIMMessageAttachment, thumbSize: V2NIMSize): Promise<V2NIMGetMediaResourceInfoResult | undefined> {
return await ChatKitClient.nim.storageService?.getImageThumbUrl(attachment, thumbSize)
}
}

View File

@ -0,0 +1,649 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMAntispamConfig,
V2NIMCreateTeamParams,
V2NIMCreateTeamResult,
V2NIMTeam,
V2NIMTeamAgreeMode,
V2NIMTeamChatBannedMode,
V2NIMTeamInviteMode,
V2NIMTeamJoinActionInfo,
V2NIMTeamJoinActionInfoQueryOption,
V2NIMTeamJoinActionInfoResult,
V2NIMTeamJoinMode,
V2NIMTeamMember,
V2NIMTeamMemberListResult,
V2NIMTeamMemberQueryOption,
V2NIMTeamMemberRole,
V2NIMTeamMemberSearchOption,
V2NIMTeamMemberSearchResult,
V2NIMTeamType,
V2NIMTeamUpdateExtensionMode,
V2NIMTeamUpdateInfoMode,
V2NIMUpdateSelfMemberInfoParams,
V2NIMUpdateTeamInfoParams,
V2NIMUser
} from '@nimsdk/base'
import { ChatKitClient } from '../ChatKitClient'
import { TeamMemberResult, TeamMemberWithUser } from '../model/TeamMemberWithUser'
import { NEFriendUserCache } from '../NEFriendUserCache'
import { ContactRepo } from './ContactRepo'
interface TeamExtension {
im_ui_kit_group: boolean
}
export class TeamRepo {
//默认群头像
public static teamDefaultIcons = [
"https://s.netease.im/safe/ABg8YjWQWvcqO6sAAAAAAAAAAAA?_im_url=1",
"https://s.netease.im/safe/ABg8YjmQWvcqO6sAAAAAAAABAAA?_im_url=1",
"https://s.netease.im/safe/ABg8YjyQWvcqO6sAAAAAAAABAAA?_im_url=1",
"https://s.netease.im/safe/ABg8YkCQWvcqO6sAAAAAAAABAAA?_im_url=1",
"https://s.netease.im/safe/ABg8YkSQWvcqO6sAAAAAAAABAAA?_im_url=1"
]
/**
* 创建讨论组
* @param inviteeAccountIds
* @param postscript
* @param antispamConfig
* @returns
*/
static async createGroupTeam(
inviteeAccountIds?: string[],
postscript?: string,
antispamConfig?: V2NIMAntispamConfig): Promise<V2NIMCreateTeamResult | undefined> {
let createParams: V2NIMCreateTeamParams = {
name: TeamRepo.getTeamNameByMemberId(inviteeAccountIds),
teamType: V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL,
joinMode: V2NIMTeamJoinMode.V2NIM_TEAM_JOIN_MODE_FREE,
inviteMode: V2NIMTeamInviteMode.V2NIM_TEAM_INVITE_MODE_ALL,
agreeMode: V2NIMTeamAgreeMode.V2NIM_TEAM_AGREE_MODE_NO_AUTH,
updateInfoMode: V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_ALL,
updateExtensionMode: V2NIMTeamUpdateExtensionMode.V2NIM_TEAM_UPDATE_EXTENSION_MODE_ALL,
serverExtension: JSON.stringify({ 'im_ui_kit_group': true }),
avatar: TeamRepo.teamDefaultIcons[Math.floor(Math.random() * 5)]
}
return TeamRepo.createTeam(createParams,
inviteeAccountIds, postscript, antispamConfig)
}
/**
* 判断是否为讨论组
* @param team
* @returns
*/
static isGroupTeam(team: V2NIMTeam) {
if (team.serverExtension && team.serverExtension.length > 0) {
try {
let obj = JSON.parse(team.serverExtension) as object | undefined
if (obj && obj['im_ui_kit_group'] === true) {
return true
}
} catch (e) {
console.error('TeamRepo isGroupTeam json parse error')
}
}
return false
}
/**
* 创建高级群
* @param inviteeAccountIds
* @param postscript
* @param antispamConfig
* @returns
*/
static async createAdvanceTeam(
inviteeAccountIds?: string[],
postscript?: string,
antispamConfig?: V2NIMAntispamConfig): Promise<V2NIMCreateTeamResult | undefined> {
let createParams: V2NIMCreateTeamParams = {
name: TeamRepo.getTeamNameByMemberId(inviteeAccountIds),
teamType: V2NIMTeamType.V2NIM_TEAM_TYPE_NORMAL,
joinMode: V2NIMTeamJoinMode.V2NIM_TEAM_JOIN_MODE_FREE,
inviteMode: V2NIMTeamInviteMode.V2NIM_TEAM_INVITE_MODE_MANAGER,
agreeMode: V2NIMTeamAgreeMode.V2NIM_TEAM_AGREE_MODE_NO_AUTH,
updateInfoMode: V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_MANAGER,
updateExtensionMode: V2NIMTeamUpdateExtensionMode.V2NIM_TEAM_UPDATE_EXTENSION_MODE_MANAGER,
avatar: TeamRepo.teamDefaultIcons[Math.floor(Math.random() * 5)]
}
return TeamRepo.createTeam(createParams,
inviteeAccountIds, postscript, antispamConfig)
}
/**
* 创建一个群组
*
* 注: 操作成功后, 触发事件的规则如下:
* - 操作者端(群主), SDK 抛出: {@link V2NIMTeamListener.onTeamCreated | V2NIMTeamListener.onTeamCreated}
* - agreeMode 需要被邀请者同意
* - 被操作者端, SDK会抛出: {@link V2NIMTeamListener.onReceiveTeamJoinActionInfo | V2NIMTeamListener.onReceiveTeamJoinActionInfo}
* - agreeMode 不需被邀请者同意
* - 被操作者端, SDK会抛出: {@link V2NIMTeamListener.onTeamJoined | V2NIMTeamListener.onTeamJoined}
* - 其他成员端, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberJoined | V2NIMTeamListener.onTeamMemberJoined}
*
* @param createTeamParams 创建群组参数
* @param invitorAccountIds 群组创建时,同时邀请加入群的成员列表
* @param postscript 群组创建时,邀请入群的附言
* @param antispamConfig 反垃圾参数. 如果开启了安全通,默认采用安全通,该配置不需要配置.
* 如果有审核需求,且直接对接易盾,则需要传入该配置
*/
static async createTeam(createTeamParams: V2NIMCreateTeamParams,
inviteeAccountIds?: string[],
postscript?: string,
antispamConfig?: V2NIMAntispamConfig): Promise<V2NIMCreateTeamResult | undefined> {
return await ChatKitClient.nim.teamService?.createTeam(createTeamParams, inviteeAccountIds, postscript,
antispamConfig)
}
/**
* 修改群组信息
*
* 注: 操作成功后, 触发事件的规则如下:
* - 全员用户端SDK会抛出: {@link V2NIMTeamListener.onTeamInfoUpdated | V2NIMTeamListener.onTeamInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param updateTeamInfoParams 更新群组信息参数
* @param antispamConfig 反垃圾参数. 如果开启了安全通,默认采用安全通,该配置不需要配置.
* 如果有审核需求,且直接对接易盾,则需要传入该配置
*/
static async updateTeamInfo(teamId: string,
teamType: V2NIMTeamType,
updateTeamInfoParams: V2NIMUpdateTeamInfoParams,
antispamConfig?: V2NIMAntispamConfig): Promise<void> {
await ChatKitClient.nim.teamService?.updateTeamInfo(teamId, teamType, updateTeamInfoParams, antispamConfig)
}
/**
* 退出群组
*
* 注: 操作成功后, 触发事件的规则如下:
* - 操作者自己本端SDK会抛出: {@link V2NIMTeamListener.onTeamLeft | V2NIMTeamListener.onTeamLeft}
* - 群内其它用户端, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberLeft | V2NIMTeamListener.onTeamMemberLeft}
*
* @param teamId 群组id
* @param teamType 群组类型
*/
static async leaveTeam(teamId: string, teamType: V2NIMTeamType): Promise<void> {
await ChatKitClient.nim.teamService?.leaveTeam(teamId, teamType)
}
/**
* 获取群组信息
*
* @param teamId 群组id
* @param teamType 群组类型
*/
static async getTeamInfo(teamId: string, teamType: V2NIMTeamType): Promise<V2NIMTeam | undefined> {
return await ChatKitClient.nim.teamService?.getTeamInfo(teamId, teamType)
}
/**
* 获取当前已经加入的群组列表
*
* 注: 群组有效且自己在群中
*
* @param teamTypes 群类型列表. 若不传入这个字段, 代表这个过滤条件不生效, 则查询所有群组
*/
static async getJoinedTeamList(teamTypes?: V2NIMTeamType[]): Promise<V2NIMTeam[]> {
return await ChatKitClient.nim.teamService?.getJoinedTeamList(teamTypes) ?? []
}
/**
* 获取当前已经加入的群组数量
*
* 注: 群组有效且自己在群中
*
* @param teamTypes 群类型列表. 若不传入这个字段, 代表这个过滤条件不生效, 则查询所有群组
*/
static async getJoinedTeamCount(teamTypes?: V2NIMTeamType[]): Promise<number | undefined> {
return await ChatKitClient.nim.teamService?.getJoinedTeamCount(teamTypes)
}
/**
* 根据群组ID获取群组信息
*
* 每次最多100个群组ID. 先查本地数据,本地缺失再查询云端
*
* @param teamIds 群组ID列表
* @param teamType 群组类型
*/
static async getTeamInfoByIds(teamIds: string[],
teamType: V2NIMTeamType): Promise<V2NIMTeam[]> {
return await ChatKitClient.nim.teamService?.getTeamInfoByIds(teamIds, teamType) ?? []
}
/**
* 解散群组
*
* 注: 操作成功后, 触发事件的规则如下:
* - 全员, SDK会抛出: {@link V2NIMTeamListener.onTeamDismissed | V2NIMTeamListener.onTeamDismissed}
*
* @param teamId 群组id
* @param teamType 群组类型
*/
static async dismissTeam(teamId: string, teamType: V2NIMTeamType): Promise<void> {
await ChatKitClient.nim.teamService?.dismissTeam(teamId, teamType)
}
/**
* 邀请成员加入群
*
* 注: 操作成功后, 触发事件的规则如下:
* - agreeMode 需要被邀请者同意
* - 被操作者端, SDK会抛出: {@link V2NIMTeamListener.onReceiveTeamJoinActionInfo | V2NIMTeamListener.onReceiveTeamJoinActionInfo}
* - agreeMode 不需要被邀请者同意
* - 被操作者端, SDK会抛出: {@link V2NIMTeamListener.onTeamJoined | V2NIMTeamListener.onTeamJoined}
* - 其他成员端, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberJoined | V2NIMTeamListener.onTeamMemberJoined}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param inviteeAccountIds 邀请加入群的成员账号列表
* @param postscript 邀请入群的附言
* @returns 邀请失败的账号列表
*/
static async inviteMember(teamId: string,
teamType: V2NIMTeamType,
inviteeAccountIds: string[],
postscript?: string): Promise<string[]> {
return await ChatKitClient.nim.teamService?.inviteMember(teamId, teamType, inviteeAccountIds, postscript) ?? []
}
/**
* 接受邀请入群
*
* 注: 操作成功后, 触发事件的规则如下:
* - 操作者(既接受邀请用户)端, SDK会抛出: {@link V2NIMTeamListener.onTeamJoined | V2NIMTeamListener.onTeamJoined}
* - 其他成员端, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberJoined | V2NIMTeamListener.onTeamMemberJoined}
*
* @param invitationInfo 邀请入群的信息
*/
static async acceptInvitation(invitationInfo: V2NIMTeamJoinActionInfo): Promise<V2NIMTeam | undefined> {
return await ChatKitClient.nim.teamService?.acceptInvitation(invitationInfo)
}
/**
* 拒绝邀请入群
*
* 注: 操作成功后, 触发事件的规则如下:
* - 群主或管理员端, SDK会抛出: {@link V2NIMTeamListener.onReceiveTeamJoinActionInfo | V2NIMTeamListener.onReceiveTeamJoinActionInfo}
*
* @param invitationInfo 邀请入群的信息
*/
static async rejectInvitation(invitationInfo: V2NIMTeamJoinActionInfo, postscript?: string): Promise<void> {
await ChatKitClient.nim.teamService?.rejectInvitation(invitationInfo)
}
/**
* 踢出群组成员
*
* 注1: 只有群主有权限操作改接口
*
* 注2: 操作成功后, 触发事件的规则如下:
* - 被操作者既被踢用户SDK会抛出: {@link V2NIMTeamListener.onTeamLeft | V2NIMTeamListener.onTeamLeft}
* - 其他成员端SDK会抛出: {@link V2NIMTeamListener.onTeamMemberKicked | V2NIMTeamListener.onTeamMemberKicked}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param memberAccountIds 踢出群组的成员账号列表
*/
static async kickMember(teamId: string, teamType: V2NIMTeamType, memberAccountIds: string[]): Promise<void> {
await ChatKitClient.nim.teamService?.kickMember(teamId, teamType, memberAccountIds)
}
/**
* (用户)申请加入群组
*
* 注: 操作成功后, 触发事件的规则如下:
* - joinMode 自由加入
* - 操作者端SDK 会抛出: {@link V2NIMTeamListener.onTeamJoined | V2NIMTeamListener.onTeamJoined}
* - 其他成员端, SDK 会抛出: {@link V2NIMTeamListener.onTeamMemberJoined | V2NIMTeamListener.onTeamMemberJoined}
* - joinMode 群主管理员同意
* - 群主或管理员端SDK 会抛出 {@link V2NIMTeamListener.onReceiveTeamJoinActionInfo | V2NIMTeamListener.onReceiveTeamJoinActionInfo}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param postscript 申请附言
* @returns 对应的群信息
*/
static async applyJoinTeam(teamId: string,
teamType: V2NIMTeamType,
postscript?: string): Promise<V2NIMTeam | undefined> {
return await ChatKitClient.nim.teamService?.applyJoinTeam(teamId, teamType, postscript)
}
/**
* (管理员)接受(用户的)入群申请
*
* 注: 操作成功后, 触发事件的规则如下:
* - 被操作者既被同意用户SDK会抛出: {@link V2NIMTeamListener.onTeamJoined | V2NIMTeamListener.onTeamJoined}
* - 其他成员, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberJoined | V2NIMTeamListener.onTeamMemberJoined}
*
* @param applicationInfo 该申请的相关信息
*/
static async acceptJoinApplication(applicationInfo: V2NIMTeamJoinActionInfo): Promise<void> {
await ChatKitClient.nim.teamService?.acceptJoinApplication(applicationInfo)
}
/**
* (管理员)拒绝(用户的)入群申请
*
* 注: 操作成功后, 触发事件的规则如下:
* - 被操作用户(既被拒绝用户), SDK会抛出: {@link V2NIMTeamListener.onReceiveTeamJoinActionInfo | V2NIMTeamListener.onReceiveTeamJoinActionInfo}
*
* @param applicationInfo 该申请的相关信息
*/
static async rejectJoinApplication(applicationInfo: V2NIMTeamJoinActionInfo, postscript?: string): Promise<void> {
await ChatKitClient.nim.teamService?.rejectJoinApplication(applicationInfo, postscript)
}
/**
* 设置成员角色
*
* 注1: 本操作只有群主可操作, 且只能在普通成员与管理员直接角色切换, 如果成员设置角色与当前角色一致,默认请求成功
*
* 注2: 操作成功后, 触发事件的规则如下:
* - 所有成员SDK会抛出: @link V2NIMTeamListener.onTeamMemberInfoUpdated | V2NIMTeamListener.onTeamMemberInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param memberAccountIds 待操作的群组的成员账号列表
* @param memberRole 新的角色类型
*/
static async updateTeamMemberRole(teamId: string,
teamType: V2NIMTeamType,
memberAccountIds: string[],
memberRole: V2NIMTeamMemberRole): Promise<void> {
await ChatKitClient.nim.teamService?.updateTeamMemberRole(teamId, teamType, memberAccountIds, memberRole)
}
/**
* 移交群主
*
* 注1: 本操作只有群主可操作
*
* 注2: 操作成功后, 触发事件的规则如下:
* - 所有成员SDK会抛出: {@link V2NIMTeamListener.onTeamInfoUpdated | V2NIMTeamListener.onTeamInfoUpdated}
* - 若入参 leave 为 true:
* - 操作者, SDK会抛出:onTeamLeft
* - 其它成员, SDK会抛出:onTeamMemberLeft
*
* @param teamId 群组id
* @param teamType 群组类型
* @param accountId 新群主的账号 ID
* @param leave 转让群主后, 操作者是否同时退出该群. 默认为 false
* @returns 该操作的时间戳
*/
static async transferTeamOwner(teamId: string,
teamType: V2NIMTeamType,
accountId: string,
leave?: boolean): Promise<void> {
await ChatKitClient.nim.teamService?.transferTeamOwner(teamId, teamType, accountId, leave)
}
/**
* 修改自己的群成员信息
*
* 注: 操作成功后, 触发事件的规则如下:
* - 所有成员, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberInfoUpdated | V2NIMTeamListener.onTeamMemberInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param memberInfoParams 被修改的字段
*/
static async updateSelfTeamMemberInfo(teamId: string,
teamType: V2NIMTeamType,
memberInfoParams: V2NIMUpdateSelfMemberInfoParams): Promise<void> {
await ChatKitClient.nim.teamService?.updateSelfTeamMemberInfo(teamId, teamType, memberInfoParams)
}
/**
* 修改群成员昵称
*
* 注: 只有群主和管理员拥有此权限可操作
*
* 注: 操作成功后, 触发事件的规则如下:
* - 所有成员SDK会抛出: {@link V2NIMTeamListener.onTeamMemberInfoUpdated | V2NIMTeamListener.onTeamMemberInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param accountId 新群主的账号 ID
* @param nick 昵称
*/
static async updateTeamMemberNick(teamId: string,
teamType: V2NIMTeamType,
accountId: string,
nick: string): Promise<void> {
await ChatKitClient.nim.teamService?.updateTeamMemberNick(teamId, teamType, accountId, nick)
}
/**
* 设置群组禁言模式
*
* 注: 操作成功后, 触发事件的规则如下:
* - 所有成员SDK会抛出: {@link V2NIMTeamListener.onTeamInfoUpdated | V2NIMTeamListener.onTeamInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param chatBannedMode 禁言模式
*/
static async setTeamChatBannedMode(teamId: string,
teamType: V2NIMTeamType,
chatBannedMode: V2NIMTeamChatBannedMode): Promise<void> {
await ChatKitClient.nim.teamService?.setTeamChatBannedMode(teamId, teamType, chatBannedMode)
}
/**
* 设置群组成员聊天禁言状态
*
* 注: 操作成功后, 触发事件的规则如下:
* - 所有成员, SDK会抛出: {@link V2NIMTeamListener.onTeamMemberInfoUpdated | V2NIMTeamListener.onTeamMemberInfoUpdated}
*
* @param teamId 群组id
* @param teamType 群组类型
* @param accountId 被修改成员的账号
* @param chatBanned 群组中聊天是否被禁言
*/
static async setTeamMemberChatBannedStatus(teamId: string, teamType: V2NIMTeamType, accountId: string,
chatBanned: boolean): Promise<void> {
await ChatKitClient.nim.teamService?.setTeamMemberChatBannedStatus(teamId, teamType, accountId, chatBanned)
}
/**
* 获取群成员列表
*
* @param teamId 群组id
* @param teamType 群组类型
* @param memberRoles 成员角色
* @returns 查询结果
*/
static async getTeamMemberList(teamId: string,
teamType: V2NIMTeamType,
queryOption: V2NIMTeamMemberQueryOption): Promise<V2NIMTeamMemberListResult | undefined> {
return await ChatKitClient.nim.teamService?.getTeamMemberList(teamId, teamType, queryOption)
}
/**
* 根据账号 ID 列表获取群组成员列表
*
* @param teamId 群组id
* @param teamType 群组类型
* @param accountIds 成员的账号 ID 列表
* @returns 成员列表
*/
static async getTeamMemberListByIds(teamId: string,
teamType: V2NIMTeamType,
accountIds: string[]): Promise<V2NIMTeamMember[]> {
return await ChatKitClient.nim.teamService?.getTeamMemberListByIds(teamId, teamType, accountIds) ?? []
}
/**
* 获取群加入相关信息
*
* @param option 查询参数
*/
static async getTeamJoinActionInfoList(option: V2NIMTeamJoinActionInfoQueryOption): Promise<V2NIMTeamJoinActionInfoResult | undefined> {
return await ChatKitClient.nim.teamService?.getTeamJoinActionInfoList(option)
}
/**
* 根据关键字搜索群信息
* - 混合搜索高级群和超大群like匹配
* - 只搜索群名称
*/
static async searchTeamByKeyword(keyword: string): Promise<V2NIMTeam[]> {
// TODO: 等待SDK实现
// return await ChatKitClient.nim.teamService?.searchTeamByKeyword(keyword) ?? []
return []
}
/**
* 根据关键字搜索群成员
*
* @param searchOption 搜索参数
* @param success 成功回调
* @param failure 失败回调
*/
static async searchTeamMembers(searchOption: V2NIMTeamMemberSearchOption): Promise<V2NIMTeamMemberSearchResult | undefined> {
// TODO: 等待SDK实现
// return await ChatKitClient.nim.teamService?.searchTeamMembers(searchOption)
return undefined
}
/**
* 分页获取群成员列表,包含用户和好友信息
* @param teamId
* @param teamType
* @param limit
* @param nextToken
* @returns
*/
static async getTeamMembers(teamId: string,
teamType: V2NIMTeamType, queryOption: V2NIMTeamMemberQueryOption): Promise<TeamMemberResult | undefined> {
let teamMemberResult = await TeamRepo.getTeamMemberList(
teamId,
teamType,
queryOption
)
let teamMembers: TeamMemberWithUser[] | undefined = teamMemberResult?.memberList.map((teamMember) => {
return new TeamMemberWithUser(teamMember,
NEFriendUserCache.getInstance().getFriendById(teamMember.accountId)?.friend,
NEFriendUserCache.getInstance().getFriendById(teamMember.accountId)?.user)
})
let noUserAccIdList = teamMembers?.filter((member) => {
return !member.userInfo
}).map((teamMember) => {
return teamMember.teamMember.accountId
})
if (noUserAccIdList && noUserAccIdList.length > 0) {
let userList = await ContactRepo.getUserList(noUserAccIdList)
let userMap = userList.reduce((acc, cur, _index) => {
acc.set(cur.accountId, cur)
return acc
}, new Map<string, V2NIMUser>())
teamMembers?.forEach((member) => {
if (!member.userInfo) {
member.userInfo = userMap.get(member.teamMember.accountId)
}
})
}
if (teamMemberResult) {
let result: TeamMemberResult = {
finished: teamMemberResult?.finished,
nextToken: teamMemberResult?.nextToken,
memberList: teamMembers ?? []
}
return result
}
return
}
/**
* 根据成员id查询群成员的信息
* @param teamId
* @param teamType
* @param accountIds
* @returns
*/
static async getTeamMembersByIds(teamId: string,
teamType: V2NIMTeamType, accountIds: string[]): Promise<TeamMemberWithUser[]> {
let teamMemberList: V2NIMTeamMember[] = []
try {
teamMemberList = await TeamRepo.getTeamMemberListByIds(
teamId,
teamType,
accountIds
)
console.debug(`TeamRepo getTeamMembersByIds result = ${teamMemberList.length} `);
} catch (e) {
console.error(`TeamRepo getTeamMembersByIds ${e}`);
}
let teamMembers: TeamMemberWithUser[] = teamMemberList.map((teamMember) => {
return new TeamMemberWithUser(teamMember,
NEFriendUserCache.getInstance().getFriendById(teamMember.accountId)?.friend,
NEFriendUserCache.getInstance().getFriendById(teamMember.accountId)?.user)
})
let noUserAccIdList = teamMembers?.filter((member) => {
return !member.userInfo
}).map((teamMember) => {
return teamMember.teamMember.accountId
})
if (noUserAccIdList && noUserAccIdList.length > 0) {
let userList = await ContactRepo.getUserList(noUserAccIdList)
let userMap = userList.reduce((acc, cur, _index) => {
acc.set(cur.accountId, cur)
return acc
}, new Map<string, V2NIMUser>())
teamMembers?.forEach((member) => {
if (!member.userInfo) {
member.userInfo = userMap.get(member.teamMember.accountId)
}
})
}
return teamMembers
}
/**
* 根据群成员生成群名
* @param inviteeAccountIds
* @returns
*/
private static getTeamNameByMemberId(inviteeAccountIds?: string[]): string {
if (inviteeAccountIds) {
let inviteeUser = NEFriendUserCache.getInstance().getFriendsByIds(inviteeAccountIds)
let inviteeNames = inviteeUser.map(user => user.user?.name ?? user.user?.accountId)
let teamName = inviteeNames.slice(0, Math.min(inviteeNames.length, 30)).join('、')
let mineName = NEFriendUserCache.mineUserCache?.showName(false)
teamName = mineName + '、' + teamName
return teamName.slice(0, Math.min(teamName.length, 30))
}
return ''
}
}

View File

@ -0,0 +1,57 @@
import { V2NIMMessageAttachment } from '@nimsdk/base';
import { mergedMessageCustomType } from '../constant/Constant';
import { MergedMessageAttachment } from '../model/CustomMessageAttachment';
export class CustomMessageUtils {
public static attachmentOfCustomMessage(attachment: V2NIMMessageAttachment) {
if (attachment.raw) {
try {
let attachmentObject = JSON.parse(attachment.raw) as object
return attachmentObject
} catch (err) {
console.error(err)
return undefined
}
}
return undefined
}
public static typeOfCustomMessage(attachment: V2NIMMessageAttachment) {
let customAttachment = CustomMessageUtils.attachmentOfCustomMessage(attachment)
if (customAttachment) {
return customAttachment["type"] as number
}
return undefined
}
public static dataOfCustomMessage(attachment: V2NIMMessageAttachment) {
let customAttachment = CustomMessageUtils.attachmentOfCustomMessage(attachment)
let type = CustomMessageUtils.typeOfCustomMessage(attachment)
if (type === mergedMessageCustomType) {
return customAttachment?.["data"] as MergedMessageAttachment
}
return customAttachment?.["data"] as object
}
public static heightOfCustomMessage(attachment: V2NIMMessageAttachment) {
let customAttachment = CustomMessageUtils.attachmentOfCustomMessage(attachment)
if (customAttachment) {
return customAttachment["customHeight"] as number
}
return undefined
}
/// 是否是【未知消息】
public static isUnknownMessage(attachment?: V2NIMMessageAttachment) {
if (attachment) {
const customType = CustomMessageUtils.typeOfCustomMessage(attachment)
switch (customType) {
case mergedMessageCustomType:
return false
default:
return true
}
}
return false
}
}

View File

@ -0,0 +1,54 @@
import { promptAction } from '@kit.ArkUI';
import { V2NIMErrorCode } from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export class ErrorUtils {
static handleErrorToast(errorCode: number) {
let msg: ResourceStr = $r('app.string.unknown_error');
switch (errorCode) {
case V2NIMErrorCode.V2NIM_ERROR_CODE_PIN_LIMIT:
msg = $r('app.string.chat_pin_limit_error_tips')
break
case V2NIMErrorCode.V2NIM_ERROR_CODE_COLLECTION_LIMIT:
msg = $r('app.string.chat_collection_limit_error_tips')
break
case V2NIMErrorCode.V2NIM_ERROR_CODE_ILLEGAL_STATE:
msg = $r('app.string.chat_network_error_tips')
break
}
try {
promptAction.showToast({
message: msg,
alignment: Alignment.Bottom
})
} catch (error) {
console.error(`showToast args error code is ${error.code}, message is ${error.message}`);
}
}
static checkNetworkAndToast(): boolean {
if (ChatKitClient.connectBroken()) {
ErrorUtils.showToast($r('app.string.chat_network_error_tips'))
}
return !ChatKitClient.connectBroken()
}
private static showToast(msg: ResourceStr) {
try {
promptAction.showToast({
message: msg,
alignment: Alignment.Bottom
})
} catch (error) {
console.error(`showToast args error code is ${error.code}, message is ${error.message}`);
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export class EventHubUtil {
private eventhub = getContext(this).eventHub
/// 订阅事件
public on(eventName: string, callback: Function) {
this.eventhub.on(eventName, callback)
}
/// 取消订阅事件
public off(eventName: string, callback?: Function) {
this.eventhub.off(eventName, callback)
}
/// 触发事件
public emit(eventName: string, params: Object) {
this.eventhub.emit(eventName, params)
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { systemDateTime } from '@kit.BasicServicesKit';
import { V2NIMMessage, V2NIMMessageRevokeNotification } from '@nimsdk/base';
import { ChatKitClient } from '../ChatKitClient';
import { RevokeMessageExtension } from '../model/RevokeMessageExtension';
import { ChatRepo } from '../repo/ChatRepo';
// 保存撤回消息到本地
export function saveLocalRevokeMessage(conversationId: string, msg: V2NIMMessage, edit: boolean) {
// let currentTime = systemDateTime.getTime()
// let revokeText = getContext().resourceManager.getStringByNameSync('chat_msg_undo_tips');
// let revokeMsg = ChatKitClient.nim.messageCreator.createTextMessage(revokeText)
// revokeMsg.serverExtension = msg.serverExtension
// revokeMsg.threadReply = msg.threadReply
// let localExtension = {
// revoke_message_local: true,
// revoke_message_local_time: currentTime,
// revoke_message_client_id: msg.messageClientId,
// revoke_message_local_edit: edit,
// revoke_message_local_content: msg.text ?? ''
//
// } as RevokeMessageExtension
//
// revokeMsg.localExtension = JSON.stringify(localExtension)
// let createTime = msg.createTime + 10
// console.debug('netease saveLocalRevokeMessage:', revokeMsg.localExtension)
// ChatRepo.saveLocalMessage(revokeMsg, conversationId, ChatKitClient.getLoginUserId(), createTime)
}
// 保存他人撤回消息到本地
export function saveLocalRevokeMessageFormOther(conversationId: string, msgNotify: V2NIMMessageRevokeNotification,
edit: boolean) {
// let currentTime = systemDateTime.getTime()
// let revokeText = getContext().resourceManager.getStringByNameSync('chat_msg_undo_tips');
// let revokeMsg = ChatKitClient.nim.messageCreator.createTextMessage(revokeText)
// let localExtension = {
// revoke_message_local: false,
// revoke_message_local_time: currentTime,
// revoke_message_client_id: msgNotify.messageRefer.messageClientId,
// revoke_message_local_edit: edit,
// revoke_message_local_content: ''
// } as RevokeMessageExtension
//
// revokeMsg.localExtension = JSON.stringify(localExtension)
// console.debug('netease saveLocalRevokeMessageFormOther:', revokeMsg.localExtension)
// let createTime = msgNotify.messageRefer.createTime + 10;
// ChatRepo.saveLocalMessage(revokeMsg, conversationId, msgNotify.revokeAccountId, createTime)
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { V2NIMTeam } from '@nimsdk/base';
const discussTeamKey = "im_ui_kit_group" // 讨论组识别关键字
/// 判断是否是讨论组
export function IsDiscussion(team?: V2NIMTeam) {
if (team?.serverExtension && team.serverExtension.includes(discussTeamKey)) {
return true
}
return false
}

View File

@ -0,0 +1,11 @@
{
"module": {
"name": "chatkit",
"type": "har",
"deviceTypes": [
"default",
"tablet",
"2in1"
]
}
}

View File

@ -0,0 +1,76 @@
{
"string": [
{
"name": "unknown_error",
"value": "未知错误"
},
{
"name": "chat_network_error_tips",
"value": "当前网络不可用,请检查你的网络设置。"
},
{
"name": "chat_pin_limit_error_tips",
"value": "PIN消息已达最大限制"
},
{
"name": "chat_collection_limit_error_tips",
"value": "收藏数已达到限制值"
},
{
"name": "chatMessageNonsupportType",
"value": "[当前版本暂不支持该消息体]"
},
{
"name": "audioMessageType",
"value": "[语音消息]"
},
{
"name": "imageMessageType",
"value": "[图片消息]"
},
{
"name": "videoMessageType",
"value": "[视频消息]"
},
{
"name": "locationMessageType",
"value": "[地理位置]"
},
{
"name": "fileMessageType",
"value": "[文件消息]"
},
{
"name": "notificationMessageType",
"value": "[通知消息]"
},
{
"name": "tipMessageType",
"value": "[提醒消息]"
},
{
"name": "chatHistoryBrief",
"value": "聊天记录"
},
{
"name": "msg_type_rtc_video",
"value": "[视频通话]"
},
{
"name": "msg_type_rtc_audio",
"value": "[语音通话]"
},
{
"name": "msg_send_failed_in_block",
"value": "对方已将你拉黑,发送消息失败"
},
{
"name": "msg_send_failed_no_friend",
"value": "双方好友关系已解除,如需沟通,请申请"
},
{
"name": "msg_send_failed_friend_application",
"value": "好友验证"
}
]
}

View File

@ -0,0 +1,76 @@
{
"string": [
{
"name": "unknown_error",
"value": "Unknown error"
},
{
"name": "chat_network_error_tips",
"value": "network error"
},
{
"name": "chat_pin_limit_error_tips",
"value": "PIN Message Limit reached"
},
{
"name": "chat_collection_limit_error_tips",
"value": "Collection Message Limit reached"
},
{
"name": "chatMessageNonsupportType",
"value": "[Nonsupport Message]"
},
{
"name": "audioMessageType",
"value": "[Audio Message]"
},
{
"name": "imageMessageType",
"value": "[Image Message]"
},
{
"name": "videoMessageType",
"value": "[Video Message]"
},
{
"name": "locationMessageType",
"value": "[Location Message]"
},
{
"name": "fileMessageType",
"value": "[File Message]"
},
{
"name": "notificationMessageType",
"value": "[Notification Message]"
},
{
"name": "tipMessageType",
"value": "[Tip Message]"
},
{
"name": "chatHistoryBrief",
"value": "Chat History"
},
{
"name": "msg_type_rtc_video",
"value": "[Video Chat]"
},
{
"name": "msg_type_rtc_audio",
"value": "[Audio Chat]"
},
{
"name": "msg_send_failed_in_block",
"value": "The other party has blocked you, message sending failed"
},
{
"name": "msg_send_failed_no_friend",
"value": "The relationship between both parties has been terminated, please "
},
{
"name": "msg_send_failed_friend_application",
"value": "reapply for contact"
}
]
}

View File

@ -0,0 +1,76 @@
{
"string": [
{
"name": "unknown_error",
"value": "未知错误"
},
{
"name": "chat_network_error_tips",
"value": "当前网络不可用,请检查你的网络设置。"
},
{
"name": "chat_pin_limit_error_tips",
"value": "PIN消息已达最大限制"
},
{
"name": "chat_collection_limit_error_tips",
"value": "收藏数已达到限制值"
},
{
"name": "chatMessageNonsupportType",
"value": "[当前版本暂不支持该消息体]"
},
{
"name": "audioMessageType",
"value": "[语音消息]"
},
{
"name": "imageMessageType",
"value": "[图片消息]"
},
{
"name": "videoMessageType",
"value": "[视频消息]"
},
{
"name": "locationMessageType",
"value": "[地理位置]"
},
{
"name": "fileMessageType",
"value": "[文件消息]"
},
{
"name": "notificationMessageType",
"value": "[通知消息]"
},
{
"name": "tipMessageType",
"value": "[提醒消息]"
},
{
"name": "chatHistoryBrief",
"value": "聊天记录"
},
{
"name": "msg_type_rtc_video",
"value": "[视频通话]"
},
{
"name": "msg_type_rtc_audio",
"value": "[语音通话]"
},
{
"name": "msg_send_failed_in_block",
"value": "对方已将你拉黑,发送消息失败"
},
{
"name": "msg_send_failed_no_friend",
"value": "双方好友关系已解除,如需沟通,请申请"
},
{
"name": "msg_send_failed_friend_application",
"value": "好友验证"
}
]
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { hilog } from '@kit.PerformanceAnalysisKit';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}

View File

@ -0,0 +1,13 @@
{
"module": {
"name": "chatkit_test",
"type": "feature",
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false
}
}

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}

7
chatkit_ui/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
/oh-package-lock.json5

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '10.1.0';
export const BUILD_MODE_NAME = 'release';
export const DEBUG = false;
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;
}

16
chatkit_ui/Index.ets Normal file
View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export { NEEmojiManager } from './src/main/ets/manager/NEEmojiManager'
export { ChatP2PPage } from './src/main/ets/pages/ChatP2PPage'
export { ForwardMessageDialog } from './src/main/ets/view/ForwardMessageDialog'
export { TextMessageDetailDialog } from './src/main/ets/view/TextMessageDetailDialog'
export { MessageItemClick } from './src/main/ets/view/MessageItemClick'

View File

@ -0,0 +1,28 @@
{
"apiType": "stageMode",
"buildOption": {
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
},
"consumerFiles": [
"./consumer-rules.txt"
]
}
}
},
],
"targets": [
{
"name": "default"
}
]
}

View File

13
chatkit_ui/hvigorfile.ts Normal file
View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}

View File

@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation

View File

@ -0,0 +1,19 @@
{
"name": "@nimkit/chatkit_ui",
"version": "10.1.0",
"description": "Please describe the basic information.",
"main": "Index.ets",
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@nimkit/common": "file:../common",
"@nimkit/chatkit": "file:../chatkit",
"@nimkit/corekit": "file:../corekit",
"@nimsdk/base": "10.9.10",
"class-transformer": "^0.5.1",
"reflect-metadata": "^0.1.13",
'@nimkit/markdown': "1.1.0",
"@itcast/basic":"file:../commons/basic"
// 用于嵌套对象@Type反射
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export class ChatKitConfig {
//消息撤回时间限制
static messageRevokeTimeLimit: number = 2 * 60 * 1000;
//消息时间展示的间隔默认5分钟
static messageTimeGap: number = 5 * 60 * 1000;
//消息分页拉取,每页大小
static chatMessagePageSize: number = 50;
// 消息已读未读功能
static messageReadState: boolean = true;
}

View File

@ -0,0 +1,365 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { filePreview } from '@kit.PreviewKit';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
import { NEChatMoreOperationData, NEChatMoreOperationType } from '../model/NEChatMoreOperationData';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { V2NIMMessageLocationAttachment, V2NIMMessageSendingState, V2NIMMessageType } from '@nimsdk/base';
import { sceneMap } from '@kit.MapKit';
import { common } from '@kit.AbilityKit';
import { MessageOperationItem, MessageOperationType } from '../model/MessageOperationItem';
import { ChatConst } from '../constants/ChatConst';
import { ChatKitClient } from '@nimkit/chatkit';
import { ChatKitConfig } from '../ChatKitConfig';
import { isSupportMessage } from '../common/MessageHelper';
export function openFileWithApp(fileUri: string, fileName: string, fileExt: string, context: Context) {
let displayInfo: filePreview.DisplayInfo = {
x: 100,
y: 100,
};
let fileInfo: filePreview.PreviewInfo = {
title: fileName,
uri: fileUri,
mimeType: getOpenFileType(fileExt)
};
filePreview.openPreview(context, fileInfo, displayInfo).then(() => {
console.info('Succeeded in opening preview');
}).catch((err: BusinessError) => {
console.error(`Failed to open preview, err.code = ${err.code}, err.message = ${err.message}`);
});
}
export function showLocationDetail(msg: NIMMessageInfo, context: common.UIAbilityContext) {
let attachment = msg.message.attachment as V2NIMMessageLocationAttachment
let queryLocationOptions: sceneMap.LocationQueryOptions = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
name: attachment.address,
address: attachment.address,
};
// 拉起地点详情页
sceneMap.queryLocation(context, queryLocationOptions).then(() => {
console.info("netease QueryLocation", "Succeeded in querying location.");
}).catch((err: BusinessError) => {
console.error("netease QueryLocation", `Failed to query Location, code: ${err.code}, message: ${err.message}`);
});
}
export function setupMoreOperationData() {
let operationMoreDataList: Array<NEChatMoreOperationData> = Array()
// 照片功能
const imageOperationData = new NEChatMoreOperationData();
imageOperationData.operationTitle = $r("app.string.photo_take");
imageOperationData.type = NEChatMoreOperationType.Image;
imageOperationData.imageSource = "app.media.im_icon_images";
operationMoreDataList.push(imageOperationData);
// 拍摄功能
const videoOperationData = new NEChatMoreOperationData();
videoOperationData.operationTitle = $r("app.string.chat_camera_take");
videoOperationData.type = NEChatMoreOperationType.Video;
videoOperationData.imageSource = "app.media.im_icon_camera";
operationMoreDataList.push(videoOperationData);
//快捷回复
const replyOperationData = new NEChatMoreOperationData();
replyOperationData.operationTitle = $r("app.string.reply");
replyOperationData.type = NEChatMoreOperationType.Reply;
replyOperationData.imageSource = "app.media.quck_message";
operationMoreDataList.push(replyOperationData);
//患教
const teachOperationData = new NEChatMoreOperationData();
teachOperationData.operationTitle = $r("app.string.teach");
teachOperationData.type = NEChatMoreOperationType.Teach;
teachOperationData.imageSource = "app.media.patient_teach_call";
operationMoreDataList.push(teachOperationData);
//出停诊
const outOperationData = new NEChatMoreOperationData();
outOperationData.operationTitle = $r("app.string.outpatient");
outOperationData.type = NEChatMoreOperationType.Outpatient;
outOperationData.imageSource = "app.media.outpatient_true";
operationMoreDataList.push(outOperationData);
//商城
const shopOperationData = new NEChatMoreOperationData();
shopOperationData.operationTitle = $r("app.string.shopping");
shopOperationData.type = NEChatMoreOperationType.Shopping;
shopOperationData.imageSource = "app.media.ytx_chattingfooter_shopping";
operationMoreDataList.push(shopOperationData);
//互联网医院
const hospatilOperationData = new NEChatMoreOperationData();
hospatilOperationData.operationTitle = $r("app.string.hospital");
hospatilOperationData.type = NEChatMoreOperationType.Hospital;
hospatilOperationData.imageSource = "app.media.ytx_chatting_hospital";
operationMoreDataList.push(hospatilOperationData);
// // 位置功能
// const locationOperationData = new NEChatMoreOperationData();
// locationOperationData.operationTitle = $r("app.string.chat_send_location");
// locationOperationData.type = NEChatMoreOperationType.Location;
// locationOperationData.imageSource = "app.media.ic_chat_more_location";
// operationMoreDataList.push(locationOperationData);
//
// // 文件功能
// const fileOperationData = new NEChatMoreOperationData();
// fileOperationData.operationTitle = $r("app.string.chat_send_file");
// fileOperationData.type = NEChatMoreOperationType.File;
// fileOperationData.imageSource = "app.media.ic_public_chat_file";
// operationMoreDataList.push(fileOperationData);
return operationMoreDataList
}
export function getOpenFileType(fileExt: string): string {
if (fileExt.includes('txt')) {
return 'text/plain'
} else if (fileExt.includes('cpp')) {
return 'text/x-c++src'
} else if (fileExt.includes('c')) {
return 'text/x-csrc'
} else if (fileExt.includes('h')) {
return 'text/x-chdr'
} else if (fileExt.includes('java')) {
return 'text/x-java'
} else if (fileExt.includes('xhtml')) {
return 'application/xhtml+xml'
} else if (fileExt.includes('xml')) {
return 'text/xml'
} else if (fileExt.includes('html') || fileExt.includes('htm')) {
return 'text/html'
} else if (fileExt.includes('pdf')) {
return 'application/pdf'
} else if (fileExt.includes('jpg')) {
return 'image/jpeg'
} else if (fileExt.includes('png')) {
return 'image/png'
} else if (fileExt.includes('gif')) {
return 'image/gif'
} else if (fileExt.includes('webp')) {
return 'image/webp'
} else if (fileExt.includes('bmp')) {
return 'image/bmp'
} else if (fileExt.includes('svg')) {
return 'image/svg+xml'
} else if (fileExt.includes('m4a')) {
return 'audio/mp4a-latm'
} else if (fileExt.includes('mp3')) {
return 'audio/mpeg'
} else if (fileExt.includes('aac')) {
return 'audio/aac'
} else if (fileExt.includes('ogg')) {
return 'audio/ogg'
} else if (fileExt.includes('wav')) {
return 'audio/x-wav'
} else if (fileExt.includes('mp4')) {
return 'video/mp4'
} else if (fileExt.includes('mkv')) {
return 'video/x-matroska'
} else if (fileExt.includes('ts')) {
return 'video/mp2ts'
}
return ' '
}
export function sliceContent(content: string | undefined, maxsize: number, isCenter: boolean): string {
if (!content) {
return ''
}
let result = content
if (content.length > maxsize) {
if (isCenter) {
let startIndex = Math.floor(maxsize * 0.75)
let lastIndex = content.length - 2
let targetStr = content.charAt(startIndex)
let lastTargetStr = content.charAt(lastIndex)
// 判断是否为高位
if (isCharacterEmoji(targetStr) && !isLastCharacterEmoji(targetStr)) {
startIndex++
}
let nick = content.substring(0, startIndex + 1) + "..."
if (isLastCharacterEmoji(lastTargetStr)) {
lastIndex++
}
result = nick + content.substring(lastIndex)
} else {
let lastIndex = maxsize - 2
let lastTargetStr = content.charAt(lastIndex)
// 判断是否为高位
if (isCharacterEmoji(lastTargetStr) && !isLastCharacterEmoji(lastTargetStr)) {
lastIndex++
}
result = content.substring(0, lastIndex + 1) + "..."
}
}
return result
}
// 最后是否为表情符号的最后一位
export function isLastCharacterEmoji(str: string): boolean {
// 使用正则表达式匹配表情符号
const emojiRegex = /[\uDC00-\uDFFF]+$/u
return emojiRegex.test(str)
}
// 最后是否为表情符号
export function isCharacterEmoji(str: string): boolean {
// 使用正则表达式匹配表情符号
const emojiRegex = /[\uD83C-\uDBFF\uDC00-\uDFFF]+$/u
return emojiRegex.test(str)
}
export function getOperateMenu(operateMsg: NIMMessageInfo | undefined): MessageOperationItem[] {
let operationMoreDataList: MessageOperationItem[] = []
let localExtension = operateMsg?.message.localExtension;
if (localExtension == undefined || !localExtension.includes(ChatConst.revokeLocalKey)) {
if (operateMsg !== undefined &&
operateMsg.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
let copyItem = new MessageOperationItem()
copyItem.operationType = MessageOperationType.Copy;
copyItem.operationText = $r('app.string.chat_operation_copy');
copyItem.operationImage = $r('app.media.ic_chat_menu_copy');
operationMoreDataList.push(copyItem)
}
// if (operateMsg !== undefined &&
// operateMsg.message.sendingState == V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
// && isSupportMessage(operateMsg)) {
// if (operateMsg.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
//
// let replyItem = new MessageOperationItem()
// replyItem.operationType = MessageOperationType.Reply;
// replyItem.operationText = $r('app.string.chat_operation_reply');
// replyItem.operationImage = $r('app.media.ic_chat_menu_reply');
// operationMoreDataList.push(replyItem);
//
//
// if (operateMsg.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO) {
// let translateItem = new MessageOperationItem()
// translateItem.operationType = MessageOperationType.Forward;
// translateItem.operationText = $r('app.string.chat_operation_forward');
// translateItem.operationImage = $r('app.media.ic_chat_menu_translate');
// operationMoreDataList.push(translateItem);
// }
//
// let pinItem = new MessageOperationItem()
// if (operateMsg?.isPinMsg) {
// pinItem.operationText = $r('app.string.chat_operation_unpin');
// pinItem.operationType = MessageOperationType.Unpin;
// } else {
// pinItem.operationType = MessageOperationType.Pin;
// pinItem.operationText = $r('app.string.chat_operation_pin');
//
// }
// pinItem.operationImage = $r('app.media.ic_chat_menu_pin');
// operationMoreDataList.push(pinItem);
// }
//
// }
let deleteItem = new MessageOperationItem()
deleteItem.operationType = MessageOperationType.Delete;
deleteItem.operationText = $r('app.string.chat_operation_delete');
deleteItem.operationImage = $r('app.media.ic_chat_menu_delete');
operationMoreDataList.push(deleteItem);
if (operateMsg !== undefined &&
operateMsg.message.sendingState == V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
&& isSupportMessage(operateMsg) && operateMsg.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
&& systemDateTime.getTime() - operateMsg.message.createTime < ChatKitConfig.messageRevokeTimeLimit
&& operateMsg.message.senderId == ChatKitClient.getLoginUserId()) {
let undoItem = new MessageOperationItem()
undoItem.operationImage = $r('app.media.ic_chat_menu_revoke');
undoItem.operationText = $r('app.string.chat_operation_undo');
undoItem.operationType = MessageOperationType.Undo;
operationMoreDataList.push(undoItem)
}
// let selectItem = new MessageOperationItem()
// selectItem.operationType = MessageOperationType.Select;
// selectItem.operationText = $r('app.string.chat_operation_multi_select');
// selectItem.operationImage = $r('app.media.ic_chat_menu_multi_select');
// operationMoreDataList.push(selectItem);
// if (operateMsg !== undefined &&
// operateMsg.message.sendingState === V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
// && isSupportMessage(operateMsg)
// && operateMsg.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
// let collectionItem = new MessageOperationItem()
// collectionItem.operationType = MessageOperationType.Collection;
// collectionItem.operationText = $r('app.string.chat_operation_collection');
// collectionItem.operationImage = $r('app.media.ic_chat_menu_collection');
// operationMoreDataList.push(collectionItem);
// }
}
return operationMoreDataList
}
export function getOperateMenuSize(msgInfo: NIMMessageInfo | undefined) {
let count = 0
if (msgInfo == undefined) {
return count
}
let localExtension = msgInfo?.message.localExtension;
if (localExtension == undefined || !localExtension.includes(ChatConst.revokeLocalKey)) {
// 多选和删除
count = count + 2
if (msgInfo !== undefined &&
msgInfo.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
// 复制
count++
}
if (msgInfo.message.sendingState == V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
&& isSupportMessage(msgInfo)) {
if (msgInfo.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
// 回复、收藏
count = count + 2
if (msgInfo.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO) {
// 转发、标记
count = count + 2
} else {
// 标记
count++
}
if (msgInfo.getMessageType() !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL &&
systemDateTime.getTime() - msgInfo.message.createTime < ChatKitConfig.messageRevokeTimeLimit
&& msgInfo.message.senderId == ChatKitClient.getLoginUserId()) {
// 撤回
count++;
}
}
}
}
return count
}
/**
* 计算消息长按弹窗宽度
* @param msg 长按的消息
* @returns 消息长按弹窗宽度
*/
export function computeOperateViewWidth(msg: NIMMessageInfo | undefined): number {
return ChatConst.menuItemWidth * Math.min(getOperateMenuSize(msg), ChatConst.menuItemColumnNum)
}
export function computeOperateViewHeight(msg: NIMMessageInfo | undefined): number {
return ChatConst.menuItemHeight * (Math.ceil(getOperateMenuSize(msg) / ChatConst.menuItemColumnNum))
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { systemDateTime } from '@kit.BasicServicesKit';
export class DateUtils {
static formatTime(msgTime: number, lastTime?: number): string {
const msgDate = new Date(msgTime);
const nowDate = new Date(systemDateTime.getTime());
const yearOptions: Intl.DateTimeFormatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
month: 'long',
day: 'numeric',
year: 'numeric'
};
const monthOptions: Intl.DateTimeFormatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
month: 'long',
day: 'numeric',
};
const minuteOptions: Intl.DateTimeFormatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
};
if (nowDate.getFullYear() !== msgDate.getFullYear()) {
return msgDate.toLocaleString('zh-CN', yearOptions);
} else if (nowDate.getMonth() !== msgDate.getMonth() || nowDate.getUTCDate() !== msgDate.getUTCDate()) {
return msgDate.toLocaleString('zh-CN', monthOptions);
} else {
return msgDate.toLocaleString('zh-CN', minuteOptions);
}
}
static formatCallTime(time: number): string {
const hour: number = Math.floor(time / 3600);
const minute: number = Math.floor((time % 3600) / 60);
const second: number = time % 60;
if (hour === 0) {
return `${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
}
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString()
.padStart(2, '0')}`;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export class DeviceUtils {
static windowPXWidth: number = 0;
static windowPXHeight: number = 0;
static rootDirPath: string = '';
private static msgLineWidth: number = 0;
static getMessageLineWidth(): number {
DeviceUtils.msgLineWidth = DeviceUtils.windowPXWidth * 0.75 - vp2px(50);
return DeviceUtils.msgLineWidth;
}
static getPinMessageLineWidth(): number {
DeviceUtils.msgLineWidth = DeviceUtils.windowPXWidth - vp2px(72);
return DeviceUtils.msgLineWidth;
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { fileShare } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class FileUtils {
static formatFileSize(bytes: number): string {
if (bytes < 1024) {
return bytes + "B";
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + "KB";
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(2) + "MB";
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + "GB";
}
}
static getFileExtension(filename: string): string {
if (filename && filename.length > 0) {
const dot = filename.lastIndexOf('.');
if (dot > -1 && dot < filename.length - 1) {
return filename.substring(dot + 1);
}
}
return "";
}
static openFile(path: string) {
try {
let uri = path;
let policyInfo: fileShare.PolicyInfo = {
uri: uri,
operationMode: fileShare.OperationMode.READ_MODE,
};
let policies: Array<fileShare.PolicyInfo> = [policyInfo];
fileShare.activatePermission(policies).then(() => {
console.info("activatePermission successfully");
}).catch(async (err: BusinessError<Array<fileShare.PolicyErrorResult>>) => {
console.error("activatePermission failed with error message: " + err.message + ", error code: " + err.code);
if (err.code == 13900001 && err.data) {
for (let i = 0; i < err.data.length; i++) {
console.error("error code : " + JSON.stringify(err.data[i].code));
console.error("error uri : " + JSON.stringify(err.data[i].uri));
console.error("error reason : " + JSON.stringify(err.data[i].message));
if (err.data[i].code == fileShare.PolicyErrorCode.PERMISSION_NOT_PERSISTED) {
await fileShare.persistPermission(policies);
}
}
}
});
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error('activatePermission failed with err: ' + JSON.stringify(err));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
/**
* Common constants for all features.
*/
export class ChatConst {
//消息分页拉取,每页大小
static readonly chatMessagePageSize: number = 50;
// 图片消息图片的高度
static readonly imageMessageHeight: number = 124;
// 图片消息图片的宽度
static readonly imageMessageWidth: number = 222;
// 合并转发消息高度
static readonly mergedMessageHeight: number = 130;
// 合并转发消息最小宽度
static readonly mergedMessageMinHeight: number = 80;
// 消息单行高度 vp
static readonly messageLineHeight: number = 60;
// 文本消息单行高度 vp
static readonly textLineHeight: number = 20;
//文本消息字体大小
static readonly messageTextFontSize: number = 14;
// 地图消息默认高度 vp
static readonly mapMessageHeight: number = 160;
// 文件消息默认宽度 vp
static readonly fileMessageWidth: number = 60;
// 语音消息默认宽度 vp
static readonly audioMessageWidth: number = 35;
// 消息输入区域高度
static readonly messageInputAreaHeight: number = 250;
// 消息长按菜单每个item的宽度
static readonly menuItemWidth: number = 60;
static readonly menuItemHeight: number = 50;
// 消息长按菜单列数
static readonly menuItemColumnNum: number = 5;
// PIN 消息操作者名称最大长度
static readonly pinOperatorNameMaxLen: number = 15;
// 消息撤回本地存储,是否为本端撤回消息
static readonly revokeLocalKey: string = 'revoke_message_local';
// 消息撤回本地存储,撤回时间
static readonly revokeLocalTimeKey: string = 'revoke_message_local_time';
// 消息撤回本地存储撤回的消息clientId
static readonly revokeMsgClientIdKey: string = 'revoke_message_client_id';
// 消息撤回本地存储,撤回的消息内容
static readonly revokeMsgTextKey: string = 'revoke_message_local_content';
// 消息撤回本地存储,撤回的消息是否为编辑状态
static readonly revokeMsgEditKey: string = 'revoke_message_local_edit';
// 消息删除限制
static readonly messageDeleteLimit: number = 50;
}

View File

@ -0,0 +1,150 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { audio } from '@kit.AudioKit';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
const TAG = 'Audio manager';
class Options {
offset?: number;
length?: number;
}
let bufferSize: number = 0;
let audioCapture: audio.AudioCapturer | undefined = undefined;
let audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
channels: audio.AudioChannel.CHANNEL_2, // 通道
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
}
let audioCaptureInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC, // 音源类型
capturerFlags: 0 // 音频采集器标志
}
let audioCaptureOptions: audio.AudioCapturerOptions = {
streamInfo: audioStreamInfo,
capturerInfo: audioCaptureInfo
}
export class AudioManager {
public static instance: AudioManager = new AudioManager()
file?: fileIo.File
private constructor() {
console.log('net ease AudioManager constructor')
this.setup();
}
// 初始化,创建实例,设置监听事件
setup() {
// 创建Audio Capture实例
audio.createAudioCapturer(audioCaptureOptions, (err, capture) => {
if (err) {
console.error(`net ease Invoke createAudioCapture failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info(`net ease ${TAG}: create AudioCapture success`);
audioCapture = capture;
if (audioCapture !== undefined) {
// (audioCapture as audio.AudioCapture).on('readData', readDataCallback);
}
});
}
// 开始一次音频采集
public start(filePath: string) {
if (audioCapture !== undefined) {
// let path = getContext().cacheDir;
// let filePath = path + '/StarWars10s-2C-48000-4SW.wav';
let file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
this.file = file;
let readDataCallback = (buffer: ArrayBuffer) => {
let options: Options = {
offset: bufferSize,
length: buffer.byteLength
}
fileIo.writeSync(file.fd, buffer, options);
bufferSize += buffer.byteLength;
}
audioCapture.on('readData', readDataCallback);
let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
if (stateGroup.indexOf((audioCapture as audio.AudioCapturer).state.valueOf()) ===
-1) { // 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集
console.error(`net ease ${TAG}: start failed`);
return;
}
// 启动采集
(audioCapture as audio.AudioCapturer).start((err: BusinessError) => {
if (err) {
console.error('net ease Capture start failed.');
} else {
console.info('net ease Capture start success.');
}
});
}
}
// 停止采集
public stop(callback: AsyncCallback<boolean>) {
if (audioCapture !== undefined && this.file != null) {
// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if ((audioCapture as audio.AudioCapturer).state.valueOf() !== audio.AudioState.STATE_RUNNING &&
(audioCapture as audio.AudioCapturer).state.valueOf() !== audio.AudioState.STATE_PAUSED) {
console.info('net ease Capture is not running or paused');
return;
}
//停止采集
(audioCapture as audio.AudioCapturer).stop((err: BusinessError) => {
if (err) {
console.error('net ease Capture stop failed.');
if (callback) {
callback(err, false);
}
} else {
fileIo.close(this.file);
console.info('net ease Capture stop success.');
if (callback) {
callback(err, true);
}
}
});
}
}
// 销毁实例,释放资源
release() {
if (audioCapture !== undefined) {
// 采集器状态不是STATE_RELEASED或STATE_NEW状态才能release
if ((audioCapture as audio.AudioCapturer).state.valueOf() === audio.AudioState.STATE_RELEASED ||
(audioCapture as audio.AudioCapturer).state.valueOf() === audio.AudioState.STATE_NEW) {
console.info('net ease Capture already released');
return;
}
//释放资源
(audioCapture as audio.AudioCapturer).release((err: BusinessError) => {
if (err) {
console.error('net ease Capture release failed.');
} else {
console.info('net ease Capture release success.');
}
});
}
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import audio from '@ohos.multimedia.audio'; // 导入audio模块
import { CommonConstants } from '@nimkit/common';
export class AudioPlayerManager {
static logTag: string = 'AudioPlayerManager'
static instance: AudioPlayerManager = new AudioPlayerManager()
onStart?: () => void
onFinish?: () => void
avPlayer?: media.AVPlayer
//正在播放的URL
playingUrl?: string
//audioRender
audioRenderer?: audio.AudioRenderer
private constructor() {
}
// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
avPlayer.on('error', (err: BusinessError) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
avPlayer.reset(); // 调用reset重置资源触发idle状态
})
// 状态机变化回调函数
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state idle called.');
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state initialized called.');
avPlayer.prepare();
break;
case 'prepared': // prepare调用成功后上报该状态机
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state prepared called.');
// avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE
avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state playing called.');
if (this.onStart) {
this.onStart()
}
break;
case 'completed': // 播放结束后触发该状态机上报
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state completed called.');
avPlayer.stop(); //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state stopped called.');
avPlayer.reset(); // 调用reset接口初始化avplayer状态
if (this.onFinish) {
this.onFinish()
}
break;
case 'released':
console.debug(`${AudioPlayerManager.logTag}`, 'AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
// 监听interrupt
avPlayer.on('audioInterrupt', (interruptEvent) => {
console.debug(`${AudioPlayerManager.logTag} audioInterrupt ${interruptEvent}`)
if (this.onFinish) {
this.onFinish()
}
})
}
//设置语言播放设备
async setAudioPlayDevice() {
let audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_VOICE_MESSAGE,
rendererFlags: 0
};
let audioRendererOptions: audio.AudioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
};
this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions)
let earpieceOpen = AppStorage.get<boolean>(CommonConstants.KEY_SETTING_EARPIECE_MODE)
console.log(`${AudioPlayerManager.logTag} , earpieceOpen = ${earpieceOpen}`)
await this.audioRenderer.setDefaultOutputDevice((earpieceOpen ?? true) ? audio.DeviceType.EARPIECE :
audio.DeviceType.SPEAKER)
if (this.audioRenderer?.state !== audio.AudioState.STATE_RUNNING) {
await this.audioRenderer.start()
}
}
// 通过url设置网络地址来实现播放直播码流
async avPlayerLive(url: string, onStart?: () => void,
onFinish?: () => void) {
await this.setAudioPlayDevice()
// 创建avPlayer实例对象
if (!this.avPlayer) {
this.avPlayer = await media.createAVPlayer();
}
if (this.onFinish) {
this.onFinish()
}
//重复点击,如果已经播放则停止播放,并返回直接返回
if (this.playingUrl === url
&& (this.avPlayer?.state === 'prepared' ||
this.avPlayer?.state === 'playing')) {
this.playingUrl = undefined
await this.avPlayer?.reset()
return
}
await this.avPlayer?.reset()
this.playingUrl = url
this.onStart = onStart
this.onFinish = onFinish
// 创建状态机变化回调函数
this.setAVPlayerCallback(this.avPlayer);
this.avPlayer.url = url
}
async stopPlayAll() {
console.log(`${AudioPlayerManager.logTag}, stopPlayAll`)
if (this.onFinish) {
this.onFinish()
}
await this.avPlayer?.reset()
await this.audioRenderer?.stop()
this.audioRenderer = undefined
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import fs from '@ohos.file.fs';
let avProfile: media.AVRecorderProfile = {
audioBitrate: 100000, // 音频比特率
audioChannels: 2, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式当前只支持m4a
}
let avConfig: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
profile: avProfile,
url: '', // 参考应用文件访问与管理开发示例新建并读写一个文件
};
export class AudioRecordManager {
public static instance: AudioRecordManager = new AudioRecordManager()
private avRecorder?: media.AVRecorder;
// 初始化
public setup() {
if (this.avRecorder === undefined) {
media.createAVRecorder().then((recorder: media.AVRecorder) => {
console.log('net ease AudioRecordManager create success');
this.avRecorder = recorder;
this.setupAudioRecordListen();
}, (error: BusinessError) => {
console.error(`net ease createAVRecorder failed ${error}`);
})
}
}
// 注册audioRecorder回调函数
setAudioRecorderCallback() {
if (this.avRecorder !== undefined) {
// 状态机变化回调函数
this.avRecorder.on('stateChange', (state: media.AVRecorderState, reason: media.StateChangeReason) => {
console.log(`net ease AudioRecorder current state is ${state}`);
})
// 错误上报回调函数
this.avRecorder.on('error', (err: BusinessError) => {
console.error(`net ease AudioRecorder failed, code is ${err.code}, message is ${err.message}`);
})
}
}
async startRecordingProcess(path: string) {
if (this.avRecorder !== undefined) {
await this.avRecorder.release();
this.avRecorder = undefined;
}
// await FileUtils.createFile(path)
// 1.创建录制实例
this.avRecorder = await media.createAVRecorder();
this.setAudioRecorderCallback();
// 2.获取录制文件fd赋予avConfig里的url参考FilePicker文档
let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
let url = 'fd://' + file.fd
console.log("net ease AudioRecorder url is: " + url);
avConfig.url = url;
// 3.配置录制参数完成准备工作
await this.avRecorder.prepare(avConfig);
// 4.开始录制
await this.avRecorder.start();
console.log('net ease AudioRecorder start success')
}
// 暂停录制对应的流程
async pauseRecordingProcess() {
if (this.avRecorder != undefined && this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
await this.avRecorder.pause();
}
}
// 恢复录制对应的流程
async resumeRecordingProcess() {
if (this.avRecorder != undefined && this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换
await this.avRecorder.resume();
}
}
// 停止录制对应的流程
async stopRecordingProcess() {
if (this.avRecorder != undefined) {
// 1. 停止录制
if (this.avRecorder.state === 'started'
|| this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换
await this.avRecorder.stop();
}
// 2.重置
await this.avRecorder.reset();
// 3.释放录制实例
await this.avRecorder.release();
this.avRecorder = undefined;
// 4.关闭录制文件fd
}
}
// 回调监听
private setupAudioRecordListen() {
if (this.avRecorder !== undefined) {
// 状态上报回调函数
this.avRecorder.on('stateChange', (state: media.AVRecorderState, reason: media.StateChangeReason) => {
console.log(`net ease current state is ${state}`);
// 用户可以在此补充状态发生切换后想要进行的动作
})
// 错误上报回调函数
this.avRecorder.on('error', (err: BusinessError) => {
console.error(`net ease avRecorder failed, code is ${err.code}, message is ${err.message}`);
})
}
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';
import { AsyncCallback } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
const TAG = 'net ease'
export class NEAudioMetaManager {
public static instance: NEAudioMetaManager = new NEAudioMetaManager()
// 获取音频专辑封面(Resource 文件夹下)
async fetchMetadataFromFdSrcByCallback(fileName: string, callback: AsyncCallback<image.PixelMap>) {
if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
// 创建AVMetadataExtractor对象
let avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor()
// 设置fdSrc
avMetadataExtractor.fdSrc = await getContext(this).resourceManager.getRawFd(fileName);
// 获取元数据callback模式
avMetadataExtractor.fetchMetadata((error, metadata) => {
if (error) {
console.error(TAG, `fetchMetadata callback failed, err = ${JSON.stringify(error)}`)
return
}
console.info(TAG, `fetchMetadata callback success, genre: ${metadata.genre}`)
})
//获取专辑封面callback模式
avMetadataExtractor.fetchAlbumCover((err, pixelMap) => {
if (err) {
console.error(TAG, `fetchAlbumCover callback failed, err = ${JSON.stringify(err)}`)
return
}
if (callback) {
callback(err, pixelMap);
}
// 释放资源callback模式
avMetadataExtractor.release((error) => {
if (error) {
console.error(TAG, `release failed, err = ${JSON.stringify(error)}`)
return
}
console.info(TAG, `release success.`)
})
})
}
}
// 使用fs文件系统打开沙箱地址获取媒体文件地址设置dataSrc属性获取音频元数据并打印
async fetchMetadataFromDataSrc(filePath: string, callback: AsyncCallback<media.AVMetadata>) {
let fd: number = fileIo.openSync(filePath, 0o0).fd;
let fileSize: number = fileIo.statSync(filePath).size;
// 设置dataSrc描述符通过callback从文件中获取资源写入buffer中
let dataSrc: media.AVDataSrcDescriptor = {
fileSize: fileSize,
callback: (buffer, len, pos) => {
if (buffer == undefined || len == undefined || pos == undefined) {
console.error(TAG, `dataSrc callback param invalid`)
return -1
}
class Option {
offset: number | undefined = 0;
length: number | undefined = len;
position: number | undefined = pos;
}
let options = new Option();
let num = fileIo.readSync(fd, buffer, options)
console.info(TAG, 'readAt end, num: ' + num)
if (num > 0 && fileSize >= pos) {
return num;
}
return -1;
}
}
if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
// 创建AVMetadataExtractor对象
let avMetadataExtractor = await media.createAVMetadataExtractor()
// 设置dataSrc
avMetadataExtractor.dataSrc = dataSrc;
// 获取元数据promise模式
let metadata = await avMetadataExtractor.fetchMetadata()
console.info(TAG, `get meta data, mimeType: ${metadata.mimeType}`)
// 获取专辑封面promise模式
if (callback) {
callback(undefined, metadata);
}
// 释放资源promise模式
avMetadataExtractor.release()
console.info(TAG, `release data source success.`)
}
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import common from '@ohos.app.ability.common';
import util from '@ohos.util';
export enum NIMEmoticonType {
file = 1,
delete = 2,
}
export enum EmojiParseType {
// 表情
emoji = 1,
// 文本
text = 2,
}
export class NEEmojiParseResult {
// 解析类型
type: EmojiParseType = EmojiParseType.text;
//开始坐标
startIndex: number = 0
// 解析文本内容
text?: string;
// 解析表情
emoji?: NIMInputEmoticon;
}
export class NIMInputEmoticon {
type: NIMEmoticonType = NIMEmoticonType.file;
id?: string;
tag?: string;
file?: string;
}
export class EmojiDataModel {
sourceData: Array<NIMInputEmoticon> = [];
}
export class NEEmojiManager {
public static instance: NEEmojiManager = new NEEmojiManager()
dataModel: EmojiDataModel = new EmojiDataModel();
public deleteEmoji: NIMInputEmoticon = new NIMInputEmoticon();
// 根据文件名获取表情
private fileEmojiCache: Map<string, NIMInputEmoticon> = new Map();
// 根据tag获取表情
private tagEmojiCache: Map<string, NIMInputEmoticon> = new Map();
private pattern = /\[([^\]]+)\]/g;
private constructor() {
}
// 初始化表情管理器
public setup(): void {
// 获取表情列表
if (this.dataModel.sourceData.length === 0) {
this.getEmojiList();
}
}
// 获取表情列表
async getEmojiList() {
this.deleteEmoji.file = 'emoji_del_normal.png';
this.deleteEmoji.type = NIMEmoticonType.delete;
let context = getContext() as common.UIAbilityContext;
context.resourceManager.getRawFileContent('emoji_en.json', (err, rawFile) => {
if (err) {
console.error('net ease getRawFileContent failed, err is: ' + err)
return
}
try {
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
let retStr = textDecoder.decode(rawFile);
let dataModel: EmojiDataModel = JSON.parse(retStr);
this.dataModel = dataModel;
dataModel.sourceData.forEach((emoji) => {
emoji.type = NIMEmoticonType.file;
if (emoji.file !== undefined) {
this.fileEmojiCache.set(emoji.file, emoji);
}
if (emoji.tag !== undefined) {
this.tagEmojiCache.set(emoji.tag, emoji);
}
})
} catch (e) {
console.error('net ease get emoji list failed, err is: ' + e)
}
})
this.fileEmojiCache.set(this.deleteEmoji.file, this.deleteEmoji);
}
getEmojiByName(fileName: string): NIMInputEmoticon | undefined {
return this.fileEmojiCache.get(fileName)
}
getEmojiByTag(tag: string): NIMInputEmoticon | undefined {
return this.tagEmojiCache.get(tag)
}
public hasEmoji(inputString: string): boolean {
let match = this.pattern.exec(inputString)
if (match !== null) {
return true
}
return false
}
public parseEmojiText(inputString: string): Array<NEEmojiParseResult> {
// const inputString: string = "This is a [sample] input [string] with [multiple] occurrences of [brackets].";
let array = new Array<NEEmojiParseResult>();
let finish = false;
let lastMatchIndex = 0;
while (finish == false) {
let match = this.pattern.exec(inputString)
if (match !== null) {
let tag = match[0];
let emoji = this.getEmojiByTag(tag);
if (emoji !== undefined) {
const nonMatchString = inputString.substring(lastMatchIndex, match.index);
if (nonMatchString.length > 0) {
let textResult = new NEEmojiParseResult();
textResult.type = EmojiParseType.text;
textResult.text = nonMatchString;
textResult.startIndex = lastMatchIndex
array.push(textResult);
}
lastMatchIndex = match.index + match[0].length;
let emojiResult = new NEEmojiParseResult();
emojiResult.type = EmojiParseType.emoji;
emojiResult.emoji = emoji;
emojiResult.startIndex = match.index
array.push(emojiResult);
}
} else {
if (lastMatchIndex < inputString.length) {
let textResult = new NEEmojiParseResult();
textResult.type = EmojiParseType.text;
textResult.text = inputString.substring(lastMatchIndex, inputString.length);
textResult.startIndex = lastMatchIndex
array.push(textResult);
}
finish = true;
}
}
if (array.length == 0) {
let textResult = new NEEmojiParseResult();
textResult.type = EmojiParseType.text;
textResult.text = inputString;
array.push(textResult);
}
return array;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { accountAll, AitMessage } from '@nimkit/chatkit';
import { AitModel } from '@nimkit/chatkit/src/main/ets/model/ait/AitModel';
export class AitManager {
aitModel: AitModel = new AitModel()
getAitModel(): AitModel | undefined {
if (this.aitModel.aitBlocks.size > 0) {
return this.aitModel
}
return undefined
}
// 通过@文本添加@用户
addAitWithText(account: string, name: string, startIndex: number): void {
this.aitModel.addAitMember(account, name, startIndex);
}
// 清理@用户,在发送之后调用
cleanAit(): void {
this.aitModel.reset();
}
// 复制@用户信息,用户撤回消息使用
forkAit(aitContactsModel: AitModel): void {
this.aitModel.fork(aitContactsModel);
}
// @用户 是否在文本最后,如果在文本最后,需要在文本后面添加空格
aitEnd(text: string): boolean {
const len = text.length;
for (const element of this.aitModel.aitBlocks.values()) {
for (const segment of element.segments) {
if (segment.start < len && segment.end >= len) {
return true;
}
}
}
return false;
}
/**
* 根据插入后的Text 文案, segment 移位或者删除。
* 返回被删除的AitMsg信息
* @param deletedText 删除后的字符串
* @param endIndex 删除的结束位置
* @param length 删除的长度
* @returns
*/
deleteAitWithText(deletedText: string, endIndex: number, length: number): AitMessage | null {
return this.aitModel.deleteAitUser(deletedText, endIndex, length);
}
/**
* 删除文本
* @param endIndex
* @param length
*/
deleteText(endIndex: number, length: number) {
this.aitModel.deleteText(endIndex, length)
}
/**
* 新增Text输入但输入的不是@
* 会进行移位或者删除,如果在@XXX 中插入文本 @XXX 会被删除
* @param endIndex 输入的结束位置
* @param length 输入的长度
*/
addTextWithoutAit(endIndex: number, length: number): void {
this.aitModel.insertText(endIndex, length);
}
// 获取需要推送的用户列表
getPushList(): string[] | undefined {
const pushList: string[] = [];
for (const key of this.aitModel.aitBlocks.keys()) {
if (key === accountAll) {
// pushList.length = 0;
// const teamMembers: string[] = TeamMemberCache.getInstance().getAllMemberAccounts()
// pushList.push(...teamMembers);
return pushList;
} else {
pushList.push(key);
}
}
if (pushList.length > 0) {
return pushList;
}
return undefined
}
// 是否已经在@列表中
haveBeAit(account: string): boolean {
return this.aitModel.aitBlocks.has(account)
}
// 是否有@成员
haveAitMember(): boolean {
return Object.keys(this.aitModel.aitBlocks).length > 0;
}
// 光标移动到@后自动到后面
resetAitCursor(baseIndex: number): number {
for (const aitMsg of this.aitModel.aitBlocks.values()) {
for (const segment of aitMsg.segments) {
if (segment.start < baseIndex && segment.end + 1 > baseIndex) {
return segment.end + 1;
}
}
}
return baseIndex;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { AitSegment } from '@nimkit/chatkit/src/main/ets/model/ait/AitSegment';
export interface ChatAitNode {
//文本
text: string,
//片段
segment?: AitSegment
//accId
account?: string
}

View File

@ -0,0 +1,429 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatKitClient } from '@nimkit/chatkit';
import { V2NIMMessagePin } from '@nimsdk/base';
import { V2NIMMessage, V2NIMMessageSendingState } from '@nimsdk/base/src/main/ets/nim/sdk/V2NIMMessageService';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
@ObservedV2
export class ChatInfo {
@Trace conversationName: string | undefined = undefined;
@Trace msgList: MessageDataSource = new MessageDataSource();
@Trace downloadProgressMap: Map<string, number> = new Map();
@Trace isReceiveMsg: boolean = false;
@Trace scrollIndex: number = -1; // 是否需要滚动到指定位置
msgMap: Map<string, NIMMessageInfo> = new Map();
@Trace msgPinMap: Map<string, V2NIMMessagePin> = new Map();
conversationId: string = '';
targetId: string = '';
constructor(conversationId: string) {
this.conversationId = conversationId;
this.targetId = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(conversationId)
}
setConversationId(conversationId: string) {
this.conversationId = conversationId;
this.targetId = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(conversationId)
}
setReceiveMsg(receiveMsg: boolean) {
this.isReceiveMsg = receiveMsg;
}
setScrollIndex(index: number) {
this.scrollIndex = index
}
getConversationName(): string {
return '';
};
getChatUserAvatarUrl(param?: string | V2NIMMessage): string {
return '';
};
getChatUserAvatarName(param?: string | V2NIMMessage): string {
if (param as V2NIMMessage) {
return (param as V2NIMMessage).senderId
}
return param as string;
};
getChatUserShowName(param?: string | V2NIMMessage, alias: boolean = true, teamNick: boolean = true): string {
if (param as V2NIMMessage) {
return (param as V2NIMMessage).senderId
}
return param as string;
};
getCurrentUserShowName(): string {
return this.targetId;
};
getCurrentUserAvatarUrl(message?: V2NIMMessage): string {
return '';
};
getCurrentUserAvatarName(): string {
return this.targetId;
};
updateMessageStatus(message: V2NIMMessage): boolean {
if (message.sendingState === V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING) {
return false
}
let targetMessage =
this.msgList.getMessageList().find(msg => msg.message.messageClientId === message.messageClientId);
if (targetMessage !== undefined) {
targetMessage.updateMessageStatus(message)
// 如果是消息 message crateTime 》 lastMessage crateTime 需要调整消息位置
this.moveMessage(message)
return true
} else {
this.insertMessage(message)
return false
}
}
addPinMessage(pinMsg: V2NIMMessagePin[]) {
pinMsg.forEach(pin => {
this.msgPinMap.set(pin.messageRefer.messageClientId, pin)
this.msgMap.get(pin.messageRefer.messageClientId)?.setPinMessage(pin)
})
}
resetPinMessage(pinMsg: V2NIMMessagePin[]) {
this.msgPinMap.clear()
this.msgList.messageData.forEach(msg => {
msg.setPinMessage(undefined)
})
pinMsg.forEach(pin => {
this.msgPinMap.set(pin.messageRefer.messageClientId, pin)
this.msgMap.get(pin.messageRefer.messageClientId)?.setPinMessage(pin)
})
}
removePinMessage(pinMsg: V2NIMMessagePin[]) {
pinMsg.forEach(pin => {
this.msgPinMap.delete(pin.messageRefer.messageClientId)
this.msgMap.get(pin.messageRefer.messageClientId)?.setPinMessage(undefined)
})
}
updateMessageReadReceipt(message: NIMMessageInfo) {
}
moveMessage(message: V2NIMMessage){
// 如果是消息 message crateTime 》 lastMessage crateTime 需要调整消息位置
const copiedArray = [...this.msgList.messageData];
const oldIndex = this.msgList.searchPosition(message.messageClientId)
const updatedElement = copiedArray[oldIndex];
// 按新的createTime降序排序
const sortedArray = [...copiedArray].sort((a, b) => a.message.createTime - b.message.createTime);
const newIndex = sortedArray.findIndex(element => element === updatedElement)
// 位置发生变换进行reload
if (oldIndex != newIndex) {
this.msgList.reloadData()
}
}
insertMessage(message: V2NIMMessage): NIMMessageInfo {
let addIndex = this.msgList.searchInsertPosition(message.createTime)
if (addIndex > this.msgList.totalCount()) {
let result = this.pushMessage(message)
this.setReceiveMsg(true)
return result
} else {
let lastMessageTime = 0
if (addIndex > 0) {
lastMessageTime = this.msgList.getMessageList()[addIndex - 1]?.message.createTime;
}
let msgInfo = new NIMMessageInfo(message)
msgInfo.setLastMessageTime(lastMessageTime)
this.updateMessageReadReceipt(msgInfo)
this.msgMap.set(message.messageClientId, msgInfo)
if (this.msgPinMap.has(message.messageClientId)) {
msgInfo.setPinMessage(this.msgPinMap.get(message.messageClientId))
}
this.msgList.addData(addIndex, msgInfo)
// 非尾部插入。
// 该情况下可能展示时间,但是可能与新的一条消息时间间隔太小,会重复展示相同时间。因此该条展示后,需要检查与下条的展示时间是否相同。若相同,则隐藏下条消息时间。
const changeIndex = addIndex + 1
if (changeIndex < this.msgList.totalCount()) {
const nextMsg: NIMMessageInfo = this.msgList.getData(changeIndex)
nextMsg.setLastMessageTime(message.createTime)
this.msgList.replaceData(changeIndex, nextMsg)
}
return msgInfo;
}
}
pushMessage(message: V2NIMMessage): NIMMessageInfo {
let current = this.getMessage(message.messageClientId)
if (current !== undefined) {
return current;
}
let lastMessageTime = 0
if (this.msgList.totalCount() > 0) {
lastMessageTime = this.msgList.getMessageList()[this.msgList.totalCount() - 1]?.message.createTime;
}
let msgInfo = new NIMMessageInfo(message)
msgInfo.setLastMessageTime(lastMessageTime)
this.updateMessageReadReceipt(msgInfo)
this.msgList.push(msgInfo);
this.msgMap.set(message.messageClientId, msgInfo)
if (this.msgPinMap.has(message.messageClientId)) {
msgInfo.setPinMessage(this.msgPinMap.get(message.messageClientId))
}
return msgInfo;
}
pushModifyMessage(message: V2NIMMessage): NIMMessageInfo | undefined {
const current: NIMMessageInfo | undefined = this.getMessage(message.messageClientId)
if (current !== undefined) {
const idx: number = this.msgList.searchPosition(message.messageClientId) // TODO 目前以遍历方式更新modify 频繁情况下(如流式消息)存在性能优化项。临时方案
if (idx >= 0) {
current.message = message
this.msgList.getData(idx).message = message
this.msgList.getData(idx).messageHeight = -1 // 重置高度,重新计算高度
this.msgList.notifyDataChange(idx)
}
return current;
} else {
return undefined
}
}
pushMessageInfo(msgInfo: NIMMessageInfo) {
let current = this.getMessage(msgInfo.message.messageClientId)
if (current !== undefined) {
return;
}
let lastMessageTime = this.msgList.getMessageList()[this.msgList.totalCount() - 1]?.message.createTime;
msgInfo.setLastMessageTime(lastMessageTime)
this.updateMessageReadReceipt(msgInfo)
this.msgMap.set(msgInfo.message.messageClientId, msgInfo)
if (this.msgPinMap.has(msgInfo.message.messageClientId)) {
msgInfo.setPinMessage(this.msgPinMap.get(msgInfo.message.messageClientId))
}
this.msgList.push(msgInfo);
}
unshiftMessage(messageList: V2NIMMessage[]): NIMMessageInfo[] {
let result: NIMMessageInfo[] = [];
for (let index = 0; index < messageList.length; index++) {
let msg = new NIMMessageInfo(messageList[index]);
if (index < messageList.length - 1) {
msg.setLastMessageTime(messageList[index+1].createTime);
}
this.msgMap.set(msg.message.messageClientId, msg)
if (this.msgPinMap.has(msg.message.messageClientId)) {
msg.setPinMessage(this.msgPinMap.get(msg.message.messageClientId))
}
this.msgList.unshift(msg)
result.push(msg);
}
return result;
}
deleteMessage(msgClientId: string) {
this.msgList.delete(msgClientId)
this.msgMap.delete(msgClientId)
}
revokeMessage(msgClientId: string) {
this.deleteMessage(msgClientId)
}
getMessage(msgClientId: string): NIMMessageInfo | undefined {
return this.msgMap.get(msgClientId)
}
searchPosition(msgClientId: string): number {
return this.msgList.searchPosition(msgClientId)
}
public cleanMessage() {
this.msgList.clean()
this.msgMap.clear()
}
}
// Basic implementation of IDataSource to handle data listener
export class MessageDataSource implements IDataSource {
messageData: NIMMessageInfo[] = [];
private listeners: DataChangeListener[] = [];
public totalCount(): number {
return this.messageData.length;
}
public getData(index: number): NIMMessageInfo {
return this.messageData[index];
}
public addData(index: number, data: NIMMessageInfo): void {
this.messageData.splice(index, 0, data);
this.notifyDataAdd(index);
}
public replaceData(index: number, data: NIMMessageInfo): void {
this.messageData.splice(index, 1, data);
this.notifyDataChange(index);
}
public push(data: NIMMessageInfo): void {
this.messageData.push(data);
this.notifyDataAdd(this.messageData.length - 1);
}
public unshift(data: NIMMessageInfo): void {
this.messageData.unshift(data);
this.notifyDataAdd(0);
}
public unshiftList(data: NIMMessageInfo[]): void {
data.forEach(element => {
this.messageData.unshift(element);
})
this.notifyDatasetChange([{
type: DataOperationType.ADD,
index: 0,
count: data.length
}]);
}
public pushList(data: NIMMessageInfo[]): void {
data.forEach(element => {
this.messageData.push(element);
});
this.notifyDataAdd(this.messageData.length - 1);
}
public delete(msgClientId: string) {
this.messageData.forEach((msg, index, msgList) => {
if (msgClientId === msg.message.messageClientId) {
this.messageData.splice(index, 1)
this.notifyDataDelete(index)
return
}
})
}
public deleteWithIndex(index: number) {
if (index >= 0 && index < this.messageData.length) {
this.messageData.splice(index, 1)
this.notifyDataDelete(index)
}
}
public reloadData(): void {
// 1. 元素重新排序
this.messageData.sort((a, b) => a.message.createTime - b.message.createTime)
// 2. 通知视图更新
this.notifyDataReload()
}
public searchInsertPosition(creatTime: number): number {
for (let index = this.messageData.length - 1; index >= 0; index--) {
if (creatTime > this.messageData[index].getCreateTime()) {
return index + 1;
}
}
return 0;
}
public searchPosition(clientId: string): number {
for (let index = this.messageData.length - 1; index >= 0; index--) {
if (clientId == this.messageData[index].getMessageClientId()) {
return index;
}
}
return -1;
}
public clean() {
while (this.messageData.length > 0) {
this.messageData.pop()
}
}
public getMessageList(): NIMMessageInfo[] {
return this.messageData;
}
// 该方法为框架侧调用为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// 该方法为框架侧调用为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
// 通知LazyForEach组件在index对应索引处数据有变化需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDatasetChange(operation: DataAddOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operation);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDatasetReload(operation: DataReloadOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operation);
})
}
}

View File

@ -0,0 +1,227 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatInfo } from './ChatInfo';
import { V2NIMFriend, V2NIMMessage, V2NIMTeamMessageReadReceipt, V2NIMUser } from '@nimsdk/base';
import { V2NIMTeam, V2NIMTeamMember } from '@nimsdk/base/src/main/ets/nim/sdk/V2NIMTeamService';
import { NIMMessageInfo } from './NIMMessageInfo';
import { NEFriendUserCache } from '@nimkit/chatkit/src/main/ets/NEFriendUserCache';
import { ChatKitClient } from '@nimkit/chatkit';
@ObservedV2
export class ChatTeamInfo extends ChatInfo {
@Trace team?: V2NIMTeam | undefined = undefined;
@Trace teamMemberMap = new Map<string, V2NIMTeamMember>();
@Trace teamUserMap = new Map<string, V2NIMUser>();
// 用于动态监听好友信息变更时,更新会话页面好友数据
@Trace teamUserFriendMap = new Map<string, V2NIMFriend>();
teamMessageReadReceipt = new Map<string, V2NIMTeamMessageReadReceipt>();
constructor(conversationId: string) {
super(conversationId)
this.conversationName = conversationId;
}
setConversationId(conversationId: string): void {
super.setConversationId(conversationId)
this.conversationName = conversationId;
}
setTeam(team: V2NIMTeam | undefined) {
this.team = team;
if (team !== undefined && team.name != '') {
this.conversationName = team.name;
}
}
setTeamMember(member: V2NIMTeamMember) {
this.teamMemberMap.set(member.accountId, member);
}
setTeamReadReadReceipt(readReceipts: V2NIMTeamMessageReadReceipt[]) {
readReceipts.forEach((readReceipt) => {
this.teamMessageReadReceipt.set(readReceipt.messageClientId, readReceipt);
let msg = this.getMessage(readReceipt.messageClientId);
msg?.setReadCount(readReceipt.readCount, readReceipt.unreadCount)
})
}
pushDataList(msgList: NIMMessageInfo[]) {
msgList.forEach(element => {
this.msgList.push(element);
});
}
addTeamUser(userList: V2NIMUser[]) {
userList.forEach((user) => {
this.teamUserMap.set(user.accountId, user);
});
}
addTeamFriend(friend: V2NIMFriend) {
this.teamUserFriendMap.set(friend.accountId, friend);
}
addTeamMember(member: V2NIMTeamMember[]) {
member.forEach((member) => {
this.teamMemberMap.set(member.accountId, member);
});
}
getTeamMember(accountId: string): V2NIMTeamMember | undefined {
if (this.teamMemberMap.has(accountId)) {
return this.teamMemberMap.get(accountId);
}
return undefined;
}
getChatUserAvatarUrl(param?: string | V2NIMMessage): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
if (this.teamUserMap.has(accountId)) {
return this.teamUserMap.get(accountId)?.avatar ?? '';
}
return '';
}
getCurrentUserAvatarUrl(): string {
let accountId = ChatKitClient.getLoginUserId()
if (this.teamUserMap.has(accountId)) {
return this.teamUserMap.get(accountId)?.avatar ?? '';
}
return '';
}
getChatUserAvatarName(param?: string | V2NIMMessage): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
let result = '';
if (this.teamUserFriendMap.has(accountId)) {
let friend = this.teamUserFriendMap.get(accountId)
if (friend && friend.alias !== undefined) {
result = friend.alias ?? ''
}
} else if (NEFriendUserCache.getInstance().isFriend(accountId)) {
let friendInfo = NEFriendUserCache.getInstance().getFriendById(accountId)
if (friendInfo !== undefined) {
result = friendInfo.shortName(true)
}
}
if (result == null || result.trim() === '') {
result = this.teamUserMap.get(accountId)?.name ?? '';
}
if (result == null || result.trim() === '') {
result = accountId;
}
if (result !== '' && result.length > 2) {
result = result.substring(result.length - 2, result.length)
}
return result
}
getCurrentUserAvatarName(): string {
let accountId = ChatKitClient.getLoginUserId()
let result = '';
if (result == null || result.trim() === '') {
result = this.teamUserMap.get(accountId)?.name ?? '';
}
if (result == null || result.trim() === '') {
result = accountId;
}
if (result !== '' && result.length > 2) {
result = result.substring(result.length - 2, result.length)
}
return result
}
getChatUserShowName(param?: string | V2NIMMessage, alias: boolean = true, teamNick: boolean = true): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
let result = '';
if (this.teamUserFriendMap.has(accountId)) {
let friend = this.teamUserFriendMap.get(accountId)
if (friend && friend.alias !== undefined) {
result = friend.alias ?? ''
}
}
let friendInfo = NEFriendUserCache.getInstance().getFriendById(accountId)
//1,备注优先
if (alias && (result == null || result.trim() === '') && friendInfo) {
result = friendInfo.getAlias() ?? ''
}
//2群昵称次之
if (teamNick && (result == null || result.trim() === '') && this.teamMemberMap.has(accountId)) {
result = this.teamMemberMap.get(accountId)?.teamNick ?? '';
}
//3好友名称再次之
if ((result == null || result.trim() === '') && NEFriendUserCache.getInstance().isFriend(accountId)) {
if (friendInfo !== undefined) {
result = friendInfo.showName(alias)
}
}
if (result == null || result.trim() === '') {
result = this.teamUserMap.get(accountId)?.name ?? '';
}
if (result == null || result.trim() === '') {
result = accountId;
}
return result;
}
getCurrentUserShowName(): string {
let result = '';
let accountId = ChatKitClient.getLoginUserId()
if (this.teamMemberMap.has(accountId)) {
result = this.teamMemberMap.get(accountId)?.teamNick ?? '';
}
if (result == null || result.trim() === '') {
result = this.teamUserMap.get(accountId)?.name ?? '';
}
if (result == null || result.trim() === '') {
result = accountId;
}
return result;
}
/**
* 获取用成员用户信息
* @param accountId
* @returns
*/
getMemberUserById(accountId: string) {
return this.teamUserMap.get(accountId)
}
/**
* 获取当前成员的群管理员
* @returns
*/
getCurrentUserTeamMember() {
let accId = ChatKitClient.getLoginUserId()
return this.teamMemberMap.get(accId)
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatKitClient, NEFriendUserCache } from '@nimkit/chatkit';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { V2NIMFriend, V2NIMMessage, V2NIMP2PMessageReadReceipt, V2NIMUser } from '@nimsdk/base';
import { ChatInfo } from './ChatInfo';
@ObservedV2
export class ChatUserInfo extends ChatInfo {
@Trace currentUser: V2NIMUser | undefined = undefined;
@Trace chatUser: V2NIMUser | undefined = undefined;
@Trace chatFriend: V2NIMFriend | undefined = undefined;
readReceipt: V2NIMP2PMessageReadReceipt | undefined = undefined;
readReceiptMsg: NIMMessageInfo | undefined = undefined;
constructor() {
super('');
}
setConversationId(conversationId: string): void {
super.setConversationId(conversationId)
this.conversationName = this.getConversationName();
}
setCurrentUser(user: V2NIMUser | undefined) {
this.currentUser = user;
}
setChatUser(user: V2NIMUser | undefined) {
this.chatUser = user;
this.conversationName = this.getConversationName();
}
setChatFriend(friend: V2NIMFriend | undefined) {
this.chatFriend = friend;
}
updateConversationName() {
this.conversationName = this.getConversationName();
}
setMessageReadReceipt(readReceipt: V2NIMP2PMessageReadReceipt | undefined) {
if (readReceipt == undefined || readReceipt.timestamp == undefined) {
return
}
this.readReceipt = readReceipt
let readCount = -1
for (let index = super.msgList.totalCount() - 1; index >= 0; index--) {
if (super.msgList.messageData[index].readCount > 0) {
break
} else if (readCount == -1) {
let hasRead = readReceipt?.timestamp >= super.msgList.messageData[index].getCreateTime()
readCount = hasRead ? 1 : 0;
}
super.msgList.messageData[index].readCount = readCount
}
}
updateMessageReadReceipt(message: NIMMessageInfo) {
super.updateMessageReadReceipt(message)
if (this.readReceipt == undefined) {
return
}
let hasRead = this.readReceipt?.timestamp >= message.getCreateTime()
message.readCount = hasRead ? 1 : 0;
}
getChatUserAvatarName(param?: string | V2NIMMessage): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
if (accountId === ChatKitClient.getLoginUserId()) {
return this.getCurrentUserAvatarName()
}
let name = '';
if (this.chatFriend) {
name = this.chatFriend?.alias ?? '';
}
if (name === '' && NEFriendUserCache.getInstance().getFriendById(accountId)) {
let friendInfo = NEFriendUserCache.getInstance().getFriendById(accountId)
if (friendInfo !== undefined) {
name = friendInfo.showName()
}
}
if (name === '' && this.chatUser !== undefined) {
name = this.chatUser?.name ?? '';
}
if (name === '') {
name = accountId;
}
if (name !== '' && name.length > 2) {
name = name.substring(name.length - 2, name.length)
}
return name;
}
getCurrentUserAvatarName(): string {
let name = '';
let accountId = ChatKitClient.getLoginUserId();
if (this.currentUser !== undefined) {
name = this.currentUser?.name ?? '';
}
if (name === '') {
name = accountId;
}
if (name !== '' && name.length > 2) {
name = name.substring(name.length - 2, name.length)
}
return name;
}
getChatUserShowName(param?: string | V2NIMMessage, alias: boolean = true, teamNick: boolean = true): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
if (accountId === ChatKitClient.getLoginUserId()) {
return this.getCurrentUserShowName()
}
let name = '';
let friendInfo = NEFriendUserCache.getInstance().getFriendById(accountId)
if (friendInfo !== undefined) {
name = friendInfo.showName(alias)
}
if (name === '' && this.chatUser !== undefined) {
name = this.chatUser?.name ?? '';
}
if (name === '') {
name = accountId;
}
return name;
}
getCurrentUserShowName(): string {
let name = '';
if (name === '' && this.currentUser !== undefined) {
name = this.currentUser?.name ?? '';
}
if (name === '') {
name = ChatKitClient.getLoginUserId();
}
return name;
}
getChatUserAvatarUrl(param?: string | V2NIMMessage): string {
let accountId: string = ""
if ((param as V2NIMMessage).senderId) {
accountId = (param as V2NIMMessage).senderId
} else {
accountId = param as string;
}
if (accountId === ChatKitClient.getLoginUserId()) {
return this.getCurrentUserAvatarUrl()
}
let result: string = '';
if (this.chatUser !== undefined) {
result = this.chatUser?.avatar ? this.chatUser?.avatar : '';
}
if (result == null || result.trim() === '') {
result = '';
}
return result;
}
getCurrentUserAvatarUrl(): string {
let result: string = '';
if (this.currentUser !== undefined) {
result = this.currentUser?.avatar ? this.currentUser?.avatar : '';
}
if (result == null || result.trim() === '') {
result = '';
}
return result;
}
getConversationName(): string {
let result: string = '';
let friendInfo = NEFriendUserCache.getInstance().getFriendById(this.targetId)
if (friendInfo !== undefined) {
result = friendInfo.showName(true)
} else if (this.chatUser !== undefined) {
result = this.chatUser?.name !== null ? this.chatUser?.name : this.chatUser?.accountId;
}
return result;
}
}

View File

@ -0,0 +1,30 @@
import { mergedMessageAvatarKey, mergedMessageNickKey } from '@nimkit/chatkit';
import { TailString } from '@nimkit/common';
import { V2NIMMessage } from '@nimsdk/base';
import { ChatInfo } from '../model/ChatInfo';
@ObservedV2
export class MergeMessageInfo extends ChatInfo {
getServerExtensionValue(key: string, param?: string | V2NIMMessage | undefined): string {
if (param as V2NIMMessage) {
const message = param as V2NIMMessage
if (message.serverExtension) {
const remoteExt = JSON.parse(message.serverExtension) as object
return remoteExt[key]
}
}
return ""
}
getChatUserAvatarUrl(param?: string | V2NIMMessage | undefined): string {
return this.getServerExtensionValue(mergedMessageAvatarKey, param)
}
getChatUserAvatarName(param?: string | V2NIMMessage | undefined): string {
return TailString(this.getChatUserShowName(param))
}
getChatUserShowName(param?: string | V2NIMMessage): string {
return this.getServerExtensionValue(mergedMessageNickKey, param)
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export enum MessageOperationType {
// 删除
Delete = 1,
// 复制
Copy = 2,
// 撤回
Undo = 3,
// 转发
Forward = 4,
//标记
Pin = 5,
//取消标记
Unpin = 6,
// 多选
Select = 7,
// 收藏
Collection = 8,
// 回复
Reply = 9
}
export class MessageOperationItem {
// 操作提示图片
operationImage?: ResourceStr;
// 操作提示文字
operationText?: ResourceStr;
// 操作类型
operationType: MessageOperationType = MessageOperationType.Delete;
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
export enum NEChatMoreOperationType {
Video = 1,
File = 2,
Location = 3,
Reply = 4,
Teach = 5,
Outpatient=6,
Shopping=7,
Hospital=8,
Image=8
}
export class NEChatMoreOperationData {
type?: NEChatMoreOperationType;
operationTitle?: ResourceStr;
imageSource?: string;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
@ObservedV2
export class NERectData {
@Trace
x: number = 0;
@Trace
y: number = 0;
@Trace
width: number = 0;
@Trace
height: number = 0;
}

View File

@ -0,0 +1,358 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
V2NIMMessage,
V2NIMMessageAttachment,
V2NIMMessageImageAttachment,
V2NIMMessageType
} from '@nimsdk/base/src/main/ets/nim/sdk/V2NIMMessageService'
import {
ChatKitClient,
ChatRepo,
CustomMessageUtils,
keyReplyMsgKey,
mergedMessageCustomType,
StorageRepo
} from '@nimkit/chatkit'
import { DateUtils } from '../common/DateUtils'
import { fileUri } from '@kit.CoreFileKit'
import { ChatConst } from '../constants/ChatConst'
import { JSONUtil } from '@nimkit/common'
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit'
import { V2NIMGetMediaResourceInfoResult, V2NIMMessageAIStreamStatus, V2NIMMessagePin } from '@nimsdk/base'
import { ChatKitConfig } from '../ChatKitConfig'
import { getMergedMessageContent, measureMessageHeight } from '../common/MessageHelper'
import fs from '@ohos.file.fs'
@ObservedV2
export class NIMMessageInfo {
// IM SDK 层的消息对象
@Trace message: V2NIMMessage
// 未读数量
@Trace unReadCount: number = 100
// 已读数量
@Trace readCount: number = -1
// 消息下载或上传进度
@Trace downloadProgress: number = -1
@Trace messageHeight = -1
// 消息附件
attachment: V2NIMMessageAttachment | null = null
// 自定义消息附件
customAttachment: object | null = null
// 上一条消息的发送时间,用于判断该消息展示时候是否需要展示发送时间
@Trace lastMessageTime: number = 0
// 是否是接收消息UI渲染时使用
isReceiveMsg: boolean = false
// 是否为撤回消息
isRevokeMsg: boolean = false
// 是否为撤回可编辑
@Trace revokeEditMsg: boolean = false
// 是否为PIN消息
@Trace isPinMsg: boolean = false
// 是否为合并转发消息
isMergeMsg: boolean = false
mergedContent: string = ''
// 是否为合并转发详情页中的消息
isMergeDetailMsg: boolean = false
// 是否为多选选中
@Trace isSelectedMsg: boolean = false
//是否为回复消息
@Trace
isReplyMsg: boolean = false
// 回复消息
@Trace replyMsg: NIMMessageInfo | undefined = undefined
// 撤回消息扩展
revokeInfo: RevokeInfo | undefined = undefined
//pin信息
pinInfo: V2NIMMessagePin | undefined = undefined
constructor(msg: V2NIMMessage) {
this.message = msg
this.parseMessage(msg)
}
async parseMessage(message: V2NIMMessage) {
this.isReceiveMsg = this.message.senderId != ChatKitClient.getLoginUserId()
// 解析撤回
this.revokeInfo = RevokeInfo.parseRevokeInfo(message)
if (this.revokeInfo != null) {
this.isRevokeMsg = true
this.revokeEditMsg = this.revokeInfo?.isEditMsg &&
(systemDateTime.getTime() - this.revokeInfo.revokeTime < ChatKitConfig.messageRevokeTimeLimit)
}
// 解析合并转发
if (message.messageType == V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM && message.attachment != null) {
this.customAttachment = CustomMessageUtils.dataOfCustomMessage(message.attachment)
let customType = CustomMessageUtils.typeOfCustomMessage(message.attachment)
if (customType == mergedMessageCustomType) {
this.isMergeMsg = true
this.mergedContent = getMergedMessageContent(this.customAttachment)
}
}
// 解析回复
this.parseReply(message)
this.messageHeight = -1
}
// @Computed
// get isReplyMsg():boolean{
// return this.replyMsg !== undefined
// }
setIsReply(reply: boolean) {
this.isReplyMsg = reply
}
async parseReply(message: V2NIMMessage) {
// 优先使用 thread 方案
if (message.threadReply) {
this.isReplyMsg = true
try {
const messages = await ChatRepo.getMessageListByRefers([message.threadReply])
if (messages.length > 0) {
this.replyMsg = new NIMMessageInfo(messages[0])
}
} catch (err) {
console.error(err)
}
return
}
// 非 thread 方案
const remoteExt = message.serverExtension
if (remoteExt) {
try {
const remoteDic = JSON.parse(remoteExt) as object
const msgReplyDic = remoteDic[keyReplyMsgKey] as object
if (msgReplyDic) {
try {
this.isReplyMsg = true
const messages = await ChatRepo.getMessageListByRefers([{
messageClientId: msgReplyDic['idClient'] as string,
messageServerId: msgReplyDic['idServer'] as string,
senderId: msgReplyDic['from'] as string,
createTime: msgReplyDic['time'] as number,
conversationId: msgReplyDic['to'] as string,
receiverId: msgReplyDic['receiverId'] as string,
conversationType: msgReplyDic['scene'] as number,
}]).catch((err: BusinessError) => {
console.debug('netease parseReply', err.message)
})
if (messages && messages.length > 0) {
this.replyMsg = new NIMMessageInfo(messages[0])
}
} catch (err) {
console.error(err)
}
}
} catch (err) {
console.error(err)
}
}
}
checkRevokeEdit(): boolean {
if (this.revokeInfo != null) {
this.isRevokeMsg = true
this.revokeEditMsg = this.revokeInfo?.isEditMsg &&
(systemDateTime.getTime() - this.revokeInfo.revokeTime < ChatKitConfig.messageRevokeTimeLimit)
return this.revokeEditMsg
}
return false
}
configReadReceipt(): boolean {
return this.message.messageConfig?.readReceiptEnabled ?? false
}
updateMessageStatus(message: V2NIMMessage) {
this.message = message
}
setLastMessageTime(time: number) {
this.lastMessageTime = time
}
setSelected(isSelected: boolean) {
this.isSelectedMsg = isSelected
}
getMessageHeight(context: UIContext): number {
if (this.messageHeight < 0) {
this.messageHeight = measureMessageHeight(context, this)
}
return this.messageHeight
}
setPinMessage(pinMsg: V2NIMMessagePin | undefined) {
if (pinMsg !== undefined) {
this.pinInfo = pinMsg
this.isPinMsg = true
} else {
this.isPinMsg = false
this.pinInfo = undefined
}
this.messageHeight = -1
}
setDownloadProgress(progress: number) {
this.downloadProgress = progress
}
setReadCount(readCount: number, unreadCount: number) {
this.unReadCount = unreadCount
this.readCount = readCount
}
getCreateTime(): number {
return this.message.createTime
}
// 获取消息时间
getMessageFormatTime(): string {
return DateUtils.formatTime(this.message.createTime, this.lastMessageTime)
}
// 获取消息时间(根据消息时间展示的间隔)
getMessageTime(): string {
let result = ''
if (this.message.createTime - this.lastMessageTime > ChatKitConfig.messageTimeGap) {
result = DateUtils.formatTime(this.message.createTime, this.lastMessageTime)
}
return result
}
getConversationId() {
return this.message.conversationId
}
getConversationType() {
return ChatKitClient.nim.conversationIdUtil.parseConversationType(this.getConversationId())
}
getMessageClientId(): string {
return this.message.messageClientId
}
getAvatarName(): string {
let result = ''
if (this.message != null && this.message != null) {
result = this.message.senderId.substring(this.message.senderId.length - 2, this.message.senderId.length)
}
if (this.message != null && this.message.senderId != null) {
result = this.message.senderId.substring(this.message.senderId.length - 2, this.message.senderId.length)
}
return result
}
getMessageType(): V2NIMMessageType {
return this.message.messageType
}
isAiStreamMessage(): boolean {
return this.message.aiConfig?.aiStream ?? false
}
isFinishedAiStream(): boolean {
return (this.message.aiConfig?.aiStreamStatus ??
V2NIMMessageAIStreamStatus.V2NIM_MESSAGE_AI_STREAM_STATUS_GENERATED) >
V2NIMMessageAIStreamStatus.V2NIM_MESSAGE_AI_STREAM_STATUS_PLACEHOLDER
}
isReceiveMessage(): boolean {
return this.isReceiveMsg
}
isPinMessage(): boolean {
return this.isPinMsg
}
isMergeMessage(): boolean {
return this.isMergeMsg
}
isMergeDetailMessage(): boolean {
return this.isMergeDetailMsg
}
getImageUrl(): string {
if (this.message.messageType == V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
let iamgeAttachment = this.message.attachment as V2NIMMessageImageAttachment
if (iamgeAttachment) {
if (iamgeAttachment.path) {
if (fs.accessSync(iamgeAttachment.path)) {
const uri = fileUri.getUriFromPath(iamgeAttachment.path)
return uri
}
}
return iamgeAttachment.url ?? ''
}
}
return ''
}
async getImageThumbUrl(): Promise<string> {
if (this.message.messageType == V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
let iamgeAttachment = this.message.attachment as V2NIMMessageImageAttachment
if (iamgeAttachment) {
if (iamgeAttachment.path) {
if (fs.accessSync(iamgeAttachment.path)) {
const uri = fileUri.getUriFromPath(iamgeAttachment.path)
return uri
}
}
const thumbResult: V2NIMGetMediaResourceInfoResult | undefined =
await StorageRepo.getImageThumbUrl(iamgeAttachment, {
width: ChatConst.imageMessageWidth
})
if (!thumbResult) {
return iamgeAttachment.url ?? ''
} else {
return thumbResult.url ?? ''
}
}
}
return ''
}
}
export class RevokeInfo {
isLocalRevoke: boolean = false
revokeTime: number = 0
isEditMsg: boolean = false
revokeMsgText: string = ''
revokeMsgClientId: string = ''
static parseRevokeInfo(msg: V2NIMMessage): RevokeInfo | undefined {
if (msg == undefined || msg.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT ||
msg.localExtension == undefined
|| !msg.localExtension?.includes(ChatConst.revokeLocalKey)) {
return undefined
}
let localExtension = msg.localExtension
if (JSONUtil.isJSONString(localExtension)) {
let localObject = JSON.parse(localExtension) as object
let revokeInfo = new RevokeInfo()
revokeInfo.revokeMsgText = localObject?.[ChatConst.revokeMsgTextKey]
revokeInfo.revokeMsgClientId = localObject?.[ChatConst.revokeMsgClientIdKey]
revokeInfo.revokeTime = localObject?.[ChatConst.revokeLocalTimeKey]
revokeInfo.isLocalRevoke = localObject?.[ChatConst.revokeLocalKey]
revokeInfo.isEditMsg = localObject?.[ChatConst.revokeMsgEditKey]
return revokeInfo
}
return undefined
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { V2NIMTeam, V2NIMTeamMember, V2NIMUser } from '@nimsdk/base';
export class TeamDataResult {
team?: V2NIMTeam | undefined = undefined;
teamMemberList: V2NIMTeamMember[] = [];
teamUserList: V2NIMUser[] = [];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatKitClient, TeamMemberWithUser } from '@nimkit/chatkit'
import { ChatReadReceiptViewModel } from '../viewmodel/ChatReadReceiptViewModel'
import { AvatarColorUntil, AvatarItem, CommonAvatar, CommonEmptyResult, NavigationBackBuilder } from '@nimkit/common'
import { V2NIMMessage } from '@nimsdk/base'
@ComponentV2
export struct ChatReadReceiptPage {
pathStack: NavPathStack = new NavPathStack()
//群Id
teamId?: string
//消息
message?: V2NIMMessage
@Local viewModel: ChatReadReceiptViewModel = new ChatReadReceiptViewModel()
build() {
NavDestination() {
NavigationBackBuilder({
title: $r('app.string.chat_read_receipt_title'),
backgroundColor: '#ffffffff',
leftButtonAction: () => {
this.pathStack.pop()
}
})
if (this.viewModel.readReceiptDetail !== null) {
Tabs({
barPosition: BarPosition.Start
}) {
TabContent() {
if (this.viewModel.readReceiptDetail.readAccountList.length <= 0) {
CommonEmptyResult({
tips: $r('app.string.chat_read_receipt_all_unread')
})
} else {
List() {
ForEach(this.viewModel.readMemberList, (member: TeamMemberWithUser) => {
ListItem() {
Row() {
CommonAvatar({
item: new AvatarItem(
member.getAvatar(),
member.getAvatarName(),
AvatarColorUntil.getBackgroundColorById(member.getAccId())
)
})
.width(42)
.height(42)
.margin({
right: 14
})
Text(member.getNickname())
.fontColor('#ff333333')
.fontSize(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.width(280)
}
.padding({
left: 20,
right: 20,
top: 10,
bottom: 10
})
.width('100%')
}
})
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffFFFFFF')
}
}
.tabBar($r('app.string.chat_read_receipt_read',
this.viewModel.readReceiptDetail.readReceipt.readCount))
TabContent() {
if (this.viewModel.readReceiptDetail.unreadAccountList.length <= 0) {
CommonEmptyResult({
tips: $r('app.string.chat_read_receipt_all_read')
}).margin({
top: 80
})
} else {
List() {
ForEach(this.viewModel.unreadMemberList, (member: TeamMemberWithUser) => {
ListItem() {
Row() {
CommonAvatar({
item: new AvatarItem(
member.getAvatar(),
member.getAvatarName(),
AvatarColorUntil.getBackgroundColorById(member.getAccId())
)
})
.width(42)
.height(42)
.margin({
right: 14
})
Text(member.getNickname())
.fontColor('#ff333333')
.fontSize(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.width(280)
}
.padding({
left: 20,
right: 20,
top: 10,
bottom: 10
})
}
})
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffFFFFFF')
}
}.tabBar($r('app.string.chat_read_receipt_unread', this.viewModel.readReceiptDetail.readReceipt.unreadCount))
.onAttach(() => {
if (this.viewModel.readReceiptDetail) {
this.viewModel.getMemberList(this.viewModel.readReceiptDetail.unreadAccountList, false)
}
})
}
}
}
.hideTitleBar(true)
.backgroundColor(Color.White)
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
this.message = context.pathStack.getParamByName('ChatReadReceiptPage')[0] as V2NIMMessage
this.teamId = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(this.message.conversationId)
this.viewModel.init(this.message)
this.viewModel.getMessageReadReceipt()
})
}
}
@Builder
export function ChatReadReceiptPageBuilder() {
ChatReadReceiptPage()
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { MergeDetailMessageComponent } from '../view/MessageComponent';
import { NEEmojiManager } from '../manager/NEEmojiManager';
import { ImagesIndexModel, ImageViewDialog, NavigationBackBuilder, VideoViewerDialog } from '@nimkit/common';
import { DeviceUtils } from '../common/DeviceUtils';
import {
downLoadAndOpenFile,
getMessageImageUrls,
getMessageVideoRatio,
getMessageVideoUrl
} from '../common/MessageHelper';
import { V2NIMMessageLocationAttachment, V2NIMMessageType } from '@nimsdk/base';
import { CustomMessageUtils, MergedMessageAttachment, mergedMessageCustomType } from '@nimkit/chatkit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AudioPlayerManager } from '../manager/AudioPlayerManager';
import { common } from '@kit.AbilityKit';
import { MergeMessageDetailViewModel } from '../viewmodel/MergeMessageDetailViewModel';
import { MergeMessageInfo } from '../model/MergeMessageInfo';
import { sceneMap } from '@kit.MapKit';
@ComponentV2
export struct MergeMessageDetailPage {
pathStack: NavPathStack = new NavPathStack()
@Local mergeMessageInfo: MergeMessageInfo = new MergeMessageInfo('');
viewModel: MergeMessageDetailViewModel = new MergeMessageDetailViewModel();
conversationId: string = '';
@Local imagesIndexModel?: ImagesIndexModel
@Local currentImageIndex: number = 0
imageViewerDialog: CustomDialogController = new CustomDialogController({
builder: ImageViewDialog({
imagesIndexModel: this.imagesIndexModel
}),
cornerRadius: 0,
alignment: DialogAlignment.Center,
backgroundColor: Color.Black,
backgroundBlurStyle: BlurStyle.NONE,
height: '100%',
width: '100%',
customStyle: true,
})
@Local videoFileUrl?: string
@Local videoRatio?: number
videoViewerDialog: CustomDialogController = new CustomDialogController({
builder: VideoViewerDialog({
videoUrl: this.videoFileUrl,
videoRatio: this.videoRatio
}),
cornerRadius: 0,
alignment: DialogAlignment.Center,
backgroundColor: Color.Black,
backgroundBlurStyle: BlurStyle.NONE,
height: '100%',
width: '100%',
customStyle: true
})
private listScroller: Scroller = new Scroller()
aboutToAppear(): void {
NEEmojiManager.instance.setup();
DeviceUtils.rootDirPath = getContext(this).filesDir
}
async requestData() {
let param = this.pathStack.getParamByName("MergeMessageDetailPage") as MergedMessageAttachment[];
if (param.length > 0) {
this.viewModel.init(this.mergeMessageInfo)
this.viewModel.loadMergeMessage(this.pathStack, param[param.length - 1])
} else {
this.pathStack.removeByName("MergeMessageDetailPage")
}
}
async showImageDetail(msg?: NIMMessageInfo) {
try {
const imageModel = await getMessageImageUrls(msg, this.mergeMessageInfo)
this.imagesIndexModel = imageModel
this.imageViewerDialog.open()
} catch (err) {
console.log(err)
}
}
showVideoDetail(msg: NIMMessageInfo) {
//点击视频消息前关闭所有音频
AudioPlayerManager.instance.stopPlayAll()
this.videoFileUrl = getMessageVideoUrl(msg, this.mergeMessageInfo)
this.videoRatio = getMessageVideoRatio(msg)
if (this.videoFileUrl) {
this.videoViewerDialog.open()
}
}
showLocationDetail(msg: NIMMessageInfo) {
let attachment = msg.message.attachment as V2NIMMessageLocationAttachment
let queryLocationOptions: sceneMap.LocationQueryOptions = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
name: attachment.address,
address: attachment.address,
};
// 拉起地点详情页
sceneMap.queryLocation(getContext(this) as common.UIAbilityContext, queryLocationOptions).then(() => {
console.info("netease QueryLocation", "Succeeded in querying location.");
}).catch((err: BusinessError) => {
console.error("netease QueryLocation", `Failed to query Location, code: ${err.code}, message: ${err.message}`);
});
}
@Builder
build() {
NavDestination() {
NavigationBackBuilder({
title: $r('app.string.chatHistoryBrief'),
backgroundColor: Color.White,
leftButtonAction: () => {
this.pathStack.pop()
},
})
RelativeContainer() {
List({ space: 12, scroller: this.listScroller }) {
LazyForEach(this.mergeMessageInfo.msgList, (msg: NIMMessageInfo) => {
ListItem() {
MergeDetailMessageComponent({
message: msg,
chatUserInfo: this.mergeMessageInfo,
onMessageClick: {
onAvatarClick: undefined,
onItemLongClick: undefined,
onItemClick: (event: ClickEvent, msg: NIMMessageInfo | undefined) => {
if (msg) {
if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
this.showImageDetail(msg)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE) {
downLoadAndOpenFile(msg, getContext(this), this.mergeMessageInfo)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO) {
this.showVideoDetail(msg)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION) {
this.showLocationDetail(msg)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
const attachment = msg.message.attachment
if (attachment) {
const type = CustomMessageUtils.typeOfCustomMessage(attachment)
if (type === mergedMessageCustomType) {
let data = CustomMessageUtils.dataOfCustomMessage(attachment)
if (data as MergedMessageAttachment) {
this.pathStack.pushPath({
name: 'MergeMessageDetailPage',
param: data as MergedMessageAttachment
})
}
}
}
}
}
}
}
})
}
}, (item: NIMMessageInfo, index?: number) => item.message.messageClientId)
}
.id("mergeMessageDetailPageListView")
.cachedCount(20)
.padding({ bottom: 56 })
.maintainVisibleContentPosition(true)
.alignRules({
left: { anchor: "__container__", align: HorizontalAlign.Start },
right: { anchor: "__container__", align: HorizontalAlign.End },
top: { anchor: "__container__", align: VerticalAlign.Top },
})
.width('100%')
.height('100%')
}
}
.hideTitleBar(true)
.backgroundColor(Color.White)
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
this.requestData()
})
}
aboutToDisappear(): void {
this.viewModel.onDestroy()
}
}
// 跳转页面入口函数
@Builder
export function MergeMessageDetailPageBuilder() {
MergeMessageDetailPage()
}

View File

@ -0,0 +1,420 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ChatTeamInfo } from '../model/ChatTeamInfo';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { PinMessageComponent } from '../view/PinMessageComponent';
import { NEEmojiManager } from '../manager/NEEmojiManager';
import {
CommonEmptyResult,
ImagesIndexModel,
ImageViewDialog,
NavigationBackBuilder,
NECommonUtils,
VideoViewerDialog
} from '@nimkit/common';
import { DeviceUtils } from '../common/DeviceUtils';
import {
downLoadAndOpenFile,
getMessageImageUrls,
getMessageVideoRatio,
getMessageVideoUrl
} from '../common/MessageHelper';
import { V2NIMConversationType, V2NIMErrorCode, V2NIMMessageType } from '@nimsdk/base';
import pasteboard from '@ohos.pasteboard';
import { ChatKitClient } from '@nimkit/chatkit/src/main/ets/ChatKitClient';
import { ChatRepo } from '@nimkit/chatkit/src/main/ets/repo/ChatRepo';
import { TextMessageDetailDialog } from '../view/TextMessageDetailDialog';
import { ConversationSelectParam } from '@nimkit/chatkit/src/main/ets/model/ConversationSelectParam';
import { ConversationSelectModel } from '@nimkit/chatkit/src/main/ets/model/ConversationSelectModel';
import { ForwardMessageDialog } from '../view/ForwardMessageDialog';
import { ChatPinViewModel } from '../viewmodel/ChatPinViewModel';
import {
conversationSelectLimitCount,
CustomMessageUtils,
ErrorUtils,
MergedMessageAttachment,
mergedMessageCustomType
} from '@nimkit/chatkit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ChatBaseViewModel } from '../viewmodel/ChatBaseViewModel';
import { AudioPlayerManager } from '../manager/AudioPlayerManager';
import { common } from '@kit.AbilityKit';
import { showLocationDetail } from '../common/ChatUtils';
@CustomDialog
export struct PinMoreActionDialog {
@BuilderParam message: NIMMessageInfo
@BuilderParam dialogHeight: number
@BuilderParam forwardMessageAction?: (message: NIMMessageInfo) => void
controller?: CustomDialogController
unpinMessage = async () => {
if (ErrorUtils.checkNetworkAndToast()) {
await ChatRepo.unpinMessage(this.message.message).catch((err: BusinessError) => {
if (err.code == V2NIMErrorCode.V2NIM_ERROR_CODE_PIN_NOT_EXIST) {
return
}
ErrorUtils.handleErrorToast(err.code)
})
NECommonUtils.showToast($r('app.string.pin_list_unpin_success_tips'))
this.cancel()
}
}
copyMessage = () => {
const copyData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.message.message.text)
pasteboard.getSystemPasteboard().setData(copyData).then(() => {
NECommonUtils.showToast($r('app.string.copy_success'))
this.cancel()
})
}
forwardMessage = () => {
this.forwardMessageAction?.(this.message)
this.cancel()
}
cancel = () => {
this.controller?.close()
}
build() {
Column() {
Column() {
Text($r("app.string.setting_unpin"))
.fontSize(14)
.fontColor("#333333")
.textAlign(TextAlign.Center)
.height(42)
.width('100%')
.onClick(this.unpinMessage)
if (this.message.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
Row()
.height(1)
.width('100%')
.backgroundColor("#EFF1F4")
Text($r("app.string.chat_operation_copy"))
.fontSize(14)
.fontColor("#333333")
.textAlign(TextAlign.Center)
.height(42)
.width('100%')
.onClick(this.copyMessage)
}
if (this.message.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
this.message.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
Row()
.height(1)
.width('100%')
.backgroundColor("#EFF1F4")
Text($r("app.string.chat_operation_forward"))
.fontSize(14)
.fontColor("#333333")
.textAlign(TextAlign.Center)
.height(42)
.width('100%')
.onClick(this.forwardMessage)
}
}
.height(this.dialogHeight)
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ left: 12, right: 12 })
Column() {
Text($r('app.string.mine_edit_cancel'))
.fontSize(14)
.fontColor("#333333")
.textAlign(TextAlign.Center)
.height(42)
.width('100%')
.onClick(this.cancel)
}
.height(42)
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ top: 10, left: 12, right: 12 })
}
.height(this.dialogHeight + 52)
.backgroundColor(Color.Transparent)
}
}
@ComponentV2
export struct PinMessagePage {
pathStack: NavPathStack = new NavPathStack()
@Local chatTeamInfo: ChatTeamInfo = new ChatTeamInfo('');
viewModel: ChatPinViewModel = new ChatPinViewModel();
conversationId: string = '';
@Local imagesIndexModel?: ImagesIndexModel
@Local currentImageIndex: number = 0
imageViewerDialog: CustomDialogController = new CustomDialogController({
builder: ImageViewDialog({
imagesIndexModel: this.imagesIndexModel
}),
cornerRadius: 0,
alignment: DialogAlignment.Center,
backgroundColor: Color.Black,
backgroundBlurStyle: BlurStyle.NONE,
height: '100%',
width: '100%',
customStyle: true,
})
@Local videoFileUrl?: string
@Local videoRatio?: number
videoViewerDialog: CustomDialogController = new CustomDialogController({
builder: VideoViewerDialog({
videoUrl: this.videoFileUrl,
videoRatio: this.videoRatio
}),
cornerRadius: 0,
alignment: DialogAlignment.Center,
backgroundColor: Color.Black,
backgroundBlurStyle: BlurStyle.NONE,
height: '100%',
width: '100%',
customStyle: true
})
@Local tapMessage?: NIMMessageInfo
@Local pinMoreActionDialogHeight: number = 42
@Local forwardMessages: NIMMessageInfo[] = []
@Local forwardConversations: ConversationSelectModel[] = []
@Local currentConversationName?: string
forwardMessageDialog = new CustomDialogController({
builder: ForwardMessageDialog({
conversationList: this.forwardConversations,
currentConversationName: this.currentConversationName,
sendForwardMsg: (text: string | undefined) => {
this.viewModel.forwardMessage(this.forwardMessages, this.forwardConversations, text)
}
}),
cornerRadius: 14,
backgroundColor: Color.White,
height: 250,
width: 276,
})
forwardMessageClick = () => {
if (ErrorUtils.checkNetworkAndToast()) {
this.pathStack.pushPath({
name: 'ConversationSelectPage',
param: new ConversationSelectParam([], conversationSelectLimitCount, this.forwardMessageAction)
})
}
}
pinMoreActionDialog: CustomDialogController = new CustomDialogController({
builder: PinMoreActionDialog({
message: this.tapMessage,
dialogHeight: this.pinMoreActionDialogHeight,
forwardMessageAction: this.forwardMessageClick
}),
cornerRadius: 0,
alignment: DialogAlignment.Bottom,
backgroundColor: Color.Transparent,
backgroundBlurStyle: BlurStyle.NONE,
})
textMessageDetailDialog: CustomDialogController = new CustomDialogController({
builder: TextMessageDetailDialog({
message: this.tapMessage
}),
cornerRadius: 0,
alignment: DialogAlignment.Center,
backgroundColor: Color.White,
height: '100%',
width: '100%',
customStyle: true
})
private listScroller: Scroller = new Scroller()
private scrollIndex: number = 0
@Monitor("viewModel.topInsert")
scrollToTop() {
if (this.scrollIndex === 0) {
this.listScroller.scrollToIndex(0)
}
this.viewModel.topInsert = false
}
forwardMessageAction = (selectedList: ConversationSelectModel[]) => {
if (this.tapMessage?.message) {
this.forwardMessages = [this.tapMessage]
this.forwardConversations = selectedList
const conversationType = ChatKitClient.nim.conversationIdUtil.parseConversationType(this.conversationId)
if (conversationType === V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P) {
const targetAccountId = ChatKitClient.nim.conversationIdUtil.parseConversationTargetId(this.conversationId)
this.currentConversationName = this.chatTeamInfo.getChatUserShowName(targetAccountId)
} else {
this.currentConversationName = this.chatTeamInfo.team?.name
}
this.forwardMessageDialog.open()
}
}
getPinMoreActionDialogHeight(message?: NIMMessageInfo): number {
let height = 42
if (message?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
height += 43
}
if (message?.message.messageType !== V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO) {
height += 43
}
return height
}
aboutToAppear(): void {
NEEmojiManager.instance.setup();
DeviceUtils.rootDirPath = getContext(this).filesDir
}
async requestChatData() {
this.chatTeamInfo.setConversationId(this.conversationId)
this.viewModel.init(this.conversationId as string, this.chatTeamInfo)
this.viewModel.getPinMessageList()
}
async showImageDetail(msg?: NIMMessageInfo) {
try {
const imageModel = await getMessageImageUrls(msg, this.chatTeamInfo)
this.imagesIndexModel = imageModel
this.imageViewerDialog.open()
} catch (err) {
console.log(err)
}
}
showVideoDetail(msg: NIMMessageInfo) {
//点击视频消息前关闭所有音频
AudioPlayerManager.instance.stopPlayAll()
this.videoFileUrl = getMessageVideoUrl(msg, this.chatTeamInfo)
this.videoRatio = getMessageVideoRatio(msg)
if (this.videoFileUrl) {
this.videoViewerDialog.open()
}
}
@Builder
build() {
NavDestination() {
NavigationBackBuilder({
title: $r("app.string.setting_pin"),
backgroundColor: '#EFF1F4',
leftButtonAction: () => {
this.pathStack.pop()
},
})
if (this.viewModel.pinListEmpty) {
CommonEmptyResult({
tips: $r('app.string.setting_pin_empty')
})
.margin({
top: 56 + 74,
})
}
RelativeContainer() {
List({ space: 12, scroller: this.listScroller }) {
LazyForEach(this.chatTeamInfo.msgList, (msg: NIMMessageInfo) => {
ListItem() {
PinMessageComponent({
message: msg,
chatUserInfo: this.chatTeamInfo,
onMessageClick: {
onAvatarClick: undefined,
onItemLongClick: undefined,
onItemClick: (_event: ClickEvent, msg: NIMMessageInfo | undefined) => {
this.tapMessage = msg
if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
this.showImageDetail(msg)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE) {
downLoadAndOpenFile(msg, getContext(this), this.chatTeamInfo)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO) {
this.showVideoDetail(msg)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
this.textMessageDetailDialog.open()
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION) {
showLocationDetail(msg, getContext(this) as common.UIAbilityContext)
} else if (msg?.message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
const attachment = msg.message.attachment
if (attachment) {
const type = CustomMessageUtils.typeOfCustomMessage(attachment)
if (type === mergedMessageCustomType) {
let data = CustomMessageUtils.dataOfCustomMessage(attachment)
if (data as MergedMessageAttachment) {
this.pathStack.pushPath({
name: 'MergeMessageDetailPage',
param: data as MergedMessageAttachment
})
}
}
}
}
}
},
onMoreButtonClick: (_event: ClickEvent, msg: NIMMessageInfo | undefined) => {
this.tapMessage = msg
this.pinMoreActionDialogHeight = this.getPinMoreActionDialogHeight(msg)
this.pinMoreActionDialog.open()
},
onPinItemClick: (_event: ClickEvent, msg: NIMMessageInfo | undefined) => {
if (msg) {
if (ChatBaseViewModel.currentViewModel) {
ChatBaseViewModel.currentViewModel.setAnchorMessage(msg)
}
if (msg.message.conversationType == V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P) {
this.pathStack.popToName('ChatP2PPage')
} else if (msg.message.conversationType == V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM) {
this.pathStack.popToName('ChatTeamPage')
}
}
}
})
}
}, (item: NIMMessageInfo) => item.message.messageClientId)
}
.id("pinPageListView")
.cachedCount(20)
.padding({ bottom: 56 })
.maintainVisibleContentPosition(true)
.alignRules({
left: { anchor: "__container__", align: HorizontalAlign.Start },
right: { anchor: "__container__", align: HorizontalAlign.End },
top: { anchor: "__container__", align: VerticalAlign.Top },
})
.width('100%')
.height('100%')
.onScrollIndex((start: number, _end: number) => {
this.scrollIndex = start
})
}
}
.hideTitleBar(true)
.backgroundColor('#EFF1F4')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
let param = this.pathStack.getParamByName("PinMessagePage") as string[];
if (param.length > 0) {
this.conversationId = param[0];
this.requestChatData();
} else {
this.pathStack.removeByName("PinMessagePage")
}
})
}
aboutToDisappear(): void {
this.viewModel.onDestroy()
}
}
// 跳转页面入口函数
@Builder
export function PinMessagePageBuilder() {
PinMessagePage()
}

View File

@ -0,0 +1,249 @@
import {
NavigationBackBuilder,
CommonEmptyResult
} from '@nimkit/common'
import { window } from '@kit.ArkUI';
import { hdHttp, HdResponse,BasicConstant, authStore} from '@itcast/basic'
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction,LengthMetrics } from '@kit.ArkUI'
import { PerfactInputSheet } from '@itcast/basic/src/main/ets/Views/PerfactInputSheet'
@ComponentV2
export struct QuickMessagePage {
@Local delectuuid:string=''
@Local statusBarHeight:number=0
pathStack: NavPathStack = new NavPathStack()
@Local replydata:QuickReplyData[]=[]
private InputSheet!:CustomDialogController;
dialog = new CustomDialogController({
builder: DelectDialog(
{
CallBack:()=>{
this.deleteQuickReply(this.delectuuid)
}
}
),
cornerRadius: 4,
width: '70%',
})
build() {
NavDestination() {
Column() {
NavigationBackBuilder({
title:$r('app.string.reply'),
top:this.statusBarHeight,
leftButtonAction: () => {
this.pathStack.pop()
}
})
Row(){
Image($r('app.media.add_drawle')).width(30).height(30)
Text('添加快捷回复').margin({left:10}).fontSize(20).fontColor($r('app.color.common_gray_03'))
}
.width('100%')
.height(50)
.padding({left:10})
.onClick(()=>{
this.InputSheet.open()
})
Text('').height(5).width('100%')
.backgroundColor($r('app.color.common_gray_bg'))
if(this.replydata==null||this.replydata.length==0)
{
CommonEmptyResult({
tips: $r('app.string.reply_empty')
})
}
else
{
List() {
ForEach(this.replydata, (item: QuickReplyData) => {
ListItem() {
Text(item.replystr).fontSize(16).fontColor($r('app.color.common_gray_01')).padding(20).textAlign(TextAlign.Start).width('100%')
}
.onClick(()=>{
this.pathStack.pop(item.replystr)
})
.gesture(
LongPressGesture({
duration: 1000, // 设置长按触发时间为1秒
repeat: true // 允许连续触发回调
})
.onAction((event: GestureEvent) => {
this.delectuuid=item.uuid
this.dialog.open()
})
)
.width('100%')
})
}
.divider({
strokeWidth: 1, // 线宽
color: $r('app.color.common_gray_bg'), // 颜色
})
.margin({ left: 10, right: 10, top: 0 })
}
}
.width('100%')
.height('100%')
}
.hideTitleBar(true)
.backgroundColor(Color.White)
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
})
}
aboutToAppear(): void {
window.getLastWindow(getContext(this)).then(currentWindow => {
let avoidArea1 = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
// 顶部状态栏高度
this.statusBarHeight= px2vp(avoidArea1.topRect.height);
})
this.initInputDialog()
this.initList()
}
initList()
{
hdHttp.post<string>(BasicConstant.QuickReplyList, {
user_uuid: authStore.getUser().uuid,
} as extraData).then(async (res: HdResponse<string>) => {
let json:QuickReplyBean = JSON.parse(res+'') as QuickReplyBean;
if(json.code == '1') {
this.replydata=json.data
} else {
promptAction.showToast({ message: json.message, duration: 1000 })
}
}).catch((err: BusinessError) => {
console.info(`Response fails: ${err}`);
})
}
initInputDialog() {
this.InputSheet = new CustomDialogController({
builder:PerfactInputSheet({
controller:this.InputSheet,
inputTitle:'请输入快捷回复',
inputPlaceholder:'',
style:'1',
inputCallBack:(input: string,title:string)=>{
this.addQuickReply(input)
}
}),
keyboardAvoidDistance: LengthMetrics.vp(0), // 设置弹窗底部与键盘顶部间距单位vp
alignment: DialogAlignment.Center,
customStyle: true,
autoCancel: false,
backgroundColor: ('rgba(0,0,0,0.5)'),
height: '100%'
})
}
addQuickReply(replystr:string)
{
hdHttp.post<string>(BasicConstant.addQuickReply, {
user_uuid: authStore.getUser().uuid,
replystr:replystr
} as addextraData).then(async (res: HdResponse<string>) => {
let json:QuickReplyBean = JSON.parse(res+'') as QuickReplyBean;
this.replydata=json.data
promptAction.showToast({ message: '添加成功', duration: 1000 })
}).catch((err: BusinessError) => {
console.info(`Response fails: ${err}`);
})
}
deleteQuickReply(uuid:string)
{
hdHttp.post<string>(BasicConstant.deleteQuickReply, {
uuid: uuid,
} as delectextraData).then(async (res: HdResponse<string>) => {
this.initList()
promptAction.showToast({ message: '删除成功', duration: 1000 })
}).catch((err: BusinessError) => {
console.info(`Response fails: ${err}`);
})
}
}
@CustomDialog
struct DelectDialog {
controller: CustomDialogController
CallBack: () => void = () => {};
build() {
Column() {
Text('删除快捷回复')
.fontSize(17)
.fontColor('#444444')
.padding(15)
Text('').height(1).width('100%')
.backgroundColor($r('app.color.home_gray'))
Text('删除')
.fontSize(16).fontColor($r('app.color.common_gray_03'))
.padding(10).width('100%').textAlign(TextAlign.Start)
.onClick(() => {
if (this.controller != undefined) {
this.controller.close()
this.CallBack();
}
})
Text('').height(1).width('100%')
.backgroundColor($r('app.color.home_gray'))
.margin({bottom:10})
}
// .onClick(() => {
//
// if (this.controller != undefined) {
// this.controller.close()
//
// }
// })
.backgroundColor($r('app.color.white'))
}
}
// 跳转页面入口函数
@Builder
export function QuickMessagePageBuilder() {
QuickMessagePage()
}
interface extraData{
user_uuid:string
}
interface delectextraData{
uuid:string
}
interface addextraData{
user_uuid:string,
replystr:string,
}
interface QuickReplyBean {
code:string;
message:string;
data:QuickReplyData[];
}
interface QuickReplyData{
createdate:string;
replystr:string;
user_uuid:string;
uuid:string;
sort:number;
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ErrorUtils } from '@nimkit/chatkit';
import { ChatBaseViewModel } from '../viewmodel/ChatBaseViewModel';
import {
AvatarColorUntil,
AvatarItem,
CommonAvatar,
CommonEmptyResult,
CommonTextInput,
MatchSearchText,
NavigationBackBuilder,
TailString
} from '@nimkit/common';
import { DateUtil } from '@nimkit/common/src/main/ets/utils/DateUtil';
import { TeamHistoryModel, TeamHistoryViewModel } from '../viewmodel/TeamHistoryViewModel';
import { V2NIMConversationType } from '@nimsdk/base';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
// 跳转页面入口函数
@Builder
export function TeamHistoryPageBuilder() {
TeamHistoryPage()
}
@ComponentV2
export struct TeamHistoryPage {
pathStack: NavPathStack = new NavPathStack()
@Local viewModel?: TeamHistoryViewModel
@Local isEmpty: boolean = false
@Local searchText: string = ""
onSubmit = async (_enterKey: EnterKeyType, event: SubmitEvent) => {
this.searchText = event.text
if (ErrorUtils.checkNetworkAndToast()) {
if (this.searchText.length > 0) {
try {
await this.viewModel?.searchHistoryMessages(event.text)
this.isEmpty = (this.viewModel?.historyList.length ?? 0) <= 0
} catch (err) {
console.error(err)
this.isEmpty = true
}
}
}
}
onTextChange = (value: string) => {
if (value.length === 0) {
this.viewModel?.clearHistoryMessages()
this.isEmpty = false
}
}
requestData() {
const teamId = this.pathStack.getParamByName('TeamHistoryPage')[0] as string
this.viewModel = new TeamHistoryViewModel(teamId)
}
build() {
NavDestination() {
Column() {
NavigationBackBuilder({
title: $r('app.string.team_history_record'),
leftButtonAction: () => {
this.pathStack.pop()
}
})
CommonTextInput({
placeHolderText: $r('app.string.team_history_record_tip'),
onSubmit: this.onSubmit.bind(this),
onTextChange: this.onTextChange,
keepEditableState: false
})
.margin({
top: 16
})
if (this.isEmpty) {
CommonEmptyResult({
emptyImage: $r('app.media.no_history'),
tips: $r('app.string.team_history_record_empty_tip')
})
.margin({
top: 74
})
}
List() {
ForEach(this.viewModel?.historyList, (item: TeamHistoryModel) => {
ListItem() {
Row() {
CommonAvatar({
item: new AvatarItem(item.teamMember?.getAvatar(),
TailString(item.teamMember?.getAvatarName()),
AvatarColorUntil.getBackgroundColorById(item.teamMember?.getAccId() ?? "")),
roundRadius: 18
})
.width(36)
.height(36)
.margin({
left: 20
})
Column() {
Text(item.teamMember?.getNickname())
.fontColor('#333333')
MatchSearchText({
allText: item.message.text,
searchText: this.searchText,
normalFontColor: "#999999"
})
.margin({
top: 6
})
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({
left: 12,
right: 12
})
// 时间
Text(DateUtil.formatTimestamp(item.message.createTime))
.fontSize(12)
.fontColor('#999999')
.margin({
right: 20
})
}
}
.height(62)
.onClick(() => {
if (ChatBaseViewModel.currentViewModel) {
let msgInfo = new NIMMessageInfo(item.message)
ChatBaseViewModel.currentViewModel.setAnchorMessage(msgInfo)
}
if (item.message.conversationType == V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P) {
this.pathStack.popToName('ChatP2PPage')
} else if (item.message.conversationType == V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM) {
this.pathStack.popToName('ChatTeamPage')
}
})
})
}
.divider({
strokeWidth: 1,
color: "0xDBE0E8",
startMargin: 20
})
}
}
.hideTitleBar(true)
.backgroundColor(Color.White)
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
this.requestData()
})
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
ChatKitClient,
ContactRepo,
ErrorUtils,
NEFriendUserCache,
NEUserWithFriend,
PersonSelectParam,
TeamRepo
} from '@nimkit/chatkit'
import {
AvatarColorUntil,
AvatarItem,
CommonAvatar,
NavigationBackBuilder,
ViewItemArrow,
ViewItemBuilder,
ViewItemSwitch
} from '@nimkit/common'
import { UserSettingViewModel } from '../viewmodel/UserSettingViewModel'
@ComponentV2
export struct UserSettingPage {
pathStack: NavPathStack = new NavPathStack()
accountId?: string
@Local userWithFriend?: NEUserWithFriend
viewModel: UserSettingViewModel = new UserSettingViewModel()
firstItems: ViewItemArrow[] = [
{
title: $r("app.string.setting_pin"),
itemHeight: 48,
routerUrl: "PinMessagePage",
rightIcon: $r('app.media.arrow_right')
}
]
// 设置是否开启消息提醒
setP2PNotify = async (isOpen: boolean) => {
if (ErrorUtils.checkNetworkAndToast()) {
try {
await this.viewModel?.setP2PNotify(isOpen)
} catch (err) {
console.log(err)
}
}
}
// 设置是否聊天置顶
stickTopConversation = async (isOpen: boolean) => {
if (ErrorUtils.checkNetworkAndToast()) {
try {
await this.viewModel?.stickTopConversation(isOpen)
} catch (err) {
console.log(err)
}
}
}
@Local secondItems: ViewItemSwitch[] = []
// 创建讨论组
createDiscussion = async (selectedList: NEUserWithFriend[]) => {
if (ErrorUtils.checkNetworkAndToast()) {
const inviteeAccountIds: string[] = []
selectedList.forEach((userWithFriend) => {
if (userWithFriend.user?.accountId) {
inviteeAccountIds.push(userWithFriend.user.accountId)
}
})
if (this.viewModel?.accountId) {
inviteeAccountIds.push(this.viewModel?.accountId)
}
try {
const createTeamResult = await TeamRepo.createGroupTeam(inviteeAccountIds)
// 替换聊天页
if (createTeamResult?.team) {
this.pathStack.popToName('ChatP2PPage')
this.pathStack.replacePathByName('ChatTeamPage',
ChatKitClient.nim.conversationIdUtil.teamConversationId(createTeamResult.team.teamId))
}
} catch (err) {
console.log(err)
}
}
}
// 【+】按钮点击事件
onAddButtonClick() {
this.pathStack.pushPath({
name: 'PersonSelectPage',
param: new PersonSelectParam(this.createDiscussion,
[this.viewModel?.accountId],
10)
})
}
updateSecondItems() {
this.secondItems = [
{
title: $r('app.string.setting_message_notify'),
itemHeight: 48,
isOpen: this.viewModel.isMessageNotify,
switchChange: this.setP2PNotify
},
{
title: $r('app.string.setting_message_stick_top'),
itemHeight: 48,
isOpen: this.viewModel.isStickTop,
switchChange: this.stickTopConversation
}
]
}
async requestData() {
const conversationId = this.pathStack.getParamByName('UserSettingPage')[0] as string
this.viewModel.init(conversationId).then(() => {
this.updateSecondItems()
})
const accountId = this.viewModel.accountId
if (NEFriendUserCache.getInstance().isFriend(accountId)) {
this.userWithFriend = NEFriendUserCache.getInstance().getFriendById(accountId)
} else {
try {
const users = await ContactRepo.getUserWithFriendByIds([accountId])
if (users.length > 0) {
this.userWithFriend = users[0]
}
} catch (err) {
console.log(err)
}
}
}
build() {
NavDestination() {
Column() {
NavigationBackBuilder({
title: $r('app.string.chat_setting'),
backgroundColor: '#ffEFF1F4',
leftButtonAction: () => {
this.pathStack.pop()
}
})
Row() {
Row() {
Column() {
CommonAvatar({
item: new AvatarItem(this.userWithFriend?.user?.avatar,
this.userWithFriend?.shortName(),
AvatarColorUntil.getBackgroundColorById(this.userWithFriend?.user?.accountId ?? "")),
roundRadius: 21,
textSize: 16
})
.width(42)
.height(42)
Text(this.userWithFriend?.showName())
.width(42)
.height(14)
.fontSize(12)
.maxLines(1)
.textOverflow({
overflow: TextOverflow.Ellipsis
})
.margin({
top: 6
})
}
.width(42)
.height(80)
Image($r('app.media.team_invite'))
.width(42)
.height(42)
.margin({
left: 16
})
.onClick(() => {
this.onAddButtonClick()
})
}
.alignSelf(ItemAlign.Start)
.alignItems(VerticalAlign.Top)
.height(80)
.width('100%')
}
.borderRadius(8)
.backgroundColor(Color.White)
.margin({
left: 20,
right: 20,
bottom: 12
})
.padding({
left: 20,
right: 20,
top: 12
})
List() {
ForEach(this.firstItems, (item: ViewItemArrow) => {
ListItem() {
ViewItemBuilder(item)
}
.width('100%')
.height(item.itemHeight)
.backgroundColor(item.backgroundColor ?? Color.White)
.onClick(() => {
this.pathStack.pushPath({
name: item.routerUrl,
param: this.viewModel?.conversationId
})
})
})
ForEach(this.secondItems, (item: ViewItemSwitch) => {
ListItem() {
ViewItemBuilder(item)
}
.width('100%')
.height(item.itemHeight)
.backgroundColor(item.backgroundColor ?? Color.White)
})
}
.borderRadius(8)
.backgroundColor(Color.White)
.margin({ left: 20, right: 20, top: 0 })
}
.width('100%')
.height('100%')
}
.hideTitleBar(true)
.backgroundColor('#ffEFF1F4')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
this.requestData()
})
}
}
// 跳转页面入口函数
@Builder
export function UserSettingPageBuilder() {
UserSettingPage()
}

View File

@ -0,0 +1,209 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import {
ChatKitClient,
keyExtensionAtAll,
TeamMemberCache,
TeamMemberWithUser,
TeamRepo,
typeExtensionAllowAll
} from '@nimkit/chatkit'
import { AvatarColorUntil, AvatarItem, CommonAvatar } from '@nimkit/common'
import { V2NIMTeam, V2NIMTeamMemberRole } from '@nimsdk/base'
/**
* @弹窗
*/
@CustomDialog
export struct ChatAitDialog {
animationDur: number = 300
controller: CustomDialogController
@BuilderParam teamId: string
@BuilderParam team?: V2NIMTeam
onMemberSelected?: (member: TeamMemberWithUser) => void
onSelectedAll?: () => void
teamMemberCache: TeamMemberCache = TeamMemberCache.getInstance()
@State showFlag: Visibility = Visibility.Visible
//是否显示@All
@State showAll: boolean = false
build() {
Column() {
RelativeContainer() {
Image($r('sys.media.ohos_ic_public_arrow_down'))
.fillColor('#999999')
.width(16)
.height(16)
.margin({
left: 20
})
.alignRules({
left: { anchor: "__container__", align: HorizontalAlign.Start },
center: { anchor: "__container__", align: VerticalAlign.Center },
})
.onClick(() => {
this.closeDialog()
})
Text($r('app.string.chat_message_ait_select_tips'))
.fontSize(16)
.fontColor('#ff333333')
.alignRules({
middle: { anchor: "__container__", align: HorizontalAlign.Center },
center: { anchor: "__container__", align: VerticalAlign.Center },
})
}.height(42)
List() {
if (this.showAll) {
ListItem() {
Row() {
Stack() {
Image($r('app.media.ic_member_all'))
.width(24)
.height(24)
}
.borderRadius(20)
.backgroundColor('#337EFF')
.width(42)
.height(42)
.margin({
right: 14
})
Text($r('app.string.chat_team_ait_all'))
.fontColor('#ff333333')
.fontSize(16)
.width('70%')
}
.padding({
left: 20,
right: 20,
top: 10,
bottom: 10
})
.onClick(() => {
this.closeDialog()
if (this.onSelectedAll) {
this.onSelectedAll()
}
})
}
}
ForEach(this.teamMemberCache.getAllMembers(), (member: TeamMemberWithUser) => {
if (member.getAccId() !== ChatKitClient.getLoginUserId()) {
ListItem() {
Row() {
CommonAvatar({
item: new AvatarItem(
member.getAvatar(),
member.getAvatarName(),
AvatarColorUntil.getBackgroundColorById(member.getAccId())
)
})
.width(42)
.height(42)
.margin({
right: 14
})
Text(member.getNickname())
.fontColor('#ff333333')
.fontSize(16)
.width('70%')
}
.padding({
left: 20,
right: 20,
top: 10,
bottom: 10
})
.onClick(() => {
this.closeDialog()
if (this.onMemberSelected) {
this.onMemberSelected(member)
}
})
}
}
})
ListItem() {
Column() {
}
.height(42)
}
}
.listDirection(Axis.Vertical)
.onReachEnd(() => {
this.teamMemberCache.getMoreMemberList()
})
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
.width('100%')
.height('60%')
.backgroundColor('#ffFFFFFF')
.visibility(this.showFlag)
.transition(TransitionEffect.OPACITY.animation({ duration: this.animationDur })
.combine(TransitionEffect.translate({ y: 100 })))
}
closeDialog() {
this.showFlag = Visibility.Hidden
setTimeout(() => {
this.controller.close()
}, this.animationDur)
}
aboutToAppear(): void {
this.updateShowAitAllItem()
}
/**
* 更新是否要显示@All的操作
*/
async updateShowAitAllItem() {
let team = await this.teamMemberCache.getTeam()
if (team) {
if (TeamRepo.isGroupTeam(team)) {
this.showAll = true
return
}
if (team?.serverExtension) {
let obgExt = JSON.parse(team.serverExtension) as object | undefined
if (obgExt) {
if (obgExt[keyExtensionAtAll] === typeExtensionAllowAll) {
this.showAll = true
return
}
} else {
this.showAll = true
return
}
} else {
this.showAll = true
return
}
}
let mineMember = await this.teamMemberCache.getMineMember()
if (mineMember
&& (mineMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
|| mineMember.memberRole === V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER)) {
this.showAll = true
return
}
this.showAll = false
}
}

View File

@ -0,0 +1,457 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { NEImageButton } from './NEImageButton';
import { NEChatMoreOperationData, NEChatMoreOperationType } from '../model/NEChatMoreOperationData';
import { NEEmojiManager, NEEmojiParseResult } from '../manager/NEEmojiManager';
import { inputMethod } from '@kit.IMEKit';
import { V2NIMTeam } from '@nimsdk/base';
import { ChatAitDialog } from './ChatAitDialog';
import { TeamMemberCache } from '@nimkit/chatkit/src/main/ets/cache/TeamMemberCache';
import { accountAll, ChatKitClient, TeamMemberWithUser } from '@nimkit/chatkit';
import { AitManager } from '../manager/ait/AitManager';
import { getReplyMessageText, getReplyMessageTitle, parseMessageText, sliceMessageText } from '../common/MessageHelper';
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { ChatInfo } from '../model/ChatInfo';
export enum InputStyleType {
// 底部无内容
None = 1,
// 展示表情输入框
Emoji = 2,
// 长按录音UI展示
Record = 3,
// 更多面板展示
More = 4,
}
@ComponentV2
export struct NEChatInputView {
inputSpans: Array<RichEditorImageSpanResult | RichEditorTextSpanResult> = []
@Local inputContentLength: number = 0;
@Param @Require
onSendTextMessage?: () => void;
@Local operationMoreDataList: Array<NEChatMoreOperationData> = Array();
@Param
placeHolder: string = '';
@Param @Require
onDidClickImage?: () => void;
@Param @Require
onDidClickAudio?: () => void;
@Param @Require
onDidClickEmoji?: () => void;
@Param @Require
onDidClickMore?: () => void;
@Param @Require
onDidClickCloseReply?: () => void;
@Param inputStyle: InputStyleType = InputStyleType.None;
@Param @Require
controller?: RichEditorController;
inputFontSize = 16;
@Param
mute: boolean = false
//群ID
@Param
teamId?: string = undefined
//群
@Param
team?: V2NIMTeam = undefined
//空格符
blank: string = ' ';
@Param
aitManager?: AitManager = undefined
//@的Span
@Param
builderSpans: AitEditorSpan[] = [];
@Param
replyMsg: NIMMessageInfo | undefined = undefined
@Param
chatInfo: ChatInfo | undefined = undefined
/**
* @弹框
*/
chatAitDialogController: CustomDialogController = new CustomDialogController({
builder: ChatAitDialog({
teamId: this.teamId,
team: this.team,
onSelectedAll: this.onSelectedAll.bind(this),
onMemberSelected: this.onTeamMemberSelected.bind(this)
}),
openAnimation: {
curve: Curve.Linear,
playMode: PlayMode.Alternate
},
autoCancel: true,
alignment: DialogAlignment.Bottom,
backgroundColor: Color.White,
height: '60%',
width: '100%',
customStyle: true
})
aboutToAppear(): void {
this.initData();
}
@Monitor('mute')
onMute() {
if (this.mute) {
this.controller?.deleteSpans()
this.controller?.stopEditing()
}
}
// 初始化数据
initData() {
const videoOperationData = new NEChatMoreOperationData();
videoOperationData.operationTitle = "video";
videoOperationData.type = NEChatMoreOperationType.Video;
videoOperationData.imageSource = "app.media.ic_public_chat_photo";
this.operationMoreDataList.push(videoOperationData);
const fileOperationData = new NEChatMoreOperationData();
fileOperationData.operationTitle = "file";
fileOperationData.type = NEChatMoreOperationType.File;
fileOperationData.imageSource = "app.media.ic_public_chat_file";
this.operationMoreDataList.push(fileOperationData);
NEEmojiManager.instance.setup();
}
/**
* @所有人
*/
onSelectedAll() {
//输入框添加@信息
let aitValue = getContext().resourceManager.getStringByNameSync('chat_team_ait_all') + this.blank
//输入框添加@信息
if (this.controller) {
const controller = this.controller;
const offset = controller.getCaretOffset();
const range: RichEditorRange = { start: offset - 1, end: offset };
controller.deleteSpans(range);
const spanOffset = offset - 1
controller.addBuilderSpan(() => this.AtSpan(aitValue), {
offset: spanOffset
});
let aitSpan: AitEditorSpan = {
spanIndex: spanOffset,
accountId: accountAll,
value: '@' + aitValue
}
let index = this.builderSpans.findIndex((e) => e.spanIndex >= spanOffset)
if (index < 0) {
this.builderSpans.push(...[aitSpan])
} else {
this.builderSpans.splice(index, 0, aitSpan)
}
}
}
/**
* @单个人
* @param member
*/
onTeamMemberSelected(member: TeamMemberWithUser) {
//输入框添加@信息
let aitValue = member.getAitName() + this.blank
if (this.controller) {
const controller = this.controller;
const offset = controller.getCaretOffset();
const range: RichEditorRange = { start: offset - 1, end: offset };
controller.deleteSpans(range);
const spanOffset = offset - 1
controller.addBuilderSpan(() => this.AtSpan(aitValue), {
offset: spanOffset
});
let aitSpan: AitEditorSpan = {
spanIndex: spanOffset,
accountId: member.getAccId(),
value: '@' + aitValue,
}
let index = this.builderSpans.findIndex((e) => e.spanIndex >= spanOffset)
if (index < 0) {
this.builderSpans.push(...[aitSpan])
} else {
this.builderSpans.splice(index, 0, aitSpan)
}
}
}
@Builder
AtSpan(nickname: string) {
Text(`@${nickname}`)
.fontColor('#FF337EFF');
}
/**
* 显示@的弹框
*/
async showAitDialog() {
let teamMemberCache = TeamMemberCache.getInstance()
if (teamMemberCache.needFetchMember()) {
await teamMemberCache.getMemberList()
}
this.chatAitDialogController.open()
}
/**
* 是否是@的span
* @param span
* @returns
*/
isAitEditorSpan(span: RichEditorImageSpanResult | RichEditorTextSpanResult, offset?: number): boolean {
if (offset !== undefined && offset >= 0) {
const buildSpan = this.builderSpans.find(e => e.spanIndex === offset)
if (!buildSpan) {
return false
}
}
return !(span as RichEditorTextSpanResult).value &&
!(span as RichEditorImageSpanResult).valueResourceStr?.toString().replaceAll(' ', '');
}
/**
* 是否有选中
* @param controller
* @returns
*/
hasSelection(controller: RichEditorController) {
const selection = controller.getSelection().selection;
return selection[0] !== selection[1];
}
/**
* 删除@Span
*/
deleteAitEditorSpan() {
const controller = this.controller;
if (controller) {
const range: RichEditorRange = { end: controller.getCaretOffset() };
const index = this.getAitEditorSpanCount(controller, range) - 1;
this.builderSpans.splice(index, 1);
}
}
/**
* 获取@Span的内容
* @param controller
* @param range
* @returns
*/
getAitEditorSpanCount(controller: RichEditorController, range: RichEditorRange) {
return controller.getSpans(range).reduce((count: number, span) => {
return this.isAitEditorSpan(span) ? count + 1 : count;
}, 0);
}
/**
* 删除操作
*/
aboutToDelete: (value: RichEditorDeleteValue) => boolean = value => {
ChatKitClient.logger?.debug('NEChatInputView', 'aboutToDelete')
const controller = this.controller;
const span = value.richEditorDeleteSpans[0];
if (controller && span && this.isAitEditorSpan(span, value.offset)) {
this.deleteAitEditorSpan();
}
return true;
}
get hasInput(): boolean {
return this.inputContentLength > 0;
}
build() {
RelativeContainer() {
Column() {
// if (!this.mute && this.replyMsg) {
// Row() {
// Image($r('app.media.ic_chat_reply_close')).height(20).width(20).margin({ right: 12 })
// .onClick(() => {
// this.onDidClickCloseReply?.()
// })
// Text() {
// if (this.chatInfo) {
// Span(getReplyMessageTitle(this.replyMsg, this.chatInfo))
// .fontSize($r('app.float.chat_desc_text_font_size'))
// .textCase(TextCase.Normal)
// .fontColor($r('app.color.color_chat_desc'))
// }
//
// ForEach(sliceMessageText(parseMessageText(getReplyMessageText(this.replyMsg)), 30),
// (item: NEEmojiParseResult) => {
// if (item.text) {
// Span(item.text)
// .fontSize($r('app.float.chat_desc_text_font_size'))
// .textCase(TextCase.Normal)
// .fontColor($r('app.color.color_chat_desc'))
// } else if (item.emoji) {
// ImageSpan($rawfile(`emoji/${item.emoji.file}`)).width('16')
// .height('16')
// .objectFit(ImageFit.Fill)
// .verticalAlign(ImageSpanAlignment.CENTER)
// }
// })
// }
// .width('90%')
// .maxLines(1)
// .textOverflow({ overflow: TextOverflow.Ellipsis })
// .ellipsisMode(EllipsisMode.END)
// .fontColor($r('app.color.color_chat_desc'))
// .fontSize($r('app.float.chat_desc_text_font_size'))
// }
// .height(32)
// .justifyContent(FlexAlign.Start)
// .width('100%')
// }
Row({ space: 0 }) {
NEImageButton({
image: $r('app.media.ic_public_chat_speaker'), onDidClick: (): void => {
console.log("net ease click speaker");
if (this.onDidClickAudio) {
inputMethod.getController().stopInputSession();
this.onDidClickAudio()
}
}
})
.width(27)
.height(27)
.margin({right:8})
RichEditor({ controller: this.controller })
.backgroundColor(this.mute ? $r('app.color.color_chat_mute_bg') : $r('app.color.color_chat_page_bg'))
.height(40)
.layoutWeight(1)
.placeholder(this.mute ? $r('app.string.chat_team_all_mute') :
$r('app.string.chat_send_tips'))
.onWillChange((value: RichEditorChangeValue) => {
console.debug(`Response ChatInputView onWillChange before: ${value.rangeBefore}`)
return true
})
.aboutToDelete(this.aboutToDelete)
.onDidChange((rangeBefore: TextRange, rangeAfter: TextRange) => {
const spans = this.controller?.getSpans() ?? [];
this.inputSpans = spans;
// 统计所有文本span的总长度
this.inputContentLength = spans
.map(span => (span as RichEditorTextSpanResult).value?.length ?? 0)
.reduce((a, b) => a + b, 0);
})
.onSelectionChange((range: RichEditorRange) => {
console.debug(`Response ChatInputView onSelectionChange ${range.start} ~ ${range.end}`)
})
.onIMEInputComplete((result: RichEditorTextSpanResult) => {
//记录输入内容
this.inputSpans = this.controller?.getSpans() ?? []
let value = result.value
let indexStart: number = result.offsetInSpan[0]
//span长度
let spanLen = result.offsetInSpan[1] - result.offsetInSpan[0]
console.debug(`Response onIMEInputComplete value = ${value} index = ${indexStart} len = ${spanLen}}`)
if (this.teamId && this.teamId.length > 0) {
//判断span 长度,解决复制粘贴问题
if (spanLen === 1 && value.charAt(indexStart) === "@") {
//输入@
this.showAitDialog()
}
}
})
.borderRadius(8)
.id('chat_edit_input')
.onSubmit((enterKey: EnterKeyType, event: SubmitEvent) => {
if (enterKey == EnterKeyType.Send) {
if (this.onSendTextMessage) {
this.onSendTextMessage();
event.keepEditableState()
}
}
})
.enterKeyType(EnterKeyType.Send)
// 右侧按钮区域
if (this.hasInput) {
NEImageButton({
image: $r("app.media.ic_public_chat_emoji"), onDidClick: (): void => {
console.log("net ease click emoji");
if (this.onDidClickEmoji) {
inputMethod.getController().stopInputSession();
this.onDidClickEmoji()
}
}
})
.width(27)
.height(27)
.margin({right:8,left:8})
Text("发送")
.width(42)
.height(30)
.margin({right:8,left:8})
.fontSize(14)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.backgroundColor('#08C163')
.borderRadius(6)
.onClick(() => {
if (this.onSendTextMessage) {
this.onSendTextMessage();
}
})
} else {
NEImageButton({
image: $r("app.media.ic_public_chat_emoji"), onDidClick: (): void => {
console.log("net ease click emoji");
if (this.onDidClickEmoji) {
inputMethod.getController().stopInputSession();
this.onDidClickEmoji()
}
}
})
.width(27)
.height(27)
.margin({right:8,left:8})
NEImageButton({
image: $r("app.media.ic_public_chat_more"), onDidClick: (): void => {
console.log("net ease click more");
if (this.onDidClickMore) {
inputMethod.getController().stopInputSession();
this.onDidClickMore()
}
}
})
.width(27)
.height(27)
}
}
.width('100%')
.height(60)
.id("action_button")
}.padding({ left: 8, right: 8 })
// if (this.mute) {
// Column()
// .alignRules({
// left: { anchor: "__container__", align: HorizontalAlign.Start },
// right: { anchor: "__container__", align: HorizontalAlign.End },
// top: { anchor: "__container__", align: VerticalAlign.Top },
// bottom: { anchor: "__container__", align: VerticalAlign.Bottom }
// })
// .backgroundColor($r('app.color.color_chat_mute_bg'))
// .opacity(0.4)
// .width('100%')
// .height('100%')
// }
}
}
}
export interface AitEditorSpan {
spanIndex: number,
accountId: string
value: string
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { OperationItem } from './OperationItemLayout';
import { NEChatMoreOperationData } from '../model/NEChatMoreOperationData';
@ComponentV2
export struct NEChatMoreOperation {
// 面板回调
@Param @Require dataList: Array<NEChatMoreOperationData>;
@Param @Require onDidClick?: (data: NEChatMoreOperationData) => void;
build() {
GridRow({
breakpoints: {
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.dataList, (data: NEChatMoreOperationData) => {
GridCol({ span: 3 }) {
Row() {
OperationItem({
operationData: data, onItemClick: (data) => {
if (this.onDidClick) {
this.onDidClick(data);
}
}
})
}.width("100%").height(90)
}.backgroundColor(Color.White)
})
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
@Entry
@ComponentV2
export struct ChatMultiSelectView {
@Param
onMultiForward?: (event: ClickEvent) => void = undefined
@Param
onSingleForward?: (event: ClickEvent) => void = undefined
@Param
onMultiDelete?: (event: ClickEvent) => void = undefined
@Param
isEnable?: boolean = true
build() {
Row() {
Column() {
Image(this.isEnable ? $r('app.media.ic_chat_select_multi_forward') :
$r("app.media.ic_chat_select_multi_forward_dis")).width(48).height(48).margin({ top: 20 })
Text($r('app.string.chat_operation_multi_forward'))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.margin({ top: 8 })
}.height('100%').alignItems(HorizontalAlign.Center).width(60).onClick((event) => {
if (this.isEnable) {
this.onMultiForward?.(event)
}
})
Column() {
Image(this.isEnable ? $r("app.media.ic_chat_select_forward") : $r("app.media.ic_chat_select_forward_dis"))
.width(48)
.height(48)
.margin({ top: 20 })
Text($r('app.string.chat_operation_single_forward'))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.margin({ top: 8 })
}.height('100%').alignItems(HorizontalAlign.Center).width(60).onClick((event) => {
if (this.isEnable) {
this.onSingleForward?.(event)
}
})
Column() {
Image(this.isEnable ? $r('app.media.ic_chat_select_delete') : $r("app.media.ic_chat_select_delete_dis"))
.width(48)
.height(48)
.margin({ top: 20 })
Text($r('app.string.chat_operation_delete'))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.margin({ top: 8 })
}.height('100%').alignItems(HorizontalAlign.Center).width(60).onClick((event) => {
if (this.isEnable) {
this.onMultiDelete?.(event)
}
})
}
.width('100%')
.height(100)
.backgroundColor($r('app.color.color_chat_op_bg'))
.justifyContent(FlexAlign.SpaceAround)
.padding({
left: 20,
right: 20,
})
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { ConversationSelectModel } from '@nimkit/chatkit/src/main/ets/model/ConversationSelectModel'
import { AvatarColorUntil, AvatarItem, CommonAvatar, NECommonUtils } from '@nimkit/common'
/**
* 转发弹框
*/
@CustomDialog
export struct ForwardMessageDialog {
controller?: CustomDialogController
conversationList: ConversationSelectModel[] = []
leaveText?: string // 留言
forwardType: ResourceStr = $r('app.string.chat_operation_forward') // 转发类型:转发、合并转发、逐条转发
currentConversationName?: string
sendForwardMsg?: (leaveText: string | undefined) => void
build() {
Column() {
Text($r('app.string.chat_send_tips', ""))
.fontSize(16)
.padding({ top: 16, left: 16 })
.height(34)
.width('100%')
Stack({ alignContent: Alignment.Start }) {
List({ space: 9 }) {
ForEach(this.conversationList, (item: ConversationSelectModel) => {
ListItem() {
CommonAvatar({
item: new AvatarItem(item.avatar, NECommonUtils.shortName(item.name),
AvatarColorUntil.getBackgroundColorById(item.conversationId ?? "")),
roundRadius: 16
})
.height(32)
.width(32)
}
.width(32)
.height(32)
})
}
.listDirection(Axis.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(48)
.backgroundColor(Color.White)
.padding({
top: 16,
left: 16,
right: 16
})
if (this.conversationList.length === 1) {
Text(this.conversationList[0].name)
.fontSize(14)
.height(18)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.margin({
top: 18,
left: 56,
right: 12
})
}
}
Column() {
Text() {
Span('[')
Span(this.forwardType)
Span(']')
Span($r('app.string.chat_operation_forward_desc', this.currentConversationName))
}
.alignSelf(ItemAlign.Start)
.fontSize(14)
.height(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.CENTER)
.margin({
top: 8,
left: 12,
right: 12
})
}
.width(244)
.height(32)
.borderRadius(4)
.backgroundColor('#F2F4F5')
.margin({
top: 12,
left: 16,
right: 16
})
TextInput({ placeholder: $r('app.string.forward_dialog_leave_message') })
.fontSize(14)
.height(32)
.placeholderColor('#A6ADB6')
.placeholderFont({ size: 14 })
.backgroundColor(Color.White)
.borderRadius(4)
.borderColor('#E1E6E8')
.borderWidth(1)
.onChange((value: string, previewText?: PreviewText) => {
this.leaveText = value
})
.margin({
top: 12,
left: 16,
right: 16
})
Row()
.height(1)
.width('100%')
.backgroundColor("#EFF1F4")
.margin({ top: 24 })
Row() {
Button($r('app.string.mine_edit_cancel'), { type: ButtonType.Normal })
.fontSize(17)
.fontColor("#666666")
.backgroundColor(Color.White)
.width(135)
.height(52)
.onClick(() => {
this.controller?.close()
})
Column()
.width(1)
.height('100%')
.backgroundColor("#EFF1F4")
Button($r('app.string.chat_send'), { type: ButtonType.Normal })
.fontSize(17)
.fontColor("#007AFF")
.backgroundColor(Color.White)
.width(135)
.height(52)
.onClick(() => {
this.sendForwardMsg?.(this.leaveText)
this.controller?.close()
})
}
.height(52)
.width(270)
.alignItems(VerticalAlign.Center)
}
.height(140)
.width(270)
.backgroundColor(Color.White)
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { getAudioMessageText, getAudioMessageWidth } from '../common/MessageHelper'
import { V2NIMMessageAudioAttachment } from '@nimsdk/base'
import { NIMMessageInfo } from '../model/NIMMessageInfo'
import { AudioPlayerManager } from '../manager/AudioPlayerManager'
import { ChatConst } from '../constants/ChatConst'
import { MessageItemClick } from './MessageItemClick'
@ComponentV2
export struct MessageAudioItemView {
@Param
@Require
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@BuilderParam
isReceiveStyle: boolean | undefined
@Local
receiveSoundIndex: number = 2
sendSoundImages = [
$r('app.media.ic_sound_from_1'),
$r('app.media.ic_sound_from_2'),
$r('app.media.ic_sound_from_3')
]
@Local
sendSoundIndex: number = 2
receiveSoundImages = [
$r('app.media.ic_sound_to_1'),
$r('app.media.ic_sound_to_2'),
$r('app.media.ic_sound_to_3')
]
//定时器id
intervalId?: number
aboutToDisappear(): void {
AudioPlayerManager.instance.stopPlayAll()
}
onPlayStart: () => void = () => {
this.startPlayAni()
}
onPlayFinished: () => void = () => {
clearInterval(this.intervalId)
this.receiveSoundIndex = 2
this.sendSoundIndex = 2
}
showReceiveStyle(): boolean {
return this.message.isReceiveMessage() || (this.isReceiveStyle !== undefined && this.isReceiveStyle)
}
build() {
Row() {
if (this.showReceiveStyle()) {
Image(this.sendSoundImages[this.sendSoundIndex]).objectFit(ImageFit.Auto).width(28).height(28)
Text(getAudioMessageText(this.message))
.margin({ left: 8 }).maxLines(1)
.fontColor('#ff333333')
.fontSize(14)
} else {
Text(getAudioMessageText(this.message))
.margin({ right: 8 }).maxLines(1)
.fontColor('#ff333333')
.fontSize(14)
Image(this.receiveSoundImages[this.receiveSoundIndex]).objectFit(ImageFit.Auto).width(28).height(28)
}
}
.justifyContent(this.showReceiveStyle() ? FlexAlign.Start : FlexAlign.End)
.margin({
left: 12,
top: 4,
bottom: 4,
right: 12
})
.onClick(() => {
clearInterval(this.intervalId)
this.intervalId = undefined
let audioManager = AudioPlayerManager.instance
if (this.message.message.attachment) {
audioManager.avPlayerLive((this.message.message.attachment as V2NIMMessageAudioAttachment).url ?? '',
this.onPlayStart,
this.onPlayFinished
)
}
})
// .gesture(LongPressGesture({ duration: 300 }).onAction((event: GestureEvent) => {
// this.onMessageClick?.onItemLongClick?.(event, this.message)
// }))
.width(getAudioMessageWidth(this.message))
.height(ChatConst.audioMessageWidth)
}
startPlayAni() {
this.intervalId = setInterval(() => {
if (this.sendSoundIndex === 2) {
this.sendSoundIndex = 0
} else {
this.sendSoundIndex++
}
if (this.receiveSoundIndex === 2) {
this.receiveSoundIndex = 0
} else {
this.receiveSoundIndex++
}
}, 600);
}
}

View File

@ -0,0 +1,568 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { NIMMessageInfo } from '../model/NIMMessageInfo';
import { V2NIMConversationType, V2NIMMessageSendingState, V2NIMMessageType } from '@nimsdk/base';
import { ChatInfo } from '../model/ChatInfo';
import { getNotificationMessageContent, getPinMessagesTips } from '../common/MessageHelper';
import { AvatarColorUntil, AvatarItem, CommonAvatar } from '@nimkit/common';
import { messageContent } from './MessageComponentBuilder';
import { MessageItemClick } from './MessageItemClick';
import { ChatKitConfig } from '../ChatKitConfig';
/**
* 消息组件按照UI样式划分为发送消息组件、接收消息组件、通知消息组件和提示消息组件
*/
@ComponentV2
export struct MessageComponent {
@Require
@Param
message: NIMMessageInfo | undefined = undefined;
@Require
@Param
chatInfo: ChatInfo
@Param
onMessageClick: MessageItemClick | undefined = undefined
@Param
onMoreButtonClick?: (event: ClickEvent, msg: NIMMessageInfo | undefined) => void = undefined
@Param
onPinItemClick?: (event: ClickEvent, msg: NIMMessageInfo | undefined) => void = undefined
@Param
showSelect: boolean = false;
build() {
if (this.message?.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION) {
NotificationMessageComponent({
message: this.message,
messageContent: getNotificationMessageContent(this.message, this.chatInfo)
});
} else if (this.message?.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIPS) {
TipsMessageComponent({ message: this.message });
} else if (this.message?.isReceiveMessage()) {
ReceiveMessageComponent({
message: this.message,
chatUserInfo: this.chatInfo,
onMessageClick: this.onMessageClick,
showRadio: this.showSelect,
});
} else {
SenderMessageComponent({
message: this.message,
chatUserInfo: this.chatInfo,
onMessageClick: this.onMessageClick,
showRadio: this.showSelect,
});
}
}
}
// 发送消息组件
@ComponentV2
export struct SenderMessageComponent {
AlignLeft: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
AlignRight: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
}
@Require
@Param
message: NIMMessageInfo | undefined = undefined
@Require
@Param
chatUserInfo: ChatInfo | undefined = undefined
@Param
onMessageClick: MessageItemClick | undefined = undefined
@Param
showRadio: boolean = false
@Local
radioOn: boolean = false
build() {
Column() {
if (this.message !== undefined) {
if (this.message?.getMessageTime() !== '') {
Row() {
Text(this.message?.getMessageTime()).fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
}.justifyContent(FlexAlign.Center).width('100%').height(20)
}
Row() {
if (this.showRadio) {
if (this.message.isRevokeMsg) {
Text().margin({ top: (this.message?.getMessageHeight(this.getUIContext()) - 20) / 2, left: 16 })
.height('20')
.width('20')
} else {
Toggle({ type: ToggleType.Checkbox, isOn: this.message.isSelectedMsg })
.margin({ top: (this.message?.getMessageHeight(this.getUIContext()) - 20) / 2, left: 16 })
.height('20')
.width('20')
.onChange((value: boolean) => {
this.onMessageClick?.onMultiSelect?.(value, this.message)
this.radioOn = value;
})
}
}
Column() {
Row() {
if (this.message.message.sendingState === V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED) {
Image($r("app.media.ic_chat_message_status_fail")).width(16).height(16)
.alignRules({
bottom: { anchor: "msgContainer", align: VerticalAlign.Bottom },
right: { anchor: "msgContainer", align: HorizontalAlign.Start }
}).margin({ right: 6 })
.onClick((event: ClickEvent) => {
if (this.onMessageClick) {
this.onMessageClick?.onSendFailClick?.(event, this.message)
}
})
} else if (this.message.message.sendingState ===
V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING) {
Progress({ value: 0, total: 100, type: ProgressType.Ring })
.width(16)
.height(16)
.color($r('app.color.color_chat_send'))
.style({ strokeWidth: 3, status: ProgressStatus.LOADING })
.alignRules({
bottom: { anchor: "msgContainer", align: VerticalAlign.Bottom },
right: { anchor: "msgContainer", align: HorizontalAlign.Start }
})
.margin({ right: 6 })
} else if (this.message.message.sendingState ===
V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED && ChatKitConfig.messageReadState
&& this.message.configReadReceipt()) {
if (!this.message.isRevokeMsg) {
if (this.message.message.conversationType == V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P) {
if (this.message.readCount >= 0) {
// Image(this.message.readCount == 0 ? $r('app.media.ic_chat_read_status_unread') :
// $r('app.media.ic_chat_read_status_read'))
// .width(16)
// .height(16)
// .alignRules({
// bottom: { anchor: "msgContainer", align: VerticalAlign.Bottom },
// right: { anchor: "msgContainer", align: HorizontalAlign.Start }
// })
// .margin({ right: 6 })
//去掉已读未读
}
} else {
if (this.message.readCount !== -1) {
if (this.message.unReadCount == 0) {
// Image(this.message.readCount == 0 ? $r('app.media.ic_chat_read_status_unread') :
// $r('app.media.ic_chat_read_status_read')).width(16).height(16)
// .alignRules({
// bottom: { anchor: "msgContainer", align: VerticalAlign.Bottom },
// right: { anchor: "msgContainer", align: HorizontalAlign.Start }
// }).margin({ right: 6 })
//去掉已读未读
} else {
Stack({}) {
Circle({ width: 15, height: 15 }).fill($r('app.color.color_chat_send'))
Progress({
value: this.message.readCount,
total: (this.message.readCount + this.message.unReadCount),
type: ProgressType.Eclipse
})
.width(13.8)
.height(13.8)
.color($r('app.color.color_chat_send'))
.backgroundColor($r('app.color.color_chat_page_bg'))
.style({
strokeWidth: 1
})
.onClick((event: ClickEvent) => {
this.onMessageClick?.onReadReceiptClick?.(event, this.message)
})
}
.alignRules({
bottom: { anchor: "msgContainer", align: VerticalAlign.Bottom },
right: { anchor: "msgContainer", align: HorizontalAlign.Start }
})
.margin({ right: 6 })
.onClick((event: ClickEvent) => {
this.onMessageClick?.onReadReceiptClick?.(event, this.message)
})
}
}
}
}
}
messageContent({
message: this.message,
onMessageClick: this.onMessageClick,
chatInfo: this.chatUserInfo
})
.backgroundColor($r('app.color.color_chat_send_message_bg'))
.borderRadius(6)
// .backgroundImage($r('app.media.green_bg'))
// .backgroundImageSize({ width: '100%', height: '100%' }) // 背景覆盖整个组件区域
// .backgroundImageResizable({
// slice: { top: 100, left: 50, bottom: 2, right: 60 } // 圆角区域不拉伸
//
// })
.onClick((event: ClickEvent) => {
if (this.showRadio) {
this.radioOn = !this.radioOn
}
this.onMessageClick?.onItemClick?.(event, this.message)
})
.margin({ top: 4, right: 6 })
.flexShrink(1)
.id("msgContainer")
}
.justifyContent(FlexAlign.End)
.alignItems(VerticalAlign.Bottom)
.margin({ right: 8 })
.width('100%')
//.height(this.message.getMessageHeight(this.getUIContext()))
.gesture(LongPressGesture()
.onAction((event: GestureEvent) => {
this.onMessageClick?.onItemLongClick?.(event, this.message)
}))
if (this.message?.isPinMsg) {
Row() {
Image($r('app.media.ic_chat_message_pin')).height(12).width(12).margin({ top: 6 })
Text(getPinMessagesTips(this.message, this.chatUserInfo))
.fontSize(12)
.fontColor($r('app.color.color_chat_pin_tips'))
.margin({ top: 6, left: 6 })
.maxLines(1)
.ellipsisMode(EllipsisMode.END)
}.width('100%').height('26vp').margin({ bottom: 6, right: 12 }).justifyContent(FlexAlign.End)
}
}.alignItems(HorizontalAlign.End).margin({ left: 8 }).width('75%')
CommonAvatar({
item: new AvatarItem(this.chatUserInfo != null ?
this.chatUserInfo?.getCurrentUserAvatarUrl() : '',
this.chatUserInfo?.getCurrentUserAvatarName() ?? '',
AvatarColorUntil.getBackgroundColorById(this.message.message.senderId),
),
longPressGesture: () => {
this.onMessageClick?.onAvatarLongPress?.( this.message)
}
})
.width(36)
.height(36)
.borderRadius(20)
.margin({ right: 16, top: 6 })
.id("mineAvatar")
.onClick(() => {
this.onMessageClick?.onAvatarClick?.(this.message)
})
}
.width('100%')
.backgroundColor(this.message.isPinMsg ? $r('app.color.color_chat_pin_bg') :
$r('app.color.color_chat_page_bg'))
.justifyContent(FlexAlign.End)
.alignItems(VerticalAlign.Top)
}
}
.width('100%').margin({ top: 6, bottom: 6 })
}
}
// 接收消息组件
@ComponentV2
export struct ReceiveMessageComponent {
AlignLeft: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
@Require
@Param
message: NIMMessageInfo | undefined = undefined
@Require
@Param
chatUserInfo: ChatInfo | undefined = undefined
@Param
showRadio: boolean = false
@Param
onMessageClick: MessageItemClick | undefined = undefined
@Local
radioOn: boolean = false
build() {
Column() {
if (this.message !== undefined) {
if (this.message?.getMessageTime() !== '') {
Row() {
Text(this.message?.getMessageTime())
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
}.justifyContent(FlexAlign.Center).width('100%').height(20)
.margin({ top: 6 })
}
Row() {
if (this.showRadio) {
if (this.message.isRevokeMsg) {
Text().margin({ top: (this.message?.getMessageHeight(this.getUIContext()) - 20) / 2, left: 16 })
.height('20')
.width('20')
} else {
Toggle({ type: ToggleType.Checkbox, isOn: this.message.isSelectedMsg })
.margin({ top: (this.message?.getMessageHeight(this.getUIContext()) - 20) / 2, left: 16 })
.height('20')
.width('20')
.onChange((value: boolean) => {
this.onMessageClick?.onMultiSelect?.(value, this.message)
// this.radioOn = value;
})
}
}
CommonAvatar({
item: new AvatarItem(this.chatUserInfo != null ?
this.chatUserInfo?.getChatUserAvatarUrl(this.message?.message) : '',
this.chatUserInfo?.getChatUserAvatarName(this.message?.message) ?? '',
AvatarColorUntil.getBackgroundColorById(this.message?.message.senderId ?? '')
),
longPressGesture: () => {
this.onMessageClick?.onAvatarLongPress?.( this.message)
}
})
.width(36)
.height(36)
.borderRadius(20)
.margin({ left: 16, top: 6 })
.id("otherAvatar")
.onClick(() => {
this.onMessageClick?.onAvatarClick?.(this.message)
})
Column() {
if (this.message?.message.conversationType !== V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P) {
Text(this.chatUserInfo?.getChatUserShowName(this.message?.message))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.maxLines(1)
.width(150)
.textAlign(TextAlign.Start)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.height(25)
}
Column() {
messageContent({
message: this.message,
onMessageClick: this.onMessageClick,
chatInfo: this.chatUserInfo
})
.backgroundColor($r('app.color.color_chat_receive_message_bg'))
.borderRadius(6)
.id("msgContainer")
.alignRules(this.AlignLeft)
.onClick((event: ClickEvent) => {
if (this.showRadio) {
this.radioOn = !this.radioOn
}
this.onMessageClick?.onItemClick?.(event, this.message)
})
}
.width('100%')
//.height(this.message?.getMessageHeight(this.getUIContext()))
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Center)
.gesture(LongPressGesture().onAction((event: GestureEvent) => {
this.onMessageClick?.onItemLongClick?.(event, this.message)
}))
if (this.message?.isPinMsg) {
Row() {
Image($r('app.media.ic_chat_message_pin')).height(12).width(12).margin({ top: 6 })
Text(getPinMessagesTips(this.message, this.chatUserInfo))
.fontSize(12)
.fontColor($r('app.color.color_chat_pin_tips'))
.margin({ top: 6, left: 6 })
.maxLines(1)
.ellipsisMode(EllipsisMode.CENTER)
}.width('100%').height('26vp')
}
}.alignItems(HorizontalAlign.Start).margin({ left: 8, top: 6 }).width('68%')
}
.width('100%')
.alignItems(VerticalAlign.Top)
.backgroundColor(this.message?.isPinMsg ? $r('app.color.color_chat_pin_bg') :
$r('app.color.color_chat_page_bg'))
.id('itemContainer')
}
}.width('100%').padding({ top: 6, bottom: 6 })
}
}
// 通知消息组件
@ComponentV2
export struct NotificationMessageComponent {
@Require
@Param
message: NIMMessageInfo | null = null;
@Require
@Param
messageContent: string;
build() {
Column() {
if (this.message !== null && this.messageContent !== '') {
if (this.message?.getMessageTime() !== '') {
Text(this.message?.getMessageTime())
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textAlign(TextAlign.Center)
.width('100%')
.height(20)
}
Row() {
Text(this.messageContent ?? $r('app.string.chat_message_empty_notification_text'))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.padding({
left: 6,
top: 6,
bottom: 6,
right: 6
})
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
}
.width('100%')
// .height(this.message?.getMessageHeight(this.getUIContext()))
.padding({ top: 6, bottom: 6 })
.justifyContent(FlexAlign.Center)
.borderRadius(6)
.id("msgContainer")
}
}.margin({ left: 60, right: 60 })
}
}
// 提示消息组件
@ComponentV2
export struct TipsMessageComponent {
@Require
@Param
message: NIMMessageInfo | null = null;
build() {
Column() {
if (this.message !== null && this.message.message.text !== undefined && this.message.message.text !== '') {
if (this.message?.getMessageTime() !== '') {
Row() {
Text(this.message?.getMessageTime()).fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
}.justifyContent(FlexAlign.Center).width('100%').height(20)
}
Row() {
Text(this.message?.message.text ?? $r('app.string.chat_message_empty_tip_text'))
.lineHeight(20)
.padding({
left: 6,
top: 6,
bottom: 6,
right: 6
})
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
}
.width('100%')
// .height(this.message?.getMessageHeight(this.getUIContext()))
.padding({ top: 6, bottom: 6 })
.margin({ left: 60, right: 60, top: 6 })
.justifyContent(FlexAlign.Center)
.borderRadius(6)
.id("msgContainer")
}
}
}
}
/**
* 合并转发详情页中的消息组件
*/
@ComponentV2
export struct MergeDetailMessageComponent {
AlignLeft: Record<string, Record<string, string | VerticalAlign | HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
@Require
@Param
message: NIMMessageInfo | undefined = undefined
@Require
@Param
chatUserInfo: ChatInfo | undefined = undefined
@Param
onMessageClick: MessageItemClick | undefined = undefined
build() {
Column() {
if (this.message !== undefined) {
if (this.message?.getMessageTime() !== '') {
Row() {
Text(this.message?.getMessageTime())
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
}.justifyContent(FlexAlign.Center).width('100%').height(20)
.margin({ top: 4 })
}
Row() {
CommonAvatar({
item: new AvatarItem(this.chatUserInfo != null ?
this.chatUserInfo?.getChatUserAvatarUrl(this.message?.message) : '',
this.chatUserInfo?.getChatUserAvatarName(this.message?.message) ?? '',
AvatarColorUntil.getBackgroundColorById(this.message?.message.senderId ?? '')),
longPressGesture: () => {
this.onMessageClick?.onAvatarLongPress?.( this.message)
}
})
.width(36)
.height(36)
.borderRadius(20)
.margin({ left: 16, top: 3 })
.id("otherAvatar")
.onClick(() => {
this.onMessageClick?.onAvatarClick?.(this.message)
})
Column() {
Text(this.chatUserInfo?.getChatUserShowName(this.message?.message))
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.maxLines(1)
.width(150)
.textAlign(TextAlign.Start)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
Column() {
messageContent({ message: this.message, onMessageClick: this.onMessageClick })
.backgroundColor($r('app.color.color_chat_receive_message_bg'))
.borderRadius(6)
.id("msgContainer")
.alignRules(this.AlignLeft)
.onClick((event: ClickEvent) => {
this.onMessageClick?.onItemClick?.(event, this.message)
})
} .height(this.message?.getMessageHeight(this.getUIContext()))
.margin({ top: 6 })
}.alignItems(HorizontalAlign.Start).margin({ left: 8 }).width('68%')
.gesture(LongPressGesture().onAction((event: GestureEvent) => {
this.onMessageClick?.onItemLongClick?.(event, this.message)
}))
}
.width('100%')
.alignItems(VerticalAlign.Top)
.backgroundColor(this.message?.isPinMsg ? $r('app.color.color_chat_pin_bg') :
$r('app.color.color_chat_page_bg'))
.id('itemContainer')
}
}.width('100%').padding({ top: 3})
}
}

View File

@ -0,0 +1,972 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*
*/
import { NEEmojiParseResult } from '../manager/NEEmojiManager'
import { V2NIMMessageFileAttachment, V2NIMMessageLocationAttachment, V2NIMMessageType,V2NIMMessageAttachment } from '@nimsdk/base'
import {
getAitNodes,
getCallMessageIcon,
getCallMessageText,
getFileMessageIcon,
getFileMessageName,
getFileMessageSize,
getImageHeight,
getImageWidth,
getReplyMessageText,
getVideoMessageThumbnail,
getVideoMessageUrl,
parseMessageText,
parseReplyMessageUserInfo,
parseText,
sliceMessageText
} from '../common/MessageHelper'
import { NIMMessageInfo } from '../model/NIMMessageInfo'
import { MessageItemClick } from './MessageItemClick'
import { MessageAudioItemView } from '../view/MessageAudioItemView'
import { image } from '@kit.ImageKit'
import { staticMap } from '@kit.MapKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { ChatAitNode } from '../model/ChatAitNode'
import { MergedMessageAttachment } from '@nimkit/chatkit'
import { ChatInfo } from '../model/ChatInfo'
import { Markdown } from '@nimkit/markdown'
import { BasicConstant,DataWebModel } from '@itcast/basic'
import { router } from '@kit.ArkUI'
@ComponentV2
export struct messageContent {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@BuilderParam
chatInfo: ChatInfo | undefined
build() {
if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
if (this.message.isAiStreamMessage() == false) {
textBuilder({
message: this.message,
onMessageClick: this.onMessageClick,
chatInfo: this.chatInfo
});
} else {
richTextBuilder({
message: this.message,
onMessageClick: this.onMessageClick,
chatInfo: this.chatInfo
});
}
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION) {
locationBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
imageBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO) {
videoBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE) {
fileBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO) {
MessageAudioItemView({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
callBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
if (this.message.isMergeMsg) {
mergedMessageBuilder({ message: this.message, onMessageClick: this.onMessageClick })
} else {
customBuilder({ message: this.message, onMessageClick: this.onMessageClick });
}
} else {
unknownBuilder({ message: this.message, onMessageClick: this.onMessageClick });
}
}
}
@ComponentV2
export struct pinMessageContent {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick?: MessageItemClick
build() {
if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
pinTextBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION) {
pinLocationBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE) {
imageBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO) {
videoBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE) {
fileBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO) {
MessageAudioItemView({ message: this.message, onMessageClick: this.onMessageClick, isReceiveStyle: true });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL) {
callBuilder({ message: this.message, onMessageClick: this.onMessageClick });
} else if (this.message.getMessageType() == V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
if (this.message.isMergeMsg) {
mergedMessageBuilder({ message: this.message, onMessageClick: this.onMessageClick }).width('80%')
} else {
unknownBuilder({ message: this.message, onMessageClick: this.onMessageClick });
}
} else {
unknownBuilder({ message: this.message, onMessageClick: this.onMessageClick });
}
}
}
@ComponentV2
export struct richTextBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@BuilderParam
chatInfo: ChatInfo | undefined
private onItemClick(event: ClickEvent) {
this.onMessageClick?.onReplyClick?.(event, this.message.replyMsg)
}
private onRevokeEditClick(event: ClickEvent) {
this.onMessageClick?.onRevokeEditClick?.(event, this.message)
}
build() {
Column() {
if (!this.message.isRevokeMsg && this.message.isReplyMsg) {
if (this.message.replyMsg) {
Text() {
if (this.chatInfo) {
Span(' | ' + parseReplyMessageUserInfo(this.message.replyMsg, this.chatInfo))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_desc'))
}
ForEach(sliceMessageText(parseMessageText(getReplyMessageText(this.message.replyMsg)), 30),
(item: NEEmojiParseResult) => {
if (item.text) {
Span(item.text)
.fontSize($r('app.float.chat_desc_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_desc'))
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`)).width('16')
.height('16')
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.height(20)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.margin({ bottom: 4 })
.onClick((event) => this.onItemClick(event))
} else {
// Text(getContext().resourceManager.getStringSync($r('app.string.chat_reply_not_exist').id))
// .fontColor($r('app.color.color_chat_desc'))
// .fontSize($r('app.float.chat_desc_text_font_size'))
// .height(20)
// .maxLines(1)
// .ellipsisMode(EllipsisMode.END)
// .margin({ bottom: 4 })
}
}
Column() {
if (this.message.isRevokeMsg) {
Span(this.message.message.text)
if (this.message.revokeEditMsg) {
Text($r('app.string.chat_msg_undo_edit_tips'))
.fontColor($r('app.color.color_chat_send'))
.onClick((event) => {
this.onRevokeEditClick(event)
})
}
} else {
ForEach(parseMessageText(this.message.message.text?.trimStart()?.trimEnd()), (item: NEEmojiParseResult) => {
if (item.text) {
ForEach(getAitNodes(item.startIndex, item.text, this.message.message.serverExtension),
(node: ChatAitNode) => {
if (node.segment) {
Span(node.text)
.fontSize($r('app.float.chat_message_text_font_size'))
.textCase(TextCase.Normal)
.fontColor('#337EFF')
} else {
Markdown({
content: node.text,
lineSpace: 0,
textLineSpace: 6,
fontStyle: {
fontColor: $r('app.color.color_chat_title'),
fontSize: $r('app.float.chat_message_text_font_size')
},
})
}
})
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`))
.width(18)
.height(18)
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
}
}.alignItems(HorizontalAlign.Start)
.padding({
left: 12,
top: 12,
bottom: 12,
right: 12
})
}
}
@ComponentV2
export struct textBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@BuilderParam
chatInfo: ChatInfo | undefined
private onItemClick(event: ClickEvent) {
this.onMessageClick?.onReplyClick?.(event, this.message.replyMsg)
}
private onRevokeEditClick(event: ClickEvent) {
this.onMessageClick?.onRevokeEditClick?.(event, this.message)
}
build() {
Column() {
if (!this.message.isRevokeMsg && this.message.isReplyMsg) {
if (this.message.replyMsg) {
Text() {
if (this.chatInfo) {
Span(' | ' + parseReplyMessageUserInfo(this.message.replyMsg, this.chatInfo))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_desc'))
}
ForEach(sliceMessageText(parseMessageText(getReplyMessageText(this.message.replyMsg)), 30),
(item: NEEmojiParseResult) => {
if (item.text) {
Span(item.text)
.fontSize($r('app.float.chat_desc_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_desc'))
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`)).width('16')
.height('16')
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.height(20)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.margin({ bottom: 4 })
.onClick((event) => this.onItemClick(event))
} else {
// Text(getContext().resourceManager.getStringSync($r('app.string.chat_reply_not_exist').id))
// .fontColor($r('app.color.color_chat_desc'))
// .fontSize($r('app.float.chat_desc_text_font_size'))
// .height(20)
// .maxLines(1)
// .ellipsisMode(EllipsisMode.END)
// .margin({ bottom: 4 })
//去掉消息已撤回
}
}
Text() {
if (this.message.isRevokeMsg) {
Span(this.message.message.text)
if (this.message.revokeEditMsg) {
Span($r('app.string.chat_msg_undo_edit_tips'))
.fontColor($r('app.color.color_chat_send'))
.onClick((event) => {
this.onRevokeEditClick(event)
})
}
} else {
ForEach(parseMessageText(this.message.message.text?.trimStart()?.trimEnd()), (item: NEEmojiParseResult) => {
if (item.text) {
ForEach(getAitNodes(item.startIndex, item.text, this.message.message.serverExtension),
(node: ChatAitNode) => {
if (node.segment) {
Span(node.text)
.fontSize($r('app.float.chat_message_text_font_size'))
.textCase(TextCase.Normal)
.fontColor('#337EFF')
} else {
Span(node.text)
.fontSize($r('app.float.chat_message_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_title'))
}
})
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`))
.width(18)
.height(18)
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
}
.lineHeight(20)
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
}.alignItems(HorizontalAlign.Start)
.padding({
left: 12,
top: 12,
bottom: 12,
right: 12
})
}
}
@ComponentV2
export struct pinTextBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
build() {
Column() {
Text() {
ForEach(parseMessageText(this.message.message.text?.trimStart()?.trimEnd()), (item: NEEmojiParseResult) => {
if (item.text) {
Span(item.text)
.fontSize($r('app.float.chat_message_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_title'))
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`)).width('18')
.height('18')
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
.fontColor($r('app.color.color_chat_title'))
.lineHeight(20)
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
}
}
}
@ComponentV2
export struct imageBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@Local thumbUrl?: string
@Local url?: string
build() {
Column() {
Image(this.thumbUrl ?? this.url)
.objectFit(ImageFit.Cover)
.width(getImageWidth(this.message))
.height(getImageHeight(this.message))
.borderRadius(8)
.gesture(LongPressGesture().onAction((event: GestureEvent) => {
this.onMessageClick?.onItemLongClick?.(event, this.message)
}), GestureMask.IgnoreInternal)
}
}
aboutToAppear(): void {
this.url = this.message.getImageUrl()
this.message.getImageThumbUrl().then((thumbUrl) => {
this.thumbUrl = thumbUrl
})
}
}
@ComponentV2
export struct videoBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@Local videoMessageThumbnail?: image.PixelMap
showLoading() {
if ((this.message.message.attachment as V2NIMMessageFileAttachment).path === undefined) {
if (this.message.downloadProgress > 0) {
return true
}
}
return false
}
async aboutToAppear(): Promise<void> {
this.videoMessageThumbnail = await getVideoMessageThumbnail(this.message)
}
build() {
Stack() {
Image(this.videoMessageThumbnail ?? getVideoMessageUrl(this.message)).objectFit(ImageFit.Auto)
.width(getImageWidth(this.message)).height(getImageHeight(this.message))
.borderRadius(8)
Image($r('app.media.ic_chat_message_video')).objectFit(ImageFit.Auto)
.width(60).height(60)
.visibility(!this.showLoading() ? Visibility.Visible : Visibility.Hidden)
Row() {
Column()
.width(3)
.height(18)
.backgroundColor(Color.White)
.borderRadius(3)
Column()
.width(3)
.height(18)
.backgroundColor(Color.White)
.borderRadius(3)
.margin({
left: 6
})
}
.width(12)
.height(18)
.backgroundColor(Color.Transparent)
.visibility(this.showLoading() ? Visibility.Visible : Visibility.Hidden)
Progress({ value: 0, total: 100, type: ProgressType.Ring })
.width(42)
.height(42)
.value(this.message.downloadProgress)
.color(Color.White)// 进度条前景色为灰色
.style({ strokeWidth: 3 })// 设置strokeWidth进度条宽度为15.0vp
.visibility(this.showLoading() ? Visibility.Visible : Visibility.Hidden)
}.gesture(LongPressGesture().onAction((event: GestureEvent) => {
this.onMessageClick?.onItemLongClick?.(event, this.message)
}), GestureMask.IgnoreInternal)
}
}
@ComponentV2
export struct callBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
build() {
Row() {
if (this.message.isReceiveMsg) {
Image(getCallMessageIcon(this.message)).width(24).height(24).align(Alignment.Start)
Text(getCallMessageText(this.message)).lineHeight(20).margin({ left: 6 })
} else {
Text(getCallMessageText(this.message)).lineHeight(20)
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.fontColor($r('app.color.color_chat_title'))
Image(getCallMessageIcon(this.message)).width(24).height(24).align(Alignment.Start).margin({ left: 6 })
}
}
.padding({
left: 12,
top: 12,
bottom: 12,
right: 12
})
}
}
@ComponentV2
export struct fileBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
build() {
Row() {
Stack({}) {
Image(getFileMessageIcon(this.message))
.objectFit(ImageFit.Auto)
.width(35)
.height(35)
.margin({ left: 12, top: 12, bottom: 12 })
Column()
.width(35)
.height(35)
.margin({ left: 12, top: 12, bottom: 12 })
.backgroundColor($r('app.color.color_chat_converse_bg'))
.opacity(0.8)
.borderRadius(6)
.visibility(this.message.downloadProgress >= 0 ? Visibility.Visible : Visibility.Hidden)
Progress({ value: 0, total: 100, type: ProgressType.Ring })
.width(20)
.height(20)
.value(this.message.downloadProgress)
.margin({ left: 16, top: 16, bottom: 12 })
.color(Color.White)// 进度条前景色为灰色
.style({ strokeWidth: 3 })// 设置strokeWidth进度条宽度为15.0vp
.visibility(this.message.downloadProgress >= 0 ? Visibility.Visible : Visibility.Hidden)
}
Column() {
Text(getFileMessageName(this.message))
.fontColor($r('app.color.color_chat_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Start)
.width('100%')
.height(20)
.maxLines(1)
.ellipsisMode(EllipsisMode.END)
Text(getFileMessageSize(this.message))
.fontColor('#666666')
.fontSize(10)
.width('100%')
.textAlign(TextAlign.Start)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.margin({ top: 2 })
}.padding({ left: 12, right: 12 }).width('70%').align(Alignment.Start)
}
}
}
@ComponentV2
export struct locationBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@Local imagePex: image.PixelMap | undefined
aboutToAppear(): void {
if (this.message) {
let attachment = this.message.message.attachment as V2NIMMessageLocationAttachment;
let staticMapMarker: staticMap.StaticMapMarker = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
defaultIconSize: staticMap.IconSize.SMALL
};
let staticMapOptions: staticMap.StaticMapOptions = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
zoom: 15,
imageWidth: 300,
imageHeight: 100,
markers: [staticMapMarker],
};
// 获取静态图
staticMap.getMapImage(staticMapOptions).then((value) => {
this.imagePex = value;
console.info("netease location Succeeded in getting image.");
}).catch((error: BusinessError) => {
console.info("netease location fail in getting image.", error.code, error.message);
});
}
}
build() {
Column() {
Text(this.message?.message.text)
.fontColor($r('app.color.color_chat_title'))
.fontSize($r('app.float.chat_title_text_font_size'))
.textAlign(TextAlign.Start)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, top: 12, right: 12 })
Text((this.message?.message.attachment as V2NIMMessageLocationAttachment).address)
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textAlign(TextAlign.Start)
.maxLines(1)
.margin({ top: 6 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, right: 12 })
if (this.imagePex) {
Image(this.imagePex).width('100%').height(88).margin({ top: 8 })
} else {
Image($r('app.media.ic_chat_location_default')).width('100%').height(88).margin({ top: 8 })
}
}.alignItems(HorizontalAlign.Start)
}
}
@ComponentV2
export struct pinLocationBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@Local imagePex: image.PixelMap | undefined
aboutToAppear(): void {
if (this.message) {
let attachment = this.message.message.attachment as V2NIMMessageLocationAttachment;
let staticMapMarker: staticMap.StaticMapMarker = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
defaultIconSize: staticMap.IconSize.SMALL
};
let staticMapOptions: staticMap.StaticMapOptions = {
location: {
latitude: attachment.latitude,
longitude: attachment.longitude
},
zoom: 15,
imageWidth: 300,
imageHeight: 100,
markers: [staticMapMarker],
};
// 获取静态图
staticMap.getMapImage(staticMapOptions).then((value) => {
this.imagePex = value;
console.info("netease location Succeeded in getting image.");
}).catch((error: BusinessError) => {
console.info("netease location fail in getting image.", error.code, error.message);
});
}
}
build() {
Column() {
Text(this.message?.message.text)
.fontColor($r('app.color.color_chat_title'))
.fontSize($r('app.float.chat_title_text_font_size'))
.textAlign(TextAlign.Start)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, top: 12, right: 12 })
Text((this.message?.message.attachment as V2NIMMessageLocationAttachment).address)
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textAlign(TextAlign.Start)
.maxLines(1)
.margin({ top: 6 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, right: 12 })
if (this.imagePex) {
Image(this.imagePex).width('100%').height(88).margin({ top: 8 })
} else {
Image($r('app.media.ic_chat_location_default')).width('100%').height(88).margin({ top: 8 })
}
}.alignItems(HorizontalAlign.Start)
.width('80%')
}
}
@ComponentV2
export struct unknownBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
build() {
Column() {
Text($r('app.string.chat_msg_unknown_type'))
.fontColor($r('app.color.color_chat_title'))
.lineHeight(20)
.fontSize($r('app.float.chat_subtitle_text_font_size'))
}
.padding({
left: 12,
top: 12,
bottom: 12,
right: 12
})
}
}
@ComponentV2
export struct mergedMessageBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
mergedAttachment: MergedMessageAttachment | undefined
aboutToAppear(): void {
if (this.message) {
this.mergedAttachment = this.message.customAttachment as MergedMessageAttachment
}
}
build() {
Column() {
if (this.mergedAttachment) {
Text($r('app.string.chat_merged_message_title', this.mergedAttachment.sessionName))
.fontColor($r('app.color.color_chat_title'))
.fontSize($r('app.float.chat_title_text_font_size'))
.height(18)
.margin({ top: 6, bottom: 4 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.CENTER)
Text() {
ForEach(parseText(this.message.mergedContent), (item: NEEmojiParseResult) => {
if (item.text) {
Span(item.text)
.fontSize($r('app.float.chat_message_text_font_size'))
.textCase(TextCase.Normal)
.fontColor($r('app.color.color_chat_desc'))
} else if (item.emoji) {
ImageSpan($rawfile(`emoji/${item.emoji.file}`)).width('18')
.height('18')
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
}
})
}
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.maxLines(3)
.lineHeight(20)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.ellipsisMode(EllipsisMode.END)
.margin({ bottom: 6 })
Line().width('100%').height(1).backgroundColor($r('app.color.color_chat_divider_line'))
Text($r('app.string.chat_merged_message_desc'))
.fontColor($r('app.color.color_chat_desc'))
.fontSize($r('app.float.chat_subtitle_text_font_size'))
.height(18).margin({ top: 6, bottom: 6 })
}
}
.margin({
left: 4,
top: 4,
bottom: 4,
right: 4
})
.padding({ left: 12, right: 6 })
.backgroundColor($r('app.color.color_chat_page_bg'))
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
}
}
@ComponentV2
export struct customBuilder {
@BuilderParam
message: NIMMessageInfo
@BuilderParam
onMessageClick: MessageItemClick | undefined
@Local photoUrl:ResourceStr=$r('app.media.mine_logo')
@Local title:string=''
@Local content:string=''
@Local customType:string=''
@Local customUrl:string=''
aboutToAppear(): void {
if (this.message) {
//
let attachment = this.message.message.attachment as V2NIMMessageAttachment ;
let custom=JSON.parse(attachment.raw+'') as customAttachment
this.title=custom.gdxz_title
this.customType=custom.gdxz_type
this.customUrl=custom.gdxz_url
if(BasicConstant.CouTeach==custom.gdxz_type)
{
this.content='点击查看问题详情'
this.photoUrl=$r('app.media.icon_chatting_file')
}
else if(BasicConstant.VisitTeach==custom.gdxz_type)
{
this.content='肝胆相照®肝胆病在线公共服务平台'
this.photoUrl=$r('app.media.mine_logo')
}
else {
if(custom.gdxz_content==null||custom.gdxz_content.trim()=='')
{
this.content='肝胆相照®肝胆病在线公共服务平台'
}
else
{
this.content=custom.gdxz_content
}
if(custom.gdxz_title!=null&&custom.gdxz_title.includes("互联网医院"))
{
this.photoUrl=custom.gdxz_img
}
else
{
this.photoUrl=$r('app.media.mine_logo')
}
}
}
}
build() {
Column() {
Text(this.title)
.fontColor($r('app.color.color_chat_title'))
.fontSize($r('app.float.chat_message_text_font_size'))
.textAlign(TextAlign.Start)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, top: 12, right: 12 })
Row()
{
Text(this.content)
.fontColor($r('app.color.color_chat_sub_title'))
.fontSize($r('app.float.chat_desc_text_font_size'))
.textAlign(TextAlign.Start)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.padding({ left: 12, top: 12, right: 12 })
.layoutWeight(1)
Image(this.photoUrl).width(57)
.height(57)
.margin({ bottom: 12, top: 12, right: 12 })
}
.alignItems(VerticalAlign.Top)
// Text((this.message?.message.attachment as V2NIMMessageLocationAttachment).address)
// .fontColor($r('app.color.color_chat_desc'))
// .fontSize($r('app.float.chat_desc_text_font_size'))
// .textAlign(TextAlign.Start)
// .maxLines(1)
// .margin({ top: 6 })
// .textOverflow({ overflow: TextOverflow.Ellipsis })
// .padding({ left: 12, right: 12 })
// if (this.imagePex) {
// Image(this.imagePex).width('100%').height(88).margin({ top: 8 })
// } else {
// Image($r('app.media.ic_chat_location_default')).width('100%').height(88).margin({ top: 8 })
// }
}.alignItems(HorizontalAlign.Start)
.onClick(()=>{
if(BasicConstant.CouTeach==this.customType)
{
// Intent intent = new Intent(UIUtils.getContext(), PublicServiceDetailsActivity.class);
// intent.putExtra("consultUuid", teachAttachment.getMessigeid());
// intent.putExtra("isCloseAnswer", true);
// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK );
// UIUtils.getContext().startActivity(intent);
}
else if(this.title!=null&&this.title.includes("互联网医院"))
{
// IWXAPI api = WXAPIFactory.createWXAPI(getActivity(), Constant.WeId);
// WXLaunchMiniProgram.Req req = new WXLaunchMiniProgram.Req();
// req.userName = "gh_9cd2fd72eb57"; // 填小程序原始id
// req.path = "/Pages/yishi/index/index"; //拉起小程序页面的可带参路径,不填默认拉起小程序首页,对于小游戏,可以只传入 query 部分,来实现传参效果,如:传入 "?foo=bar"。
// // req.miniprogramType = WXLaunchMiniProgram.Req.MINIPROGRAM_TYPE_PREVIEW;// 可选打开 开发版,体验版和正式版
// api.sendReq(req);
}
else if (this.customUrl!=null)
{
if (this.customUrl.includes("video")){
// intent = new Intent(UIUtils.getContext(), VideoDetilActivity.class);
// intent.putExtra("uuid", teachAttachment.getMessigeid());
}else {
// intent = new Intent(UIUtils.getContext(), NewsDetailActivity.class);
}
if (this.customUrl.includes("outpatient_details")
||this.customUrl.includes("wxPatient/index.htm#/outPatient")){
// intent.putExtra("title", "门诊详情");
ToWeb(this.customUrl,'门诊详情')
}else if(this.customUrl.includes(BasicConstant.getNewWa))
{
ToWeb(this.customUrl,'纽娃复合营养素固体饮料')
// intent.putExtra("title", UIUtils.getContext().getResources().getStringArray(R.array.chat_menu_shop)[0]);
// intent.putExtra("flag", 1);
}
else
{
// intent.putExtra("newsTitle", teachAttachment.getTitle()+"");
// intent.putExtra("kepuuuid", teachAttachment.getMessigeid());
// intent.putExtra("title", "患教详情");
// intent.putExtra("need_inside_share", "no");
}
if(this.customUrl.includes("dcsvip1imapp.cloopen.net:8888"))
{
// intent.putExtra("url",
// ((TeachAttachment) message.getAttachment()).getUrl().split("http://dcsvip1imapp.cloopen.net:8888")[1]);
}
else
{
// intent.putExtra("url", teachAttachment.getUrl());
}
// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK );
// UIUtils.getContext().startActivity(intent);
}
})
.width('80%')
}
}
function ToWeb(u:string,t:string)
{
let paramsInfo: DataWebModel = {
url:u ,
title:t
};
router.pushUrl({
url: 'pages/WebView/WebPage', // 目标url
params: paramsInfo // 添加params属性传递自定义参数
})
}
export interface customAttachment
{
gdxz_title:string;//标题
gdxz_url:string;//内容url
gdxz_content:string;//内容
gdxz_id:string;//id
gdxz_img:string;//照片
gdxz_type:string;//类型
gdxz_ext_data:string;//备用字段json字符串
}

Some files were not shown because too many files have changed in this diff Show More