This commit is contained in:
Your Name
2026-03-11 09:49:47 +08:00
parent 02ae537b4c
commit 38ad60f4bb
290 changed files with 36917 additions and 123 deletions
@@ -0,0 +1,241 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type { IMessageHandler, ITUIChatService } from '../interface/service';
import { JSONToObject, formatTime } from '../utils/common-utils';
export default class MessageHandler implements IMessageHandler {
private TUIChatService: ITUIChatService;
private userShowNameMap: Map<string, string>; // Map<userID, showName>
private requestedUserMap: Map<string, number>; // Map<userID, 1>
constructor(TUIChatService: ITUIChatService) {
this.TUIChatService = TUIChatService;
this.userShowNameMap = new Map();
this.requestedUserMap = new Map();
}
private getEngine() {
return this.TUIChatService.getEngine();
}
/**
* 国际化翻译
* @param {string} text 待翻译的文本
* @returns {string}
*/
private t(text: string) {
return text;
}
handleTextMessage(message: Message) {
const ret: any = {
text: message.payload.text,
};
return ret;
}
handleImageMessage(message: Message) {
return {
url: message.payload.imageInfoArray[0].url,
width: message.payload.imageInfoArray[0].width,
height: message.payload.imageInfoArray[0].height,
};
}
handleVideoMessage(message: Message) {
return {
url: message.payload.videoUrl,
snapshotUrl: message.payload.snapshotUrl,
snapshotWidth: message.payload.snapshotWidth,
snapshotHeight: message.payload.snapshotHeight,
};
}
handleCustomMessage(message: Message) {
const createGroupText = this.handleCreateGroupCustomMessage(message);
return {
custom: this.handleCallKitSignaling(message) || createGroupText || message?.payload?.extension || `[自定义消息]`,
businessID: createGroupText ? 'group_create' : '',
};
}
handleGroupTipsMessage(message: Message) {
const chatEngine = this.getEngine();
const ret: any = {
text: '',
};
// 优先取 nicknick 不存在时取 userID,以下逻辑是为兼容不同群提示消息的处理
let showName: string = message?.nick || message?.payload?.userIDList?.join(',');
if (message?.payload?.memberList?.length > 0) {
showName = '';
message?.payload?.memberList?.map((user: any) => {
const _showName = user?.nick || user?.userID;
showName += `${this.substringByLength(_showName)},`;
return user;
});
showName = showName?.slice(0, -1);
}
return ret;
}
handleGroupSystemMessage(message: Message) {
const groupName = message.payload.groupProfile.name || message.payload.groupProfile.groupID;
const ret = {
text: '',
};
return ret;
}
handleCallKitSignaling(message: any) {
const callMessage: any = JSONToObject(message.payload.data);
if (callMessage?.businessID !== 1) {
return '';
}
const objectData = JSONToObject(callMessage?.data);
const userID = message.fromAccount || message.from; // 取 message.fromAccount 兼容会话 lastMsg
const myUserID = this.getEngine().getMyUserID();
let messageSender = message.nameCard || message.nick || userID;
messageSender = this.substringByLength(messageSender);
switch (callMessage?.actionType) {
case 1: {
if (objectData?.data?.cmd === 'audioCall' || objectData?.data?.cmd === 'videoCall') {
if (callMessage?.groupID) {
return `${messageSender} ${this.t('message.custom.发起通话')}`;
}
}
if (objectData?.data?.cmd === 'hangup') {
if (callMessage?.groupID) {
return `${this.t('message.custom.通话结束')}`;
}
return `${this.t('message.custom.通话时长')}${formatTime(objectData?.call_end)}`;
}
if (objectData?.data?.cmd === 'switchToAudio') {
return `${this.t('message.custom.切换语音通话')}`;
}
if (objectData?.data?.cmd === 'switchToVideo') {
return `${this.t('message.custom.切换视频通话')}`;
}
// CDM 异常时【发起通话】作为默认返回值
return `${this.t('message.custom.发起通话')}`;
}
case 2:
if (callMessage?.groupID) {
return `${messageSender} ${this.t('message.custom.取消通话')}`;
}
if (this.isOldUIKit('message.custom.已取消')) {
return this.t('message.custom.取消通话');
}
if (callMessage?.inviter === myUserID) {
return this.t('message.custom.已取消');
}
return this.t('message.custom.对方已取消');
case 3:
if (objectData?.data?.cmd === 'switchToAudio') {
return `${this.t('message.custom.切换语音通话')}`;
}
if (objectData?.data?.cmd === 'switchToVideo') {
return `${this.t('message.custom.切换视频通话')}`;
}
if (callMessage?.groupID) {
return `${messageSender} ${this.t('message.custom.已接听')}`;
}
return this.t('message.custom.已接听');
case 4:
if (callMessage?.groupID) {
return `${messageSender} ${this.t('message.custom.拒绝通话')}`;
}
if (this.isOldUIKit('message.custom.已拒绝')) {
return this.t('message.custom.拒绝通话');
}
if (objectData?.line_busy === 'line_busy' || objectData?.data.message === 'lineBusy') {
if (callMessage?.inviter === myUserID) {
return this.t('message.custom.对方忙线中');
}
return this.t('message.custom.忙线未接听');
}
if (callMessage?.inviter === myUserID) {
return this.t('message.custom.对方已拒绝');
}
return this.t('message.custom.已拒绝');
case 5:
if (objectData?.data?.cmd === 'switchToAudio') {
return `${this.t('message.custom.切换语音通话')}`;
}
if (objectData?.data?.cmd === 'switchToVideo') {
return `${this.t('message.custom.切换视频通话')}`;
}
if (callMessage?.groupID) {
// 群通话主叫超时
if (userID === callMessage?.inviter) {
this.handleCallkitTimeoutSignaling(callMessage.inviteeList);
let inviteeList = '';
callMessage.inviteeList?.forEach((inviteeUserID: string) => {
const showName = this.userShowNameMap.get(inviteeUserID) || inviteeUserID;
inviteeList += `${this.substringByLength(showName)}`;
});
inviteeList = inviteeList.substring(0, inviteeList.lastIndexOf('、'));
return `${inviteeList} ${this.t('message.custom.无应答')}`;
}
// 群通话被叫超时
return `${messageSender} ${this.t('message.custom.无应答')}`;
}
if (this.isOldUIKit('message.custom.对方无应答')) {
return this.t('message.custom.无应答');
}
if (callMessage?.inviter === myUserID) {
return this.t('message.custom.对方无应答');
}
return this.t('message.custom.超时无应答');
default:
return '';
}
}
private handleCreateGroupCustomMessage(message: Message) {
let text: undefined | string;
const data = JSONToObject(message.payload.data);
if (data?.businessID === 'group_create') {
text = `${data.opUser} ${data.content}`;
}
return text;
}
private handleCallkitTimeoutSignaling(inviteeList: string[] = []) {
if (inviteeList.length === 0) {
return;
}
const userIDList: string[] = [];
inviteeList.forEach((userID: string) => {
const showName = false;
if (showName) {
this.userShowNameMap.set(userID, showName);
} else if (!this.requestedUserMap.has(userID)) {
userIDList.push(userID);
this.requestedUserMap.set(userID, 1);
}
});
// 注意:按照一条主叫超时信令请求一次处理,先不用截流处理
if (userIDList.length > 0) {
this.getEngine().TUIUser.getUserProfile({ userIDList }).then((imResponse: any) => {
const profileList = imResponse.data || [];
profileList.forEach((profile: any) => {
const { userID, nick } = profile;
const showName = nick || userID;
this.userShowNameMap.set(userID, showName);
});
}).catch(() => {
// 请求出错时捕获异常,避免阻塞代码执行流程
});
}
}
private substringByLength(str: string, len = 12) {
return str.length > len ? `${str.slice(0, len)}...` : str;
}
private isOldUIKit(target: string) {
const index = target.lastIndexOf('.');
const flag = target.slice(0, index + 1);
return this.t(target)?.startsWith(flag);
}
}
@@ -0,0 +1,768 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import TUIBase from '../tui-base';
import type { ITUIChatService } from '../interface/service';
import type {
SendMessageParams,
GetMessageListParams,
SendMessageOptions,
GetMessageListHoppingParams,
func,
} from '../type';
import type { IMessageModel } from '../interface/model';
import MessageHandler from './message-handler';
import { JSONToObject, isArray, isUndefined } from '../utils/common-utils';
import { StoreName, ERROR_CODE, ERROR_MSG, ERROR_CODE_ENGINE, ERROR_MSG_ENGINE } from '../const';
import isEmpty from '../utils/is-empty';
export default class TUIChatService extends TUIBase implements ITUIChatService {
static instance: TUIChatService;
private serv: string;
public messageHandler: MessageHandler;
private isSwitching: boolean;
private delayGetHoppingFunction: func | undefined;
private hoppingConfigMap: Map<string, any>;
constructor() {
super();
this.serv = 'TUIChatService';
this.messageHandler = new MessageHandler(this);
this.isSwitching = true;
this.delayGetHoppingFunction = undefined;
this.hoppingConfigMap = new Map();
}
/**
* 获取 TUIChatService 实例
*/
static getInstance() {
if (!TUIChatService.instance) {
TUIChatService.instance = new TUIChatService();
}
return TUIChatService.instance;
}
init() {
const chatEngine = this.getEngine();
chatEngine.eventCenter.addEvent(chatEngine.EVENT.MESSAGE_RECEIVED, this.onMessageReceived.bind(this));
this.onCurrentConversationIDUpdated();
this.onMessageSource();
}
private onMessageReceived(messageList: Message[]) {
this.updateMessageList(messageList, 'received');
// 提供给 TUINotification 组件使用(即时消费,Store 不存储)
this.getEngine().TUIStore.update(StoreName.CHAT, 'newMessageList', messageList);
}
private onCurrentConversationIDUpdated() {
const chatEngine = this.getEngine();
chatEngine.TUIStore.watch(StoreName.CONV, {
currentConversationID: (conversationID) => {
// 记录切换会话开始状态
this.isSwitching = true;
this.delayGetHoppingFunction = undefined;
this.hoppingConfigMap.clear();
// conversation 切换, 需重置 chat 数据
chatEngine.TUIStore.reset(StoreName.CHAT);
// todo 此处特化处理,防止在 switch conversation 的时候调用用户页面还没打开,导致事件没监听
// 注: 此时 Chat Store 内的数值已经在 conversation 模块被 reset
if (!isEmpty(conversationID)) {
this.getMessageList().finally(() => {
// 切换会话完成
this.isSwitching = false;
this.delayGetHoppingFunction && this.delayGetHoppingFunction();
});
}
},
});
}
private onMessageSource() {
const chatEngine = this.getEngine();
chatEngine.TUIStore.watch(StoreName.CHAT, {
messageSource: (targetMessage: IMessageModel) => {
const currentConversationID = this.getStoreData(StoreName.CONV, 'currentConversationID');
// 当前没有打开会话或搜索的消息不在当前会话中,不进行 messageList 切换
if (!currentConversationID || (targetMessage && targetMessage.conversationID !== currentConversationID)) {
return;
}
// targetMessage === undefined 有两种情况,1.切换会话; 2.回到最新消息
if (isUndefined(targetMessage)) {
this.hoppingConfigMap.clear();
chatEngine.TUIStore.update(StoreName.CHAT, 'messageList', []);
chatEngine.TUIStore.update(StoreName.CHAT, 'nextReqMessageID', '');
chatEngine.TUIStore.update(StoreName.CHAT, 'isCompleted', false);
this.getMessageList();
return;
}
const currentMessageList = this.getStoreData(StoreName.CHAT, 'messageList');
const message = currentMessageList && currentMessageList.find((item: IMessageModel) => targetMessage && item.ID === targetMessage.ID);
if (message) {
return;
}
// 防止切换会话,getMessageList 没有执行完成,再执行 getMessageListHopping 会导致 messageList 错乱
if (this.isSwitching) {
this.delayGetHoppingFunction = this.getMessageListHoppingForDown;
return;
}
this.getMessageListHoppingForDown();
},
});
}
private getMessageListHoppingForDown() {
const currentMessageList = this.getStoreData(StoreName.CHAT, 'messageList');
const { conversationID, sequence, time, ID } = this.getStoreData(StoreName.CHAT, 'messageSource');
const message = currentMessageList && currentMessageList.find((item: IMessageModel) => ID && item.ID === ID);
if (message) {
return;
}
const chatEngine = this.getEngine();
chatEngine.TUIStore.update(StoreName.CHAT, 'messageList', []);
chatEngine.TUIStore.update(StoreName.CHAT, 'nextReqMessageID', '');
chatEngine.TUIStore.update(StoreName.CHAT, 'isCompleted', false);
this.getMessageListHopping({ conversationID, sequence, time, direction: 1 });
}
/**
* 私有方法,获取 store data
*/
private getStoreData(storeName: StoreName, key: string) {
return this.getEngine().TUIStore.getData(storeName, key);
}
/**
* 私有方法,调用 TIM 接口发送消息
* @param {Message} message 要发送的 message
* @param {SendMessageOptions} [options] 消息发送选项
* @returns {Promise<any>} im 接口的 response 原样返回
*
*/
private sendMessage(message: Message, options?: SendMessageOptions) {
this.updateMessageList([message], 'send'); // 不论成功失败,先消息上屏,更新 UI
const promise = this.getEngine().chat.sendMessage(message, options);
return this.getResponse(promise);
}
/**
* 私有方法,用于 messageList 状态变更并 return
* @param {Promise} promise 调用 im 接口返回的 promise
* @param {boolean} success 成功回调是否更新数据
* @param {boolean} fail 失败回调是否更新数据
* @returns {Promise<any>} im 接口的 response 原样返回
*/
private getResponse(promise: Promise<any>, success = true, fail = true) {
return promise
.then((imResponse: any) => {
const messageList = imResponse.data.messageList ? imResponse.data.messageList : [imResponse.data.message];
success && this.updateMessageList(messageList, 'edit');
return imResponse;
})
.catch((error: any) => {
fail && error?.data?.message && this.updateMessageList([error.data.message], 'edit');
return Promise.reject(error);
});
}
updateMessageList(messageList: Message[], type = '') {
if (this.getStoreData(StoreName.CHAT, 'messageSource') && type !== 'unshift' && type !== 'edit') {
return;
}
const currentMessageList = this.getStoreData(StoreName.CHAT, 'messageList');
const tempMessageList = this.updateTargetMessageList(messageList, currentMessageList, type);
this.getEngine().TUIStore.update(StoreName.CHAT, 'messageList', tempMessageList);
}
updateTargetMessageList(newMessageList: Message[], target: IMessageModel[], type = '') {
const conversationID = this.getStoreData(StoreName.CONV, 'currentConversationID');
// filter current conversation message
let toBeUpdatedMessageList: Message[] = newMessageList.filter(item => item.conversationID === conversationID);
// handle call signaling message
toBeUpdatedMessageList = this.handleC2CCallSignaling(toBeUpdatedMessageList);
if (!type || toBeUpdatedMessageList.length === 0) return target;
const currentMessageList: any = target || [];
let tempMessageList: Message[] = [];
// When Chat is integrated into Room, it needs to be updated.
if (type === 'send' || type === 'push' || type === 'received') {
const userInfo: any = this.getStoreData(StoreName.CHAT, 'userInfo');
if (Object.keys(userInfo).length > 0) {
this.updateLocalMessage(toBeUpdatedMessageList, userInfo);
}
}
const enableAutoMessageRead = this.getStoreData(StoreName.APP, 'enableAutoMessageRead');
switch (type) {
case 'edit':
for (const currentMessage of target) {
const message = toBeUpdatedMessageList.find(m => m.ID === currentMessage.ID);
tempMessageList.push(message || currentMessage);
}
break;
case 'resend':
// Resending message is not typing and only one message
tempMessageList = currentMessageList.filter((message: IMessageModel) => message.ID !== toBeUpdatedMessageList[0].ID).concat(toBeUpdatedMessageList);
break;
case 'send':
tempMessageList = currentMessageList.concat(toBeUpdatedMessageList);
break;
case 'push':
// message from call or room
tempMessageList = currentMessageList.concat(toBeUpdatedMessageList);
this.getEngine().chat.setMessageRead({ conversationID });
break;
case 'received':
// message from MESSAGE_RECEIVED
tempMessageList = currentMessageList.concat(toBeUpdatedMessageList);
tempMessageList = this.sortMessageList(tempMessageList);
if (enableAutoMessageRead) {
this.getEngine().chat.setMessageRead({ conversationID });
}
break;
case 'unshift':
// Deduplicate the data in the Store when pulling roaming
tempMessageList = toBeUpdatedMessageList.filter((message: Message) => (
(currentMessageList.length === 0)
|| !currentMessageList.find((m: Message | IMessageModel) => m.ID === message.ID)
));
tempMessageList.push(...currentMessageList);
tempMessageList = this.sortMessageList(tempMessageList);
break;
default:
break;
}
return tempMessageList;
}
enterTypingState() {
const enableTyping = this.getStoreData(StoreName.APP, 'enableTyping');
if (enableTyping) {
this.sendTyping(true);
}
}
leaveTypingState() {
const enableTyping = this.getStoreData(StoreName.APP, 'enableTyping');
if (enableTyping) {
this.sendTyping(false);
}
}
private sendTyping(status: boolean) {
const chatEngine = this.getEngine();
const currentConversationID = this.getStoreData(StoreName.CONV, 'currentConversationID');
if (!currentConversationID.startsWith(chatEngine.TYPES.CONV_C2C)) return;
const to = currentConversationID.replace(chatEngine.TYPES.CONV_C2C, '');
if (status) {
const messageList: IMessageModel[] = this.getStoreData(StoreName.CHAT, 'messageList');
const receivedMessageList = messageList.filter((item: IMessageModel) => item.flow === 'in');
if (receivedMessageList.length === 0) return;
const latestMessageTime = receivedMessageList[receivedMessageList.length - 1].time * 1000;
const nowTime = new Date().getTime();
const diff = nowTime - latestMessageTime;
if (diff > 1000 * 30) return;
}
}
quoteMessage(message: Message) {
this.getEngine().TUIStore.update(StoreName.CHAT, 'quoteMessage', {
message,
type: 'quote',
});
this.getEngine().TUIReport?.reportFeature(205);
return message;
}
replyMessage(message: Message) {
this.getEngine().TUIStore.update(StoreName.CHAT, 'quoteMessage', {
message,
type: 'reply',
});
return message;
}
private getCurrentConvInfo() {
const { conversationID = '', type } = this.getStoreData(StoreName.CONV, 'currentConversation') || {};
const to = conversationID.replace(type, '');
return {
to,
conversationType: type,
};
}
private getMessageAbstractAndType(message: Message) {
const chatEngine = this.getEngine();
const data = {
abstract: '',
type: 0,
};
// 对齐 native, abstract 不传
// 兼容低版本,已传入的 abstract 不进行调整,新增的不传 abstract
switch (message.type) {
case chatEngine.TYPES.MSG_TEXT:
data.abstract = message?.payload?.text;
data.type = 1;
break;
case chatEngine.TYPES.MSG_CUSTOM:
data.abstract = '[自定义消息]';
data.type = 2;
break;
case chatEngine.TYPES.MSG_IMAGE:
data.abstract = '[图片]';
data.type = 3;
break;
case chatEngine.TYPES.MSG_AUDIO:
data.abstract = '[语音]';
data.type = 4;
break;
case chatEngine.TYPES.MSG_VIDEO:
data.abstract = '[视频]';
data.type = 5;
break;
case chatEngine.TYPES.MSG_FILE:
data.abstract = '[文件]';
data.type = 6;
break;
case chatEngine.TYPES.MSG_LOCATION:
data.type = 7;
break;
case chatEngine.TYPES.MSG_FACE:
data.abstract = '[表情]';
data.type = 8;
break;
case chatEngine.TYPES.MSG_GRP_TIP:
data.type = 9;
break;
case chatEngine.TYPES.MSG_MERGER:
data.abstract = message?.payload?.title;
data.type = 10;
break;
default:
break;
}
return data;
}
/**
* - 注意: 暂只支持文本发送时携带引用消息,结构体参考 https://iwiki.woa.com/pages/viewpage.action?pageId=1238962637
*/
private genMessageReply(message: Message, type: string) {
if (type !== 'reply' && type !== 'quote') {
return {};
}
const { abstract, type: messageType } = this.getMessageAbstractAndType(message);
const messageReplyRoot: any = {
messageAbstract: abstract,
messageSender: message.nick || message.from,
messageID: message.ID,
};
const messageReply: any = {
...messageReplyRoot,
messageType,
messageTime: message?.time,
messageSequence: message?.sequence,
version: 1,
};
if (type === 'reply') {
messageReply.messageRootID = message.ID;
if (message.cloudCustomData) {
const rootCloudCustomData = JSONToObject(message.cloudCustomData);
if (rootCloudCustomData.messageReply && rootCloudCustomData.messageReply.messageRootID) {
messageReply.messageRootID = rootCloudCustomData.messageReply.messageRootID;
}
}
}
return {
messageReply,
messageReplyRoot,
};
}
private getMessageInfo(options: SendMessageParams, message: Message, type: string) {
const { messageReply, messageReplyRoot } = this.genMessageReply(message, type);
const cloudCustomData = options.cloudCustomData ? JSONToObject(options.cloudCustomData) : {};
let rootMessage: any;
if (cloudCustomData.messageReply) {
cloudCustomData.messageReply = { ...messageReply, ...cloudCustomData.messageReply };
} else {
cloudCustomData.messageReply = messageReply;
}
if (type === 'reply') {
const { messageRootID } = messageReply;
rootMessage = this.getEngine().chat.findMessage(messageRootID);
const rootCloudCustomData = rootMessage?.cloudCustomData ? JSONToObject(rootMessage.cloudCustomData) : {};
if (!rootCloudCustomData.messageReplies) {
rootCloudCustomData.messageReplies = {};
}
if (!isArray(rootCloudCustomData.messageReplies.replies)) {
rootCloudCustomData.messageReplies.replies = [];
}
rootCloudCustomData.messageReplies.replies.push(messageReplyRoot);
rootMessage.cloudCustomData = JSON.stringify(rootCloudCustomData);
}
return {
cloudCustomData: JSON.stringify(cloudCustomData),
rootMessage,
};
}
sendTextMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const chatEngine = this.getEngine();
const { message: quoteMessage, type } = this.getStoreData(StoreName.CHAT, 'quoteMessage');
let quoteInfo = {
cloudCustomData: options.cloudCustomData || '',
rootMessage: undefined,
};
if (quoteMessage) {
quoteInfo = this.getMessageInfo(options, quoteMessage, type);
}
const message = chatEngine.chat.createTextMessage({
...this.getCurrentConvInfo(),
...options,
cloudCustomData: quoteInfo.cloudCustomData,
});
return this.sendMessage(message, sendMessageOptions).then((imResponse: any) => {
if (quoteInfo.rootMessage) {
this.modifyMessage(quoteInfo.rootMessage as any);
}
chatEngine.TUIStore.reset(StoreName.CHAT, ['quoteMessage'], true);
return imResponse;
});
}
sendTextAtMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const chatEngine = this.getEngine();
const { message: quoteMessage, type } = this.getStoreData(StoreName.CHAT, 'quoteMessage');
let quoteInfo = {
cloudCustomData: options.cloudCustomData || '',
rootMessage: undefined,
};
if (quoteMessage) {
quoteInfo = this.getMessageInfo(options, quoteMessage, type);
}
const message = chatEngine.chat.createTextAtMessage({
...this.getCurrentConvInfo(),
...options,
cloudCustomData: quoteInfo.cloudCustomData,
});
return this.sendMessage(message, sendMessageOptions).then((imResponse: any) => {
if (quoteInfo.rootMessage) {
this.modifyMessage(quoteInfo.rootMessage as any);
}
chatEngine.TUIStore.reset(StoreName.CHAT, ['quoteMessage'], true);
return imResponse;
});
}
sendImageMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const message = this.getEngine().chat.createImageMessage({
...this.getCurrentConvInfo(),
...options,
onProgress: (progress: number) => {
this.onProgress(message.ID, progress);
},
});
return this.sendMessage(message, sendMessageOptions);
}
sendAudioMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const message = this.getEngine().chat.createAudioMessage({
...this.getCurrentConvInfo(),
...options,
onProgress: (progress: number) => {
this.onProgress(message.ID, progress);
},
});
return this.sendMessage(message, sendMessageOptions);
}
sendVideoMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const message = this.getEngine().chat.createVideoMessage({
...this.getCurrentConvInfo(),
...options,
onProgress: (progress: number) => {
this.onProgress(message.ID, progress);
},
});
return this.sendMessage(message, sendMessageOptions);
}
sendCustomMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
const message = this.getEngine().chat.createCustomMessage({
...this.getCurrentConvInfo(),
...options,
});
return this.sendMessage(message, sendMessageOptions);
}
private onProgress(messageID: string, progress: number) {
const message: IMessageModel = this.getEngine().TUIStore.getMessageModel(messageID);
if (message) {
// 避免频繁触发 messageList 更新
const diff = progress - message.progress;
if (diff >= 0.1 || progress === 1) {
message.progress = progress;
this.updateMessageList([message], 'edit');
}
}
}
resendMessage(message: Message) {
message.status = 'unSend';
this.updateMessageList([message], 'resend');
const promise = this.getEngine().chat.resendMessage(message);
return this.getResponse(promise, true, true);
}
setMessageExtensions(message: Message, extensions: object[]) {
return this.getEngine().chat.setMessageExtensions(message, extensions);
}
getMessageExtensions(message: Message) {
return this.getEngine().chat.getMessageExtensions(message);
}
deleteMessageExtensions(message: Message, keyList?: string[]) {
return this.getEngine().chat.deleteMessageExtensions(message, keyList);
}
modifyMessage(message: Message) {
const promise = this.getEngine().chat.modifyMessage(message);
return this.getResponse(promise, true, false).catch((error: any) => {
const { code = 0, data = {} } = error.code;
if (code === ERROR_CODE.MSG_MODIFY_CONFLICT) {
console.warn(`${ERROR_MSG.MSG_MODIFY_CONFLICT} data.message: ${data?.message}`);
} else if (code === ERROR_CODE.MSG_MODIFY_DISABLED_IN_AVCHATROOM) {
console.warn(ERROR_MSG.MSG_MODIFY_DISABLED_IN_AVCHATROOM);
} else if (code === ERROR_CODE.MODIFY_MESSAGE_NOT_EXIST) {
console.warn(ERROR_MSG.MODIFY_MESSAGE_NOT_EXIST);
}
throw error;
});
}
getMessageList(options: Partial<GetMessageListParams> = {
conversationID: this.getStoreData(StoreName.CONV, 'currentConversationID'),
nextReqMessageID: this.getStoreData(StoreName.CHAT, 'nextReqMessageID'),
}) {
const chatEngine = this.getEngine();
if (!chatEngine.chat.isReady()) {
return Promise.reject({
code: ERROR_CODE_ENGINE.GET_MSG_LIST_ERROR,
message: ERROR_MSG_ENGINE.GET_MSG_LIST_ERROR,
});
}
if (this.getStoreData(StoreName.CHAT, 'isCompleted')) {
return Promise.resolve({
data: {
messageList: [],
nextReqMessageID: '',
isCompleted: true,
},
});
}
const messageSource = this.getStoreData(StoreName.CHAT, 'messageSource');
const nextMessageSeq = this.hoppingConfigMap.get('nextMessageSeq');
const nextMessageTime = this.hoppingConfigMap.get('nextMessageTime');
const isHopping = nextMessageSeq || nextMessageTime;
if (messageSource && messageSource.conversationID === options?.conversationID && isHopping) {
return this.getMessageListHopping();
}
return chatEngine.chat.getMessageList(options as GetMessageListParams).then((imResponse: any) => {
const { messageList, nextReqMessageID, isCompleted } = imResponse.data;
const userInfo = this.getStoreData(StoreName.CHAT, 'userInfo');
if (Object.keys(userInfo).length > 0) {
// When Chat is integrated into Room, it needs to be updated.
this.updateLocalMessage(messageList, userInfo);
}
this.updateMessageList(messageList, 'unshift');
chatEngine.TUIStore.update(StoreName.CHAT, 'nextReqMessageID', nextReqMessageID);
chatEngine.TUIStore.update(StoreName.CHAT, 'isCompleted', isCompleted);
// 异步获取消息回应、群已读回执信息(operationType > 0 说明用户已经不在群组内)
// const conversationID = messageList[0]?.conversationID;
// const { operationType = 0 } = this.getEngine().TUIStore.getConversationModel(conversationID) || {};
// if (operationType === 0) {
// this.getMessageReactions({ messageList });
// this.readReceiptHandler.getMessageReadReceiptList(messageList);
// }
return imResponse;
}).catch((error: any) => Promise.reject(error));
}
getMessageListHopping(options: GetMessageListHoppingParams = {
conversationID: this.getStoreData(StoreName.CHAT, 'messageSource')?.conversationID,
sequence: this.hoppingConfigMap.get('nextMessageSeq'),
time: this.hoppingConfigMap.get('nextMessageTime'),
}) {
const chatEngine = this.getEngine();
const promise = chatEngine.chat.getMessageListHopping(options);
return promise.then((imResponse: any) => {
const { messageList, nextMessageSeq, nextMessageTime, isCompleted } = imResponse.data;
const _nextMessageSeq = options.direction === 1 ? options.sequence : nextMessageSeq;
const _nextMessageTime = options.direction === 1 ? options.time : nextMessageTime;
this.updateMessageList(messageList, 'unshift');
this.delayGetHoppingFunction = undefined;
this.hoppingConfigMap.set('nextMessageSeq', _nextMessageSeq);
this.hoppingConfigMap.set('nextMessageTime', _nextMessageTime);
chatEngine.TUIStore.update(StoreName.CHAT, 'isCompleted', isCompleted);
return imResponse;
}).catch((error: any) => Promise.reject(error));
}
clearHistoryMessage(conversationID: string) {
const chatEngine = this.getEngine();
return chatEngine.chat.clearHistoryMessage(conversationID).then((imResponse: any) => {
chatEngine.TUIStore.update(StoreName.CHAT, 'messageList', []);
chatEngine.TUIStore.update(StoreName.CHAT, 'nextReqMessageID', '');
chatEngine.TUIStore.update(StoreName.CHAT, 'isCompleted', false);
return imResponse;
});
}
private updateLocalMessage(messageList: any[], userInfo: Record<string, any>) {
let flag = false;
messageList.forEach((message: any) => {
if (userInfo[message.from]) {
const { nick, nameCard, avatar } = userInfo[message.from];
if (nick) {
message.nick = nick;
flag = true;
}
if (nameCard) {
message.nameCard = nameCard;
flag = true;
}
if (avatar) {
message.avatar = avatar;
flag = true;
}
}
});
return flag;
}
private handleC2CCallSignaling(messageList: Message[]) {
const chatEngine = this.getEngine();
const myUserID = chatEngine.getMyUserID();
const list = messageList.filter((message: Message) => {
const { conversationType, type, payload } = message;
let isDisplayInChat = true;
// handle C2C and custom message
if (conversationType === chatEngine.TYPES.CONV_C2C && type === chatEngine.TYPES.MSG_CUSTOM) {
const signalingInfo = this.getSignalingInfo(message);
if (signalingInfo) {
const callSignaling: any = JSONToObject(payload.data);
// callSignaling(businessID = 1)
if (callSignaling?.businessID === 1) {
const customData = JSONToObject(callSignaling.data);
isDisplayInChat = !((message as any)._isExcludedFromUnreadCount && (message as any)._isExcludedFromLastMessage);
if (isDisplayInChat) {
// handle call signaling when it is not consumed
if (customData?.data?.consumed !== true) {
let inviter = customData?.data?.inviter;
if (customData?.line_busy === 'line_busy' || customData?.data?.message === 'lineBusy') {
inviter = callSignaling.inviter;
}
const { from, to } = message;
// reverse message: currentUser is not inviter
if (inviter !== myUserID && message.from === myUserID) {
const currentConversation = this.getStoreData(StoreName.CONV, 'currentConversation');
message.from = to;
message.to = from;
message.flow = 'in';
message.avatar = currentConversation?.userProfile?.avatar || '';
}
// reverse message: currentUser is inviter
if (inviter === myUserID && message.from !== myUserID) {
const myProfile = this.getStoreData(StoreName.USER, 'userProfile');
message.from = to;
message.to = from;
message.flow = 'out';
message.avatar = myProfile?.avatar;
}
console.log(`${this.serv}.handleC2CCallSignaling myUserID:${myUserID} callSignaling.inviter:${callSignaling.inviter} customData.data.inviter:${customData?.data?.inviter}`);
}
}
}
}
}
return isDisplayInChat;
});
return list;
}
private getSignalingInfo(message: Message) {
const signalingList = this.filterSignalFromMessageList([message]);
if (signalingList.length === 0) {
return;
}
const signalingData = this.getPayloadData(message);
if (!signalingData) {
return;
}
const signaling = {
businessID: signalingData.businessID || 1,
inviteID: signalingData.inviteID,
groupID: signalingData.groupID || '',
inviter: signalingData.inviter || '',
inviteeList: signalingData.inviteeList || [],
data: signalingData.data || '',
actionType: signalingData.actionType || 1,
timeout: signalingData.timeout || 0,
};
return signaling;
}
private filterSignalFromMessageList(messageList: Message[]) {
return messageList.filter((message) => {
const chatEngine = this.getEngine();
if (message.type === chatEngine.TYPES.MSG_CUSTOM) {
const { cloudCustomData = '', payload = {} } = message;
const data = payload?.data || '';
if (!cloudCustomData && !data) {
return false;
}
const isSignalingTypeExisted = cloudCustomData && cloudCustomData.match(/"type":"tsignaling"/);
const isInviteIDExisted = data && data.match(/inviteID/);
const isActionTypeExisted = data && data.match(/actionType/);
return isSignalingTypeExisted || (isInviteIDExisted && isActionTypeExisted);
}
return false;
});
}
private getPayloadData(message: Message) {
const { data } = message.payload;
try {
return JSON.parse(data);
} catch (error) {
console.error(error);
return null;
}
}
private sortMessageList(messageList: Message[] | IMessageModel[]) {
const { conversationType } = messageList[0];
// C2C sort by time
if (conversationType === this.getEngine().TYPES.CONV_C2C) {
return messageList.sort((a, b) => a.time - b.time);
}
// GROUP sort by sequence
const sortedMessageList = messageList.filter(item => item.status === 'success').sort((a, b) => a.sequence - b.sequence);
// Insert not success message into sortedList
for (let i = 0; i < messageList.length; i++) {
if (messageList[i].status !== 'success') {
sortedMessageList.splice(i, 0, messageList[i]);
}
}
return sortedMessageList;
}
}
@@ -0,0 +1,226 @@
import type { Conversation, Message } from '@tencentcloud/lite-chat/basic';
import TUIBase from '../tui-base';
import type { ITUIConversationService } from '../interface/service';
import { StoreName, ERROR_CODE_ENGINE, ERROR_MSG_ENGINE } from '../const';
import { isUndefined } from '../utils/common-utils';
import ConversationModel from '../model/conversation';
export default class TUIConversationService extends TUIBase implements ITUIConversationService {
static instance: TUIConversationService;
private serv: string;
constructor() {
super();
this.serv = 'TUIConversationService';
}
/**
* 获取 TUIConversationService 实例
*/
static getInstance() {
if (!TUIConversationService.instance) {
TUIConversationService.instance = new TUIConversationService();
}
return TUIConversationService.instance;
}
public init() {
const chatEngine = this.getEngine();
chatEngine.eventCenter.addEvent(chatEngine.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated.bind(this));
chatEngine.eventCenter.addEvent(chatEngine.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED, this.onTotalUnreadCountUpdated.bind(this));
chatEngine.eventCenter.addEvent(chatEngine.EVENT.MESSAGE_RECEIVED, this.onMessageReceived.bind(this));
this.getConversationInitData();
}
private onConversationListUpdated(conversationList: Conversation[]) {
const tmpConversationList = this.filterSystemConversation(conversationList);
this.getEngine().TUIStore.update(StoreName.CONV, 'conversationList', tmpConversationList);
this.updateCurrentConversation();
}
private onTotalUnreadCountUpdated(totalUnreadCount: number) {
this.getEngine().TUIStore.update(StoreName.CONV, 'totalUnreadCount', totalUnreadCount);
}
private onMessageReceived(messageList: Message[]) {
const chatEngine = this.getEngine();
const conversationList = this.getEngine().TUIStore.getData(StoreName.CONV, 'conversationList');
let toBeUpdated = false;
for (let i = 0; i < messageList.length; i++) {
if (messageList[i].type !== chatEngine.TYPES.MSG_GRP_SYS_NOTICE) {
continue;
}
const { operationType } = messageList[i].payload;
const conversationID = `GROUP${messageList[i].to}`;
const condition1 = (operationType === 4 || operationType === 5 || operationType === 8); // 群成员被踢(operationType=4)、群组被解散(operationType=5)、主动退群(operationType=8)
const condition2 = (operationType === 2 || operationType === 6 || operationType === 7); // 主动加群(operationType=2)、创建群组(operationType=6)、被邀请进群(operationType=7)
if (condition1 || condition2) {
for (let j = 0; j < conversationList.length; j++) {
// C2C 会话直接跳过,节省循环执行时间
if (conversationList[j].type === chatEngine.TYPES.CONV_C2C) {
continue;
}
if (conversationList[j].conversationID === conversationID) {
if (condition1) {
this.getEngine().TUIStore.update(StoreName.CONV, 'operationTypeMap', { conversationID, operationType });
toBeUpdated = true;
break;
}
// 当前登录生命周期内,主动退群、解散群组、被踢后,再次主动加群、创建群组、被邀请进群时需要重置 operationType 为 0
if (condition2 && conversationList[j].operationType > 0) {
this.getEngine().TUIStore.update(StoreName.CONV, 'operationTypeMap', { conversationID, operationType: 0 });
toBeUpdated = true;
break;
}
}
}
}
}
if (toBeUpdated) {
this.getEngine().TUIStore.update(StoreName.CONV, 'conversationList', conversationList);
const currentConversationID = this.getEngine().TUIStore.getData(StoreName.CONV, 'currentConversationID') || '';
const currentConversation = this.findConversation(currentConversationID);
if (currentConversation) {
this.getEngine().TUIStore.update(StoreName.CONV, 'currentConversation', currentConversation);
}
}
}
// 解决 TUIChatEngine 含 UI集成通过 TUILogin 登录时注册 Chat SDK 事件时机后置问题
private getConversationInitData() {
const chatEngine = this.getEngine();
// TUIChatEngine 无 UI 集成时不需要执行此逻辑
if (!chatEngine.chat.isReady()) {
return;
}
chatEngine.chat.getConversationList().then((imResponse: any) => {
const { conversationList, isSyncCompleted } = imResponse.data;
console.log(`${this.serv}.init, getConversationList count:${conversationList.length} isSyncCompleted:${isSyncCompleted}`);
if (conversationList.length > 0) {
this.onConversationListUpdated(conversationList);
const totalUnreadCount: number = chatEngine.chat.getTotalUnreadMessageCount();
this.onTotalUnreadCountUpdated(totalUnreadCount);
}
});
}
public async switchConversation(conversationID: string) {
const logTxt = `${this.serv}.switchConversation`;
const chatEngine = this.getEngine();
if (!conversationID) {
chatEngine.TUIStore.reset(StoreName.CHAT, ['messageList', 'isCompleted', 'nextReqMessageID']);
chatEngine.TUIStore.update(StoreName.CONV, 'currentConversationID', '');
chatEngine.TUIStore.update(StoreName.CONV, 'currentConversation', null);
console.log(`${logTxt} conversationID is empty, conversationID:${conversationID}`);
return Promise.resolve({});
}
if (!conversationID.startsWith(chatEngine.TYPES.CONV_C2C) && !conversationID.startsWith(chatEngine.TYPES.CONV_GROUP)) {
console.warn(`${logTxt} conversationID is invalid, conversationID:${conversationID}`);
return Promise.reject({
code: ERROR_CODE_ENGINE.INVALID_CONV_ID,
message: ERROR_MSG_ENGINE.INVALID_CONV_ID,
});
}
const enableAutoMessageRead = chatEngine.TUIStore.getData(StoreName.APP, 'enableAutoMessageRead');
const currentConversationID = chatEngine.TUIStore.getData(StoreName.CONV, 'currentConversationID');
if (currentConversationID && currentConversationID === conversationID) {
if (enableAutoMessageRead) {
this.setMessageRead(currentConversationID);
}
console.warn(`${logTxt} please check conversationID, conversationID:${conversationID}`);
return Promise.resolve({
code: ERROR_CODE_ENGINE.CONV_ID_SAME,
message: ERROR_MSG_ENGINE.CONV_ID_SAME,
});
}
// 需要切换的目标会话
const targetConversation: any = await this.getConversationModel(conversationID);
if (isUndefined(targetConversation)) {
console.warn(`${logTxt} target conversation is not exist, conversationID:${conversationID}`);
return Promise.reject({
code: ERROR_CODE_ENGINE.CONV_NOT_EXIST,
message: ERROR_MSG_ENGINE.CONV_NOT_EXIST,
});
}
if (enableAutoMessageRead) {
if (currentConversationID) {
this.setMessageRead(currentConversationID);
}
if (conversationID) {
this.setMessageRead(conversationID);
}
}
chatEngine.TUIStore.reset(StoreName.CHAT, ['messageList', 'isCompleted', 'nextReqMessageID']);
chatEngine.TUIStore.update(StoreName.CONV, 'currentConversationID', conversationID);
chatEngine.TUIStore.update(StoreName.CONV, 'currentConversation', targetConversation);
return Promise.resolve(targetConversation);
}
private async getConversationModel(conversationID: string) {
let conversationModel: any = this.findConversation(conversationID);
if (isUndefined(conversationModel)) {
try {
const res: any = await this.getConversationProfile(conversationID);
if (res.data && res.data.conversation) {
conversationModel = new ConversationModel(res.data.conversation);
}
} catch (error) {
conversationModel = undefined;
}
}
return conversationModel;
}
private findConversation(conversationID: string) {
let conversation: any;
const conversationList = this.getEngine().TUIStore.getData(StoreName.CONV, 'conversationList');
for (let i = 0; i < conversationList.length; i++) {
if (conversationList[i].conversationID === conversationID) {
conversation = conversationList[i];
break;
}
}
return conversation;
}
private updateCurrentConversation() {
const chatEngine = this.getEngine();
const currentConversationID: string = chatEngine.TUIStore.getData(StoreName.CONV, 'currentConversationID');
const currentConversation = this.findConversation(currentConversationID);
// 更新当前会话
if (currentConversation) {
chatEngine.TUIStore.update(StoreName.CONV, 'currentConversation', currentConversation);
}
}
public getConversationList() {
return this.getEngine().chat.getConversationList();
}
public getConversationProfile(conversationID: string) {
return this.getEngine().chat.getConversationProfile(conversationID);
}
public clearHistoryMessage(conversationID: string) {
return this.getEngine().chat.clearHistoryMessage(conversationID).then((imResponse: any) => {
this.getEngine().TUIStore.update(StoreName.CHAT, 'messageList', []);
this.getEngine().TUIStore.update(StoreName.CHAT, 'nextReqMessageID', '');
this.getEngine().TUIStore.update(StoreName.CHAT, 'isCompleted', true);
return imResponse;
});
}
public setMessageRead(conversationID: string) {
return this.getEngine().chat.setMessageRead({ conversationID });
}
/**
* 过滤系统会话
* @param {Array<any>} list SDK 更新下来的会话列表数据
*/
private filterSystemConversation(list: any[]) {
const toBeUpdatedList = list.filter(conversation => conversation.type !== this.getEngine().TYPES.CONV_SYSTEM);
return toBeUpdatedList;
}
}
@@ -0,0 +1,198 @@
import type { ChatSDK } from '@tencentcloud/lite-chat/basic';
import TencentCloudChat from '@tencentcloud/lite-chat/basic';
import EventCenter from './event-center';
import type { ITUIChatEngine, IEventCenter } from '../interface/engine';
import type {
ITUIConversationService,
ITUIChatService,
ITUIGroupService,
ITUIUserService,
ITUIReportService,
} from '../interface/service';
import type { ITUIStore } from '../interface/store';
import type { LoginParams } from '../type';
import type { MountedList } from '../const';
import { StoreName } from '../const';
import { isUndefined } from '../utils/common-utils';
import { commercialAbility } from '../config';
import TUIGlobal from '../TUIGlobal/tui-global';
export default class ChatEngine implements ITUIChatEngine {
static instance: ChatEngine;
public isInited: boolean; // 是否执行了初始化
public chat!: ChatSDK;
public EVENT: any;
public TYPES: any;
public eventCenter!: IEventCenter;
public TUIStore!: ITUIStore;
public TUIConversation!: ITUIConversationService;
public TUIChat!: ITUIChatService;
public TUIGroup!: ITUIGroupService;
public TUIUser!: ITUIUserService;
public TUIReport!: ITUIReportService;
private loginStatusPromise: Map<string, unknown>; // 保存 login Promise
private userID: string; // 当前用户 ID
constructor() {
this.EVENT = TencentCloudChat.EVENT;
this.TYPES = TencentCloudChat.TYPES;
this.loginStatusPromise = new Map();
this.userID = '';
this.isInited = false;
}
/**
* 获取 ChatEngine 实例
*/
static getInstance() {
if (!ChatEngine.instance) {
ChatEngine.instance = new ChatEngine();
}
return ChatEngine.instance;
}
mount(name: MountedList, instance: any) {
this[name] = instance;
}
login(options: LoginParams) {
const { chat, SDKAppID, userID } = options;
const isOfficial = SDKAppID === 1400187352 || SDKAppID === 1400188366;
this.createChat(options);
this.userID = userID;
TUIGlobal.getInstance().initOfficial(isOfficial); // 兼容低版本 uikit
this.TUIStore.update(StoreName.APP, 'isOfficial', isOfficial);
this.TUIStore.update(StoreName.APP, 'SDKVersion', (TencentCloudChat as any).VERSION);
this.eventCenter = new EventCenter(this);
this.eventCenter.removeEvents(); // 移除注册的事件监听,避免重复注册
this.resetStore();
this.initService();
if (chat && chat.isReady()) {
console.log('TUIChatEngine.login ok, from TUICore.');
this.TUIUser.getUserProfile();
this.checkCommercialAbility();
return Promise.resolve({});
}
this.eventCenter.addEvent(this.EVENT.SDK_READY, () => {
this.onSDKReady();
});
this.eventCenter.addEvent(this.EVENT.SDK_NOT_READY, () => {
this.onSDKNotReady();
});
return this.loginChat(options);
}
logout() {
this.userID = '';
this.isInited = false;
this.resetStore();
return this.chat.logout();
}
isReady() {
return this.chat?.isReady() || false;
}
setLogLevel(level: number) {
if (!this.chat) {
console.warn('TUIChatEngine 初始化未完成,请确认 TUIChatEngine.login 接口调用是否正常。');
return;
}
this.chat.setLogLevel(level);
}
destroy() {
// 解绑 IM 事件
this.eventCenter.unbindIMEvents();
this.isInited = false;
this.resetStore();
return this.chat.destroy();
}
getMyUserID() {
return this.userID;
}
/**
* 重置 Store
*/
private resetStore() {
this.TUIStore.reset(StoreName.CHAT);
this.TUIStore.reset(StoreName.CONV);
this.TUIStore.reset(StoreName.GRP);
this.TUIStore.reset(StoreName.USER);
this.TUIStore.reset(StoreName.SEARCH);
this.TUIStore.reset(StoreName.FRIEND);
this.TUIStore.reset(StoreName.CUSTOM);
console.log('TUIChatEngine.resetStore ok.');
}
/**
* 初始化 Service
*/
private initService() {
this.TUIChat.init();
this.TUIConversation.init();
this.TUIUser.init();
this.isInited = true;
console.log('TUIChatEngine.initService ok.');
}
private createChat(options: LoginParams) {
const { chat, ...rest } = options;
if (!isUndefined(chat)) {
this.chat = chat;
return;
}
this.chat = TencentCloudChat.create({
...rest,
scene: 'engine-lite',
});
}
private loginChat(options: LoginParams) {
const { userID, userSig } = options;
return new Promise((resolve, reject) => {
this.chat.login({ userID, userSig }).then((imResponse: any) => {
console.log('TUIChatEngine.loginChat ok.');
this.checkCommercialAbility();
if (imResponse.data.repeatLogin && this.chat.isReady()) {
resolve(imResponse);
}
this.loginStatusPromise.set('login', { resolve, reject, imResponse });
}).catch((error: any) => {
reject(error);
});
});
}
private onSDKReady() {
if (this.loginStatusPromise.has('login')) {
const handler: any = this.loginStatusPromise.get('login');
handler.resolve(handler.imResponse);
// 独立使用 engine 登录成功 SDK Ready 后获取一次自己的资料
this.TUIUser.getUserProfile();
}
this.loginStatusPromise.delete('login');
}
private onSDKNotReady() {
if (this.loginStatusPromise.has('login')) {
const handler: any = this.loginStatusPromise.get('login');
handler.reject(new Error('sdk not ready'));
}
this.loginStatusPromise.delete('login');
this.resetStore();
}
// 登录成功后校验商业化能力位
private checkCommercialAbility() {
Object.keys(commercialAbility).forEach((key: string) => {
const ability: number = commercialAbility[key];
this.chat.callExperimentalAPI('isCommercialAbilityEnabled', ability).then((imResponse: any) => {
const { enabled = false } = imResponse.data;
this.TUIStore.update(StoreName.APP, key, enabled);
});
});
}
}
@@ -0,0 +1,116 @@
import type { IEventCenter, ITUIChatEngine } from '../interface/engine';
import type { func } from '../type';
export default class EventCenter implements IEventCenter {
static instance: EventCenter | null;
private engine!: ITUIChatEngine;
private events: any;
constructor(ChatEngine: any) {
if (!EventCenter.instance) {
EventCenter.instance = this;
this.engine = ChatEngine;
this.events = {};
this.bindIMEvents();
}
return EventCenter.instance;
}
public addEvent(event: string, callback: func) {
if (this.events[event]) {
this.events[event].set(callback, 1);
} else {
this.events[event] = new Map();
this.events[event].set(callback, 1);
}
}
public removeEvents() {
Object.keys(this.events).forEach((event) => {
this.events[event].clear();
});
this.events = {};
}
private dispatch(event: string, data: any) {
if (!this.events[event]) {
return;
}
for (const callback of this.events[event].keys()) {
callback.call(this, data);
}
}
private bindIMEvents() {
this.engine.chat.on(this.engine.EVENT.SDK_READY, this.onSDKReady, this);
this.engine.chat.on(this.engine.EVENT.SDK_NOT_READY, this.onSDKNotReady, this);
this.engine.chat.on(this.engine.EVENT.KICKED_OUT, this.onKickedOut, this);
this.engine.chat.on(this.engine.EVENT.MESSAGE_RECEIVED, this.onReceiveMessage, this);
this.engine.chat.on(this.engine.EVENT.MESSAGE_REACTIONS_UPDATED, this.onMessageReactionsUpdated, this);
this.engine.chat.on(this.engine.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated, this);
this.engine.chat.on(this.engine.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED, this.onTotalMessageCountUpdated, this);
this.engine.chat.on(this.engine.EVENT.PROFILE_UPDATED, this.onProfileUpdated, this);
this.engine.chat.on(this.engine.EVENT.GROUP_LIST_UPDATED, this.onGroupListUpdated, this);
this.engine.chat.on(this.engine.EVENT.GROUP_ATTRIBUTES_UPDATED, this.onGroupAttributesUpdated, this);
this.engine.chat.on(this.engine.EVENT.GROUP_COUNTER_UPDATED, this.onGroupCounterUpdated, this);
}
public unbindIMEvents() {
this.engine.chat.off(this.engine.EVENT.SDK_READY, this.onSDKReady, this);
this.engine.chat.off(this.engine.EVENT.SDK_NOT_READY, this.onSDKNotReady, this);
this.engine.chat.off(this.engine.EVENT.KICKED_OUT, this.onKickedOut, this);
this.engine.chat.off(this.engine.EVENT.MESSAGE_RECEIVED, this.onReceiveMessage, this);
this.engine.chat.off(this.engine.EVENT.MESSAGE_REACTIONS_UPDATED, this.onMessageReactionsUpdated, this);
this.engine.chat.off(this.engine.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated, this);
this.engine.chat.off(this.engine.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED, this.onTotalMessageCountUpdated, this);
this.engine.chat.off(this.engine.EVENT.PROFILE_UPDATED, this.onProfileUpdated, this);
this.engine.chat.off(this.engine.EVENT.GROUP_LIST_UPDATED, this.onGroupListUpdated, this);
this.engine.chat.off(this.engine.EVENT.GROUP_ATTRIBUTES_UPDATED, this.onGroupAttributesUpdated, this);
this.engine.chat.off(this.engine.EVENT.GROUP_COUNTER_UPDATED, this.onGroupCounterUpdated, this);
EventCenter.instance = null;
}
private onSDKReady(event: any) {
this.dispatch(this.engine.EVENT.SDK_READY, event.data);
}
private onSDKNotReady(event: any) {
this.dispatch(this.engine.EVENT.SDK_NOT_READY, event.data);
}
private onKickedOut(event: any) {
this.dispatch(this.engine.EVENT.KICKED_OUT, event.data);
}
private onReceiveMessage(event: any) {
this.dispatch(this.engine.EVENT.MESSAGE_RECEIVED, event.data);
}
private onMessageReactionsUpdated(event: any) {
this.dispatch(this.engine.EVENT.MESSAGE_REACTIONS_UPDATED, event.data);
}
private onConversationListUpdated(event: any) {
this.dispatch(this.engine.EVENT.CONVERSATION_LIST_UPDATED, event.data);
}
private onTotalMessageCountUpdated(event: any) {
this.dispatch(this.engine.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED, event.data);
}
private onProfileUpdated(event: any) {
this.dispatch(this.engine.EVENT.PROFILE_UPDATED, event.data);
}
private onGroupListUpdated(event: any) {
this.dispatch(this.engine.EVENT.GROUP_LIST_UPDATED, event.data);
}
private onGroupAttributesUpdated(event: any) {
this.dispatch(this.engine.EVENT.GROUP_ATTRIBUTES_UPDATED, event.data);
}
private onGroupCounterUpdated(event: any) {
this.dispatch(this.engine.EVENT.GROUP_COUNTER_UPDATED, event.data);
}
}
@@ -0,0 +1,41 @@
import { APP_NAMESPACE, IS_PC, IS_H5, IN_WX_MINI_APP, IN_UNI_NATIVE_APP } from '../utils/env';
import type { ITUIGlobal } from '../interface/global';
export default class TUIGlobal implements ITUIGlobal {
static instance: TUIGlobal;
public global: any;
public isOfficial: boolean;
constructor() {
this.global = APP_NAMESPACE;
this.isOfficial = false;
}
/**
* 获取 TUIGlobal 实例
*/
static getInstance() {
if (!TUIGlobal.instance) {
TUIGlobal.instance = new TUIGlobal();
}
return TUIGlobal.instance;
}
initOfficial(isOfficial: boolean) {
this.isOfficial = isOfficial;
}
getPlatform() {
let platform = '';
if (IS_PC) {
platform = 'pc';
} else if (IS_H5) {
platform = 'h5';
} else if (IN_WX_MINI_APP) {
platform = 'wechat';
} else if (IN_UNI_NATIVE_APP && !IN_WX_MINI_APP) {
platform = 'app';
}
return platform;
}
}
@@ -0,0 +1,474 @@
import type { Group, GroupMember, Message } from '@tencentcloud/lite-chat/basic';
import type { ITUIGroupService } from '../interface/service';
import type {
AddMemberParams,
ChangGroupOwnerParams,
CountersParams,
CreateGroupParams,
DeleteMemberParams,
GetGroupProfileParams,
GetMemberListParams,
GetMemberProfileParams,
GroupAttrParams,
JoinGroupParams,
KeyListParams,
MarkMemberParams,
SetCountersParams,
SetMemberCustomFiledParams,
SetMemberMuteParams,
SetMemberNameCardParams,
SetMemberRoleParams,
UpdateGroupParams,
handleGroupApplicationParams,
} from '../type';
import type GroupModel from '../model/group';
import type { IGroupModel } from '../interface/model';
import TUIBase from '../tui-base';
import { StoreName } from '../const';
import { isUndefined } from '../utils/common-utils';
export default class TUIGroupService extends TUIBase implements ITUIGroupService {
static instance: TUIGroupService;
private groupMap: Map<string, any>;
constructor() {
super();
this.groupMap = new Map();
}
/**
* 获取 TUIGroupService 实例
*/
static getInstance() {
if (!TUIGroupService.instance) {
TUIGroupService.instance = new TUIGroupService();
}
return TUIGroupService.instance;
}
/**
* 初始化 Service
*/
public init() {
const chatEngine = this.getEngine();
chatEngine.eventCenter.addEvent(chatEngine.EVENT.GROUP_LIST_UPDATED, this.onGroupListUpdated.bind(this));
chatEngine.eventCenter.addEvent(chatEngine.EVENT.GROUP_ATTRIBUTES_UPDATED, this.onGroupAttributesUpdated.bind(this));
chatEngine.eventCenter.addEvent(chatEngine.EVENT.GROUP_COUNTER_UPDATED, this.onGroupCounterUpdated.bind(this));
chatEngine.eventCenter.addEvent(chatEngine.EVENT.MESSAGE_RECEIVED, this.onMessageReceived.bind(this));
this.getGroupInitData();
}
private onGroupListUpdated(groupList: Group[]) {
const chatEngine = this.getEngine();
chatEngine.TUIStore.update(StoreName.GRP, 'groupList', groupList);
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
groupList.forEach((item: Group) => {
if (item.groupID === currentGroupID) {
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroup', item);
}
});
}
private onGroupAttributesUpdated(data: any) {
const chatEngine = this.getEngine();
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
let groupList = chatEngine.TUIStore.getData(StoreName.GRP, 'groupList');
const { groupID, groupAttributes } = data;
if (currentGroupID === groupID) {
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupAttributes', groupAttributes);
}
groupList = groupList.map((item: GroupModel) => {
if (item.groupID === groupID) {
item.groupAttributes = groupAttributes;
}
return item;
});
chatEngine.TUIStore.update(StoreName.GRP, 'groupList', groupList);
}
private onGroupCounterUpdated(data: any) {
const chatEngine = this.getEngine();
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
const currentGroupCounters = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupCounters') || {};
let groupList = chatEngine.TUIStore.getData(StoreName.GRP, 'groupList');
const { groupID, key, value } = data;
if (currentGroupID === groupID) {
currentGroupCounters[key] = value;
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupCounters', currentGroupCounters);
}
groupList = groupList.map((item: GroupModel) => {
if (item.groupID === groupID) {
item.groupCounters = {
...item.groupCounters,
[key]: value,
};
}
return item;
});
chatEngine.TUIStore.update(StoreName.GRP, 'groupList', groupList);
}
private onMessageReceived(messageList: Message[]) {
const chatEngine = this.getEngine();
const groupSystemNoticeList: Message[] = [];
messageList.forEach((message: Message) => {
if (message.type === chatEngine.TYPES.MSG_GRP_TIP) {
const { payload } = message;
const { operationType, userIDList } = payload;
switch (operationType) {
case chatEngine.TYPES.GRP_TIP_MBR_JOIN:
this.addMemberList(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_MBR_QUIT:
this.removeMemberList(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_MBR_KICKED_OUT:
this.removeMemberList(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_MBR_SET_ADMIN:
this.updateGroupMember(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_MBR_CANCELED_ADMIN:
this.updateGroupMember(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_GRP_PROFILE_UPDATED:
// v2.3.3, update currentGroup by GROUP_LIST_UPDATED when group profile is updated.
break;
case chatEngine.TYPES.GRP_TIP_MBR_PROFILE_UPDATED:
this.updateGroupMember(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_BAN_AVCHATROOM_MEMBER:
this.updateGroupMember(userIDList);
break;
case chatEngine.TYPES.GRP_TIP_UNBAN_AVCHATROOM_MEMBER:
this.updateGroupMember(userIDList);
break;
default:
break;
}
}
if (message.type === chatEngine.TYPES.MSG_GRP_SYS_NOTICE) {
groupSystemNoticeList.push(message);
}
});
if (groupSystemNoticeList.length > 0) {
chatEngine.TUIStore.update(StoreName.GRP, 'groupSystemNoticeList', groupSystemNoticeList);
}
}
private getGroupInitData() {
const chatEngine = this.getEngine();
// TUIChatEngine 无 UI 集成时不需要执行此逻辑
if (!chatEngine.chat.isReady()) {
return;
}
chatEngine.chat.getGroupList().then((imResponse: any) => {
const { groupList = [] } = imResponse.data;
console.log(`TUIGroupService.init, getGroupList count:${groupList.length}`);
if (groupList.length > 0) {
this.onGroupListUpdated(groupList);
}
});
}
private async updateGroupMember(userIDList: string[]) {
const groupID = this.getEngine().TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (groupID) {
const imResponse = await this.getGroupMemberProfile({
groupID,
userIDList,
});
const { memberList } = imResponse.data;
this.updateMemberList(memberList);
}
}
private resetCurrentStore() {
const keyList = [
'currentGroupID',
'currentGroup',
'currentGroupAttributes',
'currentGroupCounters',
'currentGroupMemberList',
];
this.getEngine().TUIStore.reset(StoreName.GRP, keyList, true);
}
public async switchGroup(groupID: string) {
const chatEngine = this.getEngine();
chatEngine.TUIStore.update(StoreName.GRP, 'offset', 0);
chatEngine.TUIStore.update(StoreName.GRP, 'isCompleted', false);
const conversation = chatEngine.TUIStore.getConversationModel(`GROUP${groupID}`);
// operationType > 0 说明用户已经不在群组内了,直接返回
if (conversation?.operationType > 0) {
const currentGroup = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroup');
return Promise.resolve(currentGroup);
}
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (!groupID) {
this.resetCurrentStore();
return Promise.resolve({});
}
if (currentGroupID === groupID) {
const currentGroup = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroup');
return Promise.resolve(currentGroup);
}
this.resetCurrentStore();
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupID', groupID);
try {
await this.getGroupInfo(groupID);
} catch (error) {
throw error
}
// 5s 后删除是为了解决快速反复切换到群管理操作面板是重复调用问题
const timer = setTimeout(() => {
this.groupMap.delete(groupID);
clearTimeout(timer);
}, 5000);
const currentGroup = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroup');
return Promise.resolve(currentGroup);
}
private async getGroupInfo(groupID: string) {
const chatEngine = this.getEngine();
const currentGroupMap = this.groupMap.get(groupID);
if (currentGroupMap) {
this.updateMemberList(currentGroupMap?.memberList || []);
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroup', currentGroupMap.group);
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupAttributes', currentGroupMap.groupAttributes || {});
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupCounters', currentGroupMap.counters || {});
return;
}
const groupInfo = {
group: {},
memberList: [],
groupAttributes: undefined,
counters: undefined,
};
const { data: { group } } = await this.getGroupProfile({ groupID });
groupInfo.group = group;
const { data: { memberList } } = await this.getGroupMemberList({ groupID });
groupInfo.memberList = memberList;
const attributesRes = await this.getGroupAttributes({ groupID, keyList: [] });
const { groupAttributes } = attributesRes.data;
groupInfo.groupAttributes = groupAttributes;
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupAttributes', groupAttributes);
try {
const countersRes = await this.getGroupCounters({ groupID, keyList: [] });
const { counters } = countersRes.data;
groupInfo.counters = counters;
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupCounters', counters);
} catch (error: any) {
console.warn(error?.message); // 旗舰功能,预防初次跑通(直接打印 error uni 打包小程序会报错)
}
this.groupMap.set(groupID, groupInfo);
}
public getGroupProfile(options: GetGroupProfileParams) {
const chatEngine = this.getEngine();
return chatEngine.chat.getGroupProfile(options).then(async (imResponse: any) => {
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (currentGroupID === options.groupID) {
const { group } = imResponse.data;
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroup', group);
}
return imResponse;
});
}
public updateGroupProfile(options: UpdateGroupParams) {
return this.getEngine().chat.updateGroupProfile(options);
}
public createGroup(options: CreateGroupParams) {
return this.getEngine().chat.createGroup(options);
}
public dismissGroup(groupID: string) {
return this.getEngine().chat.dismissGroup(groupID);
}
public searchGroupByID(groupID: string) {
const chatEngine = this.getEngine();
return chatEngine.chat.searchGroupByID(groupID).then((imResponse: any) => {
const { group } = imResponse.data;
const groupList = chatEngine.TUIStore.getData(StoreName.GRP, 'groupList');
imResponse.data.group.isJoinedGroup = groupList.some((item: IGroupModel) => item.groupID === group.groupID);
return imResponse;
});
}
public joinGroup(options: JoinGroupParams) {
return this.getEngine().chat.joinGroup(options);
}
public quitGroup(groupID: string) {
return this.getEngine().chat.quitGroup(groupID);
}
public getGroupApplicationList() {
return this.getEngine().chat.getGroupApplicationList();
}
public handleGroupApplication(options: handleGroupApplicationParams) {
return this.getEngine().chat.handleGroupApplication(options);
}
public getGroupOnlineMemberCount(groupID: string) {
return this.getEngine().chat.getGroupOnlineMemberCount(groupID);
}
public changeGroupOwner(options: ChangGroupOwnerParams) {
return this.getEngine().chat.changeGroupOwner(options);
}
public initGroupAttributes(options: GroupAttrParams) {
return this.getEngine().chat.initGroupAttributes(options);
}
public setGroupAttributes(options: GroupAttrParams) {
return this.getEngine().chat.setGroupAttributes(options);
}
public deleteGroupAttributes(options: KeyListParams) {
return this.getEngine().chat.deleteGroupAttributes(options);
}
public getGroupAttributes(options: KeyListParams) {
return this.getEngine().chat.getGroupAttributes(options);
}
public setGroupCounters(options: SetCountersParams) {
return this.getEngine().chat.setGroupCounters(options);
}
public increaseGroupCounter(options: CountersParams) {
return this.getEngine().chat.increaseGroupCounter(options);
}
public decreaseGroupCounter(options: CountersParams) {
return this.getEngine().chat.decreaseGroupCounter(options);
}
public getGroupCounters(options: KeyListParams) {
return this.getEngine().chat.getGroupCounters(options);
}
// group member
private updateMemberList(memberList: GroupMember[]) {
const chatEngine = this.getEngine();
const currentGroupMemberList = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupMemberList') || [];
const filterMemberList = currentGroupMemberList.filter((currentMember: GroupMember) => {
const isExist = memberList.find((member: GroupMember) => member.userID === currentMember.userID);
return !isExist;
});
const newMemberList = [...filterMemberList, ...memberList];
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupMemberList', newMemberList);
}
private async addMemberList(userIDList: string[]) {
const groupID = this.getEngine().TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (groupID) {
try {
const imResponse = await this.getGroupMemberProfile({
groupID,
userIDList,
});
const { memberList } = imResponse.data;
this.updateMemberList(memberList);
} catch (error) {
const memberList: GroupMember[] = userIDList.map((userID: string) => {
const GroupMember = {
userID,
avatar: '',
nick: '',
role: '',
joinTime: 0,
nameCard: '',
muteUntil: 0,
memberCustomField: [],
};
return GroupMember;
});
this.updateMemberList(memberList);
}
}
}
private removeMemberList(userIDList: string[]) {
const chatEngine = this.getEngine();
const currentGroupMemberList = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupMemberList');
const newMemberList = currentGroupMemberList.filter((member: GroupMember) => userIDList.indexOf(member.userID) === -1);
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroupMemberList', newMemberList);
}
public getGroupMemberList(options: GetMemberListParams) {
const chatEngine = this.getEngine();
if (isUndefined(options.offset)) {
const _offset = chatEngine.TUIStore.getData(StoreName.GRP, 'offset');
options.offset = _offset;
}
return chatEngine.chat.getGroupMemberList(options).then((imResponse: any) => {
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (currentGroupID === options.groupID) {
const { memberList, offset = 0 } = imResponse.data;
this.updateMemberList(memberList);
chatEngine.TUIStore.update(StoreName.GRP, 'offset', offset);
if (offset === 0) {
chatEngine.TUIStore.update(StoreName.GRP, 'isCompleted', true);
}
}
return imResponse;
});
}
public getGroupMemberProfile(options: GetMemberProfileParams) {
return this.getEngine().chat.getGroupMemberProfile(options);
}
public addGroupMember(options: AddMemberParams) {
const chatEngine = this.getEngine();
return chatEngine.chat.addGroupMember(options).then(async (imResponse: any) => {
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (currentGroupID === options.groupID) {
const { successUserIDList, group } = imResponse.data;
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroup', group);
this.addMemberList(successUserIDList);
}
return imResponse;
});
}
public deleteGroupMember(options: DeleteMemberParams) {
const chatEngine = this.getEngine();
return chatEngine.chat.deleteGroupMember(options).then((imResponse: any) => {
const currentGroupID = chatEngine.TUIStore.getData(StoreName.GRP, 'currentGroupID');
if (currentGroupID === options.groupID) {
const { userIDList, group } = imResponse.data;
this.removeMemberList(userIDList);
chatEngine.TUIStore.update(StoreName.GRP, 'currentGroup', group);
}
return imResponse;
});
}
public setGroupMemberMuteTime(options: SetMemberMuteParams) {
return this.getEngine().chat.setGroupMemberMuteTime(options);
}
public setGroupMemberRole(options: SetMemberRoleParams) {
return this.getEngine().chat.setGroupMemberRole(options);
}
public setGroupMemberNameCard(options: SetMemberNameCardParams) {
return this.getEngine().chat.setGroupMemberNameCard(options);
}
public setGroupMemberCustomField(options: SetMemberCustomFiledParams) {
return this.getEngine().chat.setGroupMemberCustomField(options);
}
public markGroupMemberList(options: MarkMemberParams) {
return this.getEngine().chat.markGroupMemberList(options);
}
}
@@ -0,0 +1,25 @@
import TUIBase from '../tui-base';
export default class TUIReportService extends TUIBase {
static instance: TUIReportService;
/**
* Get TUIReportService instance
*/
static getInstance() {
if (!TUIReportService.instance) {
TUIReportService.instance = new TUIReportService();
}
return TUIReportService.instance;
}
/**
* Report key feature usage for analytics
* @param code - Event code for analytics
* @param feature - Feature name or identifier
*/
public reportFeature(code: number, feature?: string) {
return this.getEngine().chat?.callExperimentalAPI('statTUIKeyFeatures', {
code,
msg: feature ? `${code}-${feature}` : ''
});
}
}
@@ -0,0 +1,89 @@
import type { IAppStore, ITasks } from '../../interface/store';
export default class AppStore implements IAppStore {
public store: {
enabledMessageReadReceipt: boolean;
enabledEmojiPlugin: boolean;
enabledOnlineStatus: boolean;
enabledCustomerServicePlugin: boolean;
enabledTranslationPlugin: boolean;
enabledVoiceToText: boolean;
enableTyping: boolean;
enableConversationDraft: boolean;
enableAutoMessageRead: boolean;
isOfficial: boolean;
SDKVersion: string;
tasks: ITasks;
[key: string]: any;
};
public defaultStore = {
enabledMessageReadReceipt: false,
enabledEmojiPlugin: false,
enabledOnlineStatus: false,
enabledCustomerServicePlugin: false,
enabledTranslationPlugin: false,
enabledVoiceToText: false,
enableTyping: true,
enableConversationDraft: true,
enableAutoMessageRead: true,
isOfficial: false,
SDKVersion: '3.0.0',
tasks: {
sendMessage: false,
revokeMessage: false,
modifyNickName: false,
groupChat: false,
muteGroup: false,
dismissGroup: false,
call: false,
searchCloudMessage: false,
customerService: false,
translateTextMessage: false,
},
} as any;
constructor() {
this.store = {
enabledEmojiPlugin: false,
enabledMessageReadReceipt: false,
enabledOnlineStatus: false,
enabledCustomerServicePlugin: false,
enabledTranslationPlugin: false,
enabledVoiceToText: false,
enableTyping: true,
enableConversationDraft: true,
enableAutoMessageRead: true,
isOfficial: false,
SDKVersion: '3.0.0',
tasks: {
sendMessage: false,
revokeMessage: false,
modifyNickName: false,
groupChat: false,
muteGroup: false,
dismissGroup: false,
call: false,
searchCloudMessage: false,
customerService: false,
translateTextMessage: false,
},
};
}
update(key: string, data: any) {
this.store[key] = data;
}
getData(key: string) {
return this.store[key];
}
reset(keyList: string[] = []) {
this.store = {
...this.defaultStore,
...this.store,
...keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key] }), {}),
};
}
}
@@ -0,0 +1,135 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type { IChatStore } from '../../interface/store';
import type { IMessageModel } from '../../interface/model';
import MessageModel from '../../model/message';
import { CHAT_IGNORE_KEYS } from '../../config';
interface ITextInfo {
messageID: string;
visible: boolean;
}
export default class ChatStore implements IChatStore {
public store: {
messageList: IMessageModel[];
isCompleted: boolean;
nextReqMessageID: string;
quoteMessage: Message | any;
newMessageList: Message[];
typingStatus: boolean;
messageSource: IMessageModel | undefined;
translateTextInfo: Map<string, ITextInfo[]> | undefined;
voiceToTextInfo: Map<string, ITextInfo[]> | undefined;
userInfo: Record<string, any>;
[key: string]: any;
};
public defaultStore = {
messageList: [],
isCompleted: false,
nextReqMessageID: '',
quoteMessage: {},
newMessageList: [],
typingStatus: false,
messageSource: undefined,
translateTextInfo: undefined,
voiceToTextInfo: undefined,
userInfo: {},
} as any;
constructor() {
this.store = {
messageList: [],
isCompleted: false,
nextReqMessageID: '',
quoteMessage: {},
newMessageList: [],
typingStatus: false,
messageSource: undefined,
translateTextInfo: undefined,
voiceToTextInfo: undefined,
userInfo: {},
};
}
update(key: string, data: any): void {
switch (key) {
case 'messageList':
this.updateMessageList(data);
break;
case 'translateTextInfo':
this.updateTranslateTextInfo(data);
break;
case 'voiceToTextInfo':
this.updateVoiceToTextInfo(data);
break;
default:
this.store[key] = data;
}
}
getData(key: string) {
return this.store[key];
}
getModel(messageID: string) {
return this.store.messageList.find((message: IMessageModel) => message.ID === messageID);
}
reset(keyList: string[] = []) {
const resetkeyList = keyList.filter((key: string) => !CHAT_IGNORE_KEYS.includes(key));
this.store = {
...this.defaultStore,
...this.store,
...resetkeyList?.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key] }), {}),
};
}
private updateMessageList(list: any[]) {
const toBeUpdatedList: IMessageModel[] = [];
list.forEach((item) => {
let messageModel: Message | IMessageModel | undefined = item;
if (!(item instanceof MessageModel)) {
messageModel = this.getModel(item.ID);
if (messageModel) {
(messageModel as MessageModel).updateProperties(item);
} else {
messageModel = new MessageModel(item);
}
}
toBeUpdatedList.push(messageModel as IMessageModel);
});
this.store.messageList = toBeUpdatedList;
}
private updateTranslateTextInfo(data: Record<string, any>) {
this.updateBykey('translateTextInfo', data);
}
private updateVoiceToTextInfo(data: Record<string, any>) {
this.updateBykey('voiceToTextInfo', data);
}
private updateBykey(key: string, data: Record<string, any>) {
const { conversationID, messageID, visible = false } = data;
if (!this.store[key]) {
this.store[key] = new Map();
}
if (!this.store[key].has(conversationID)) {
this.store[key].set(conversationID, []);
}
const list = this.store[key].get(conversationID) || [];
let isAdded = true;
for (let i = 0; i < list.length; i++) {
if (list[i].messageID === messageID) {
list[i].visible = visible;
isAdded = false;
break;
}
}
if (isAdded) {
list.push({ messageID, visible });
}
this.store[key].set(conversationID, list);
}
}
@@ -0,0 +1,88 @@
import type { Conversation } from '@tencentcloud/lite-chat/basic';
import type { IConversationStore } from '../../interface/store';
import type { IConversationModel } from '../../interface/model';
import ConversationModel from '../../model/conversation';
export default class ConversationStore implements IConversationStore {
public store: {
currentConversationID: string;
currentConversation: IConversationModel | null;
totalUnreadCount: number;
conversationList: IConversationModel[];
operationTypeMap: Map<string, number>; // 记录退群、群解散、被踢的群
[key: string]: any;
};
public defaultStore = {
currentConversationID: '',
totalUnreadCount: 0,
conversationList: [],
currentConversation: null,
operationTypeMap: new Map(),
} as any;
constructor() {
this.store = {
currentConversationID: '',
totalUnreadCount: 0,
conversationList: [],
currentConversation: null,
operationTypeMap: new Map(),
};
}
update(key: string, data: any) {
switch (key) {
case 'conversationList':
this.updateConversationList(data);
break;
case 'operationTypeMap':
this.updateOperationTypeMap(data);
break;
default:
this.store[key] = data;
}
}
getData(key: string) {
return this.store[key];
}
getModel(conversationID: string) {
return this.store.conversationList.find((conversation: IConversationModel) => conversation.conversationID === conversationID);
}
reset(keyList: string[] = []) {
this.store = {
...this.defaultStore,
...this.store,
...keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key] }), {}),
};
}
private updateConversationList(list: Conversation[] | ConversationModel[]) {
const toBeUpdatedList: any[] = [];
list.forEach((item: Conversation | ConversationModel) => {
let conversationModel: Conversation | IConversationModel | undefined = item;
if (!(item instanceof ConversationModel)) {
conversationModel = new ConversationModel(item);
} else {
(conversationModel as ConversationModel).updateProperties(item);
}
const optType = this.getOperationType(item);
(conversationModel as ConversationModel).updateOperationType(optType);
toBeUpdatedList.push(conversationModel);
});
this.store.conversationList = toBeUpdatedList;
}
private updateOperationTypeMap(data: Record<string, any>) {
const { conversationID, operationType = 0 } = data;
this.store.operationTypeMap.set(conversationID, operationType);
}
private getOperationType(conversation: any) {
const { conversationID } = conversation;
return this.store.operationTypeMap.get(conversationID) || 0;
}
}
@@ -0,0 +1,27 @@
import type { ICustomStore } from '../../interface/store';
export default class CustomStore implements ICustomStore {
public store: any;
constructor() {
this.store = {};
}
update(key: string, data: any) {
this.store[key] = data;
}
getData(key: string) {
return this.store[key];
}
reset(keyList: string[] = []) {
if (keyList.length === 0) {
this.store = {};
}
this.store = {
...this.store,
...keyList.reduce((acc, key) => ({ ...acc, [key]: undefined }), {}),
};
}
}
@@ -0,0 +1,89 @@
import type { Group, Message } from '@tencentcloud/lite-chat/basic';
import type { GroupMember, IGroupStore } from '../../interface/store';
import type { IGroupModel } from '../../interface/model';
import GroupModel from '../../model/group';
export default class GroupStore implements IGroupStore {
public store: {
currentGroupID: string;
currentGroup: IGroupModel | undefined;
currentGroupAttributes: Record<string, any>;
currentGroupCounters: Record<string, number>;
currentGroupMemberList: GroupMember[];
groupList: IGroupModel[];
groupSystemNoticeList: Message[]; // 下发给个人的群系统通知
isCompleted: boolean;
offset: number;
[key: string]: any;
};
public defaultStore = {
currentGroupID: '',
currentGroup: {},
currentGroupAttributes: {},
currentGroupCounters: {},
currentGroupMemberList: [],
groupList: [],
groupSystemNoticeList: [],
isCompleted: false,
offset: 0,
} as any;
constructor() {
this.store = {
currentGroupID: '',
currentGroup: undefined,
currentGroupAttributes: {},
currentGroupCounters: {},
currentGroupMemberList: [],
groupList: [],
groupSystemNoticeList: [],
isCompleted: false,
offset: 0,
};
}
update(key: string, data: any) {
switch (key) {
case 'groupList':
this.updateGroupList(data);
break;
case 'currentGroup':
if (data instanceof GroupModel) {
this.store.currentGroup = data;
} else {
this.store.currentGroup = new GroupModel(data);
}
break;
default:
this.store[key] = data;
}
}
getData(key: string) {
if (key === 'groupSystemNoticeList') {
const groupSystemNoticeList = [...this.store.groupSystemNoticeList];
// 群系统通知给 UI 后立即清空
this.store.groupSystemNoticeList.length = 0;
return groupSystemNoticeList;
}
return this.store[key];
}
reset(keyList: string[] = []) {
this.store = {
...this.defaultStore,
...this.store,
...keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key] }), {}),
};
}
private updateGroupList(list: any[]) {
this.store.groupList = list.map((item: Group | GroupModel) => {
if (item instanceof GroupModel) {
return item;
}
return new GroupModel(item);
});
}
}
@@ -0,0 +1,87 @@
import type { IUserStore } from '../../interface/store';
import { UserStatus } from '../../const';
interface IBlackUser {
userID: string;
nick: string;
avatar: string;
}
export default class UserStore implements IUserStore {
public store: {
userProfile: object;
displayOnlineStatus: boolean;
displayMessageReadReceipt: boolean;
userStatusList: Map<string, {
statusType: number;
customStatus: string;
}>;
kickedOut: string;
netStateChange: string;
userBlacklist: IBlackUser[];
targetLanguage: string;
[key: string]: any;
};
public defaultStore = {
userProfile: {},
displayOnlineStatus: false,
displayMessageReadReceipt: true,
userStatusList: new Map(),
kickedOut: '',
netStateChange: '',
userBlacklist: [],
targetLanguage: 'zh',
} as any;
constructor() {
this.store = {
userProfile: {},
displayOnlineStatus: false, // 默认关闭在线状态功能,防止专业版集成引起报错
displayMessageReadReceipt: true, // 默认在用户纬度开启消息阅读状态功能
userStatusList: new Map(), // 获取/订阅/取消订阅用户状态的 userID 列表
kickedOut: '',
netStateChange: '',
userBlacklist: [],
targetLanguage: 'zh',
};
}
update(key: string, data: any) {
switch (key) {
case 'userStatusList':
this.updateUserStatusList(data);
break;
default:
this.store[key] = data;
}
}
getData(key: string) {
return this.store[key];
}
reset(keyList: string[] = []) {
this.store = {
...this.defaultStore,
...this.store,
...keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key] }), {}),
};
}
private updateUserStatusList(list: any[]) {
if (list.length === 0) {
this.store.userStatusList.clear();
return;
}
list.forEach((item: { userID: string; statusType: number; customStatus: string }) => {
const { userID, statusType = 0, customStatus = '' } = item;
// statusType = -1 表示该用户已被取消订阅,内部约定规则
if (statusType === UserStatus.UNSUB_USER) {
this.store.userStatusList.delete(userID);
} else {
this.store.userStatusList.set(userID, { statusType, customStatus });
}
});
}
}
@@ -0,0 +1,155 @@
import type { ITUIStore, IOptions, Task } from '../interface/store';
import { StoreName } from '../const';
import { notNotifyList } from '../config';
import AppStore from './store/app';
import UserStore from './store/user';
import ConversationStore from './store/conversation';
import ChatStore from './store/chat';
import GroupStore from './store/group';
import CustomStore from './store/custom';
import type { func } from '../type';
export default class TUIStore implements ITUIStore {
static instance: TUIStore;
public task: Task;
private storeMap: Partial<Record<StoreName, any>>;
constructor() {
this.storeMap = {
[StoreName.APP]: new AppStore(),
[StoreName.USER]: new UserStore(),
[StoreName.CONV]: new ConversationStore(),
[StoreName.CHAT]: new ChatStore(),
[StoreName.GRP]: new GroupStore(),
};
// todo 此处后续优化结构后调整
this.task = {} as Task; // 保存监听回调列表
}
/**
* 获取 TUIStore 实例
*/
static getInstance() {
if (!TUIStore.instance) {
TUIStore.instance = new TUIStore();
}
return TUIStore.instance;
}
/**
* UI 组件注册监听回调
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息
*/
watch(storeName: StoreName, options: IOptions) {
if (!this.task[storeName]) {
this.task[storeName] = {};
}
const watcher = this.task[storeName];
Object.keys(options).forEach((key) => {
const callback = options[key];
if (!watcher[key]) {
watcher[key] = new Map();
}
watcher[key].set(callback, 1);
// 注册监听后立即通知一次默认数据,避免UI层初始化调用 getData
this.notifyOnWatch(storeName, key, callback);
});
}
/**
* UI 取消组件监听回调
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息,包含需要取消的回掉等
*/
unwatch(storeName: StoreName, options: IOptions) {
// todo 该接口暂未支持,unwatch掉同一类,如仅传入store注销掉该store下的所有callback,同样options仅传入key注销掉该key下的所有callback
// options的callback function为必填参数,后续修改
if (!this.task[storeName]) {
return;
}
const watcher = this.task[storeName];
Object.keys(options).forEach((key) => {
watcher[key]?.delete(options[key]);
});
}
/**
* 通用 store 数据更新,messageList 的变更需要单独处理
* @param {StoreName} storeName store 名称
* @param {string} key 变更的 key
* @param {any} data 变更的数据
*/
update(storeName: StoreName, key: string, data: any) {
if (storeName === StoreName.CUSTOM && !this.storeMap[storeName]) {
this.storeMap[storeName] = new CustomStore();
}
this.storeMap[storeName]?.update(key, data);
this.notify(storeName, key);
}
/**
* 获取 Store 数据
* @param {StoreName} storeName store 名称
* @param {string} key 待获取的 key
*/
getData(storeName: StoreName, key: string) {
if (storeName === StoreName.CUSTOM && !this.storeMap[storeName]) {
this.storeMap[storeName] = new CustomStore();
}
return this.storeMap[storeName]?.getData(key);
}
getConversationModel(conversationID: string) {
return this.storeMap[StoreName.CONV]?.getModel(conversationID);
}
getMessageModel(messageID: string) {
return this.storeMap[StoreName.CHAT]?.getModel(messageID);
}
reset(storeName: StoreName, keyList: string[] = [], isNotificationNeeded = false) {
if (storeName in this.storeMap) {
const store = this.storeMap[storeName];
if (keyList.length === 0) {
// reset all
keyList = Object.keys(store?.store);
}
store.reset(keyList);
if (isNotificationNeeded) {
keyList.forEach((key) => {
this.notify(storeName, key);
});
}
}
}
// 注册监听时默认给当前 callback 通知一次,notNotifyList 中配置的 key 如果数据为空就不触发 callback
private notifyOnWatch(storeName: StoreName, key: string, callback: func) {
const data = this.getData(storeName, key);
if (notNotifyList.indexOf(key) > -1 && data.length === 0) {
return;
}
callback && callback.call(this, data);
}
/**
* UI 组件注册监听回调
* @param {StoreName} storeName store 名称
* @param {string} key 变更的 key
*/
private notify(storeName: StoreName, key: string) {
if (!this.task[storeName]) {
return;
}
const watcher = this.task[storeName];
if (watcher[key]) {
const callbackMap = watcher[key];
const data = this.getData(storeName, key);
for (const [callback] of callbackMap.entries()) {
callback.call(this, data);
}
}
}
}
@@ -0,0 +1,45 @@
import TUIBase from '../tui-base';
import type { ITUIUserService } from '../interface/service';
import type { UpdateMyProfileParams, UserIDListParams } from '../type';
import UserProfileHandler from './user-profile-handler';
import { StoreName } from '../const';
export default class TUIUserService extends TUIBase implements ITUIUserService {
static instance: TUIUserService;
private userProfileHandler: UserProfileHandler;
constructor() {
super();
this.userProfileHandler = new UserProfileHandler(this);
}
/**
* 获取 TUIUserService 实例
*/
static getInstance() {
if (!TUIUserService.instance) {
TUIUserService.instance = new TUIUserService();
}
return TUIUserService.instance;
}
init() {
const chatEngine = this.getEngine();
// 监听用户被踢通知
chatEngine.eventCenter.addEvent(chatEngine.EVENT.KICKED_OUT, this.onKickedOut.bind(this));
this.userProfileHandler.init();
}
private onKickedOut(data: any) {
this.getEngine().TUIStore.update(StoreName.USER, 'kickedOut', data.type);
}
// 用户资料相关的方法
public getUserProfile(userIDList?: UserIDListParams) {
return this.userProfileHandler.getUserProfile(userIDList);
}
public updateMyProfile(options: UpdateMyProfileParams) {
return this.userProfileHandler.updateMyProfile(options);
}
}
@@ -0,0 +1,61 @@
import type { Profile } from '@tencentcloud/lite-chat/basic';
import type { UpdateMyProfileParams, UserIDListParams } from '../type';
import type { ITUIUserService } from '../interface/service';
import { StoreName } from '../const';
import { isUndefined } from '../utils/common-utils';
export default class UserProfileHandler {
private TUIUserService: ITUIUserService;
constructor(TUIUserService: ITUIUserService) {
this.TUIUserService = TUIUserService;
}
private getEngine() {
return this.TUIUserService.getEngine();
}
init() {
this.getEngine().eventCenter.addEvent(this.getEngine().EVENT.PROFILE_UPDATED, this.onProfileUpdated.bind(this));
this.getUserProfileInitData();
}
private onProfileUpdated(userProfileList: Profile[]) {
const chatEngine = this.getEngine();
const myProfile = chatEngine.TUIStore.getData(StoreName.USER, 'userProfile');
userProfileList.forEach((profile: Profile) => {
// 自己资料的更新通知需要更新 store 并通知给 UI,好友资料更新通知先不处理
if (profile.userID === myProfile.userID) {
chatEngine.TUIStore.update(StoreName.USER, 'userProfile', profile);
}
});
}
private getUserProfileInitData() {
const chatEngine = this.getEngine();
if (!chatEngine.chat.isReady()) {
return;
}
// chatEngine.chat.getBlacklist().then((imResponse: any) => {
// const blockList = imResponse.data || [];
// console.log(`TUIUserProfileHandler.init, getBlacklist count:${blockList.length}`);
// if (blockList.length > 0) {
// this.onBlacklistUpdated(blockList);
// }
// });
}
public getUserProfile(options?: UserIDListParams) {
const chatEngine = this.getEngine();
if (isUndefined(options)) {
return chatEngine.chat.getMyProfile().then((imResponse: any) => {
chatEngine.TUIStore.update(StoreName.USER, 'userProfile', imResponse.data);
return imResponse;
}).catch((error: any) => Promise.reject(error));
}
return chatEngine.chat.getUserProfile(options as UserIDListParams);
}
public updateMyProfile(options: UpdateMyProfileParams) {
return this.getEngine().chat.updateMyProfile(options);
}
}
@@ -0,0 +1,5 @@
export const AVATAR = {
C2C: 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_16.png',
GROUP: 'https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.png',
SYSTEM: 'https://web.sdk.qcloud.com/component/TUIKit/assets/group_avatar.png',
};
@@ -0,0 +1,18 @@
// 商业化能力位文档:https://iwiki.woa.com/pages/viewpage.action?pageId=717923757
interface ICommercialAbility {
enabledMessageReadReceipt: number;
enabledEmojiPlugin: number;
enabledOnlineStatus: number;
enabledCustomerServicePlugin: number;
enabledTranslationPlugin: number;
enabledVoiceToText: number;
[key: string]: number;
}
export const commercialAbility: ICommercialAbility = {
enabledMessageReadReceipt: Math.pow(2, 5),
enabledEmojiPlugin: Math.pow(2, 48),
enabledOnlineStatus: Math.pow(2, 7),
enabledCustomerServicePlugin: Math.pow(2, 40),
enabledTranslationPlugin: Math.pow(2, 38),
enabledVoiceToText: Math.pow(2, 39),
};
@@ -0,0 +1,4 @@
export const CHAT_IGNORE_KEYS = [
'translateTextInfo',
'voiceToTextInfo',
];
@@ -0,0 +1,4 @@
export * from './avatar';
export * from './commercial-ability';
export * from './not-notify-list';
export * from './ignore-store-key';
@@ -0,0 +1,6 @@
// 注册监听时如果列表数据为空不触发 callback
export const notNotifyList = [
'messageList',
'conversationList',
'newMessageList',
];
@@ -0,0 +1,14 @@
export enum ERROR_CODE {
MSG_MODIFY_CONFLICT = 2480,
MSG_MODIFY_DISABLED_IN_AVCHATROOM = 2481,
MODIFY_MESSAGE_NOT_EXIST = 20026,
}
export enum ERROR_CODE_ENGINE {
NOT_INIT = -100000,
INVALID_CONV_ID = -100001,
CONV_ID_SAME = -100002,
CONV_NOT_EXIST = -100003,
GET_MSG_LIST_ERROR = -100004,
MISMATCH_TYPE_AND_PAYLOAD = -100005,
}
@@ -0,0 +1,14 @@
export enum ERROR_MSG {
MSG_MODIFY_CONFLICT = 'MODIFY_MESSAGE_ERROR,修改消息发生冲突, data.message 是最新的消息',
MSG_MODIFY_DISABLED_IN_AVCHATROOM = 'MODIFY_MESSAGE_ERROR,不支持修改直播群消息.',
MODIFY_MESSAGE_NOT_EXIST = 'MODIFY_MESSAGE_ERROR,消息不存在.',
}
export enum ERROR_MSG_ENGINE {
NOT_INIT = 'TUIChatEngine 初始化未完成,请确认 TUIChatEngine.login 接口调用是否正常。',
INVALID_CONV_ID = '会话 ID 无效',
CONV_ID_SAME = '您切换的是同一个会话 ID',
CONV_NOT_EXIST = '会话不存在',
GET_MSG_LIST_ERROR = 'Chat SDK is not ready.',
MISMATCH_TYPE_AND_PAYLOAD = 'type 与 payload 不匹配.',
}
@@ -0,0 +1,7 @@
export * from './mounted-list';
export * from './store-name';
export * from './user-status';
export * from './operate-type';
export * from './error-code';
export * from './error-msg';
export * from './validate-api-list';
@@ -0,0 +1,9 @@
// ChatEngine 上挂载的业务模块列表
export enum MountedList {
TUIStore = 'TUIStore',
TUIConversation = 'TUIConversation',
TUIChat = 'TUIChat',
TUIGroup = 'TUIGroup',
TUIUser = 'TUIUser',
TUIReport = 'TUIReport',
}
@@ -0,0 +1,4 @@
export enum operateType {
ADD = 'add',
REMOVE = 'remove',
}
@@ -0,0 +1,6 @@
const OPTIONAL_MODULE = {
GRP: 'group-module',
SNS: 'relationship-module',
};
export default OPTIONAL_MODULE;
@@ -0,0 +1,20 @@
/**
* @property {String} APP 应用级别的数据管理,主要用于某些功能的全局开关控制。
* @property {String} CONV 会话数据管理
* @property {String} CHAT 聊天数据管理
* @property {String} GRP 群组数据管理
* @property {String} USER 用户数据管理
* @property {String} FRIEND 好友数据管理
* @property {String} SEARCH 搜索数据管理
* @property {String} CUSTOM 自定义数据管理,业务侧可根据需要添加自定义 key-value。
*/
export enum StoreName {
APP = 'application',
CONV = 'conversation',
CHAT = 'chat',
GRP = 'group',
USER = 'user',
FRIEND = 'friend',
SEARCH = 'search',
CUSTOM = 'custom',
}
@@ -0,0 +1,3 @@
export enum UserStatus {
UNSUB_USER = -1, // 用于取消订阅时删除 userStore 中 map 缓存
}
@@ -0,0 +1,95 @@
// 需要校验的 API
export const ValidateAPIList = {
// TUIChatEngine
ENGINE: {
logout: 1,
destroy: 1,
},
CONV: {
// ConversationModel
// deleteConversation: 1,
// pinConversation: 1,
// muteConversation: 1,
// TUIConversationService
switchConversation: 1,
getConversationProfile: 1,
clearHistoryMessage: 1,
setMessageRead: 1,
},
CHAT: {
// MessageModel
modifyMessage: 1,
revokeMessage: 1,
resendMessage: 1,
deleteMessage: 1,
quoteMessage: 1,
replyMessage: 1,
setMessageExtensions: 1,
deleteMessageExtensions: 1,
getMessageExtensions: 1,
// TUIChatService
sendTextMessage: 1,
sendTextAtMessage: 1,
sendImageMessage: 1,
sendAudioMessage: 1,
sendVideoMessage: 1,
sendFileMessage: 1,
sendCustomMessage: 1,
sendFaceMessage: 1,
sendLocationMessage: 1,
sendForwardMessage: 1,
enterTypingState: 1,
leaveTypingState: 1,
sendMessageReadReceipt: 1,
getGroupMessageReadMemberList: 1,
getMessageList: 1,
downloadMergedMessages: 1,
setTranslationLanguage: 1,
translateText: 1,
searchCloudMessages: 1,
addMessageReaction: 1,
removeMessageReaction: 1,
getMessageReactions: 1,
getAllUserListOfMessageReaction: 1,
},
GRP: {
// TUIGroupService
switchGroup: 1,
getGroupProfile: 1,
updateGroupProfile: 1,
createGroup: 1,
dismissGroup: 1,
searchGroupByID: 1,
joinGroup: 1,
quitGroup: 1,
getGroupApplicationList: 1,
handleGroupApplication: 1,
getGroupOnlineMemberCount: 1,
changeGroupOwner: 1,
initGroupAttributes: 1,
setGroupAttributes: 1,
deleteGroupAttributes: 1,
getGroupAttributes: 1,
setGroupCounters: 1,
increaseGroupCounter: 1,
decreaseGroupCounter: 1,
getGroupCounters: 1,
getGroupMemberList: 1,
getGroupMemberProfile: 1,
addGroupMember: 1,
deleteGroupMember: 1,
setGroupMemberMuteTime: 1,
setGroupMemberRole: 1,
setGroupMemberNameCard: 1,
setGroupMemberCustomField: 1,
markGroupMemberList: 1,
},
USER: {
// TUIUserService
switchUserStatus: 1,
getUserProfile: 1,
updateMyProfile: 1,
},
};
@@ -0,0 +1,66 @@
import type { ITUIChatEngine } from './interface/engine';
import type { ITUIGlobal } from './interface/global';
import type { ITUIStore } from './interface/store';
import type {
ITUIConversationService,
ITUIUserService,
ITUIChatService,
ITUIGroupService,
ITUIReportService,
} from './interface/service';
import ChatEngine from './TUIEngine/engine';
import TUIGlobal from './TUIGlobal/tui-global';
import TUIStore from './TUIStore/tui-store';
import TUIConversationService from './TUIConversationService/tui-conversation';
import TUIGroupService from './TUIGroupService/tui-group';
import TUIUserService from './TUIUserService/tui-user';
import TUIChatService from './TUIChatService/tui-chat';
import TUIReportService from './TUIReportService/tui-report';
import { MountedList, StoreName, ValidateAPIList } from './const';
import validateInitialization from './utils/validate-initialization';
const version = 'BUNDLE_VERSION';
console.log(`TUIChatEngine-Lite.VERSION:${version}`);
// 实例化
const TUIChatEngine: ITUIChatEngine = ChatEngine.getInstance();
const tuiGlobal: ITUIGlobal = TUIGlobal.getInstance();
const tuiStore: ITUIStore = TUIStore.getInstance();
const tuiConversation: ITUIConversationService = TUIConversationService.getInstance();
const tuiGroup: ITUIGroupService = TUIGroupService.getInstance();
const tuiUser: ITUIUserService = TUIUserService.getInstance();
const tuiChat: ITUIChatService = TUIChatService.getInstance();
const tuiReport: ITUIReportService = TUIReportService.getInstance();
// 模块挂载
TUIChatEngine.mount(MountedList.TUIStore, tuiStore);
TUIChatEngine.mount(MountedList.TUIConversation, tuiConversation);
TUIChatEngine.mount(MountedList.TUIUser, tuiUser);
TUIChatEngine.mount(MountedList.TUIChat, tuiChat);
TUIChatEngine.mount(MountedList.TUIReport, tuiReport);
// API 调用校验 Engine 初始化状态
validateInitialization(TUIChatEngine, TUIChatEngine, ValidateAPIList.ENGINE);
validateInitialization(TUIChatEngine, tuiConversation, ValidateAPIList.CONV);
validateInitialization(TUIChatEngine, tuiChat, ValidateAPIList.CHAT);
validateInitialization(TUIChatEngine, tuiUser, ValidateAPIList.USER);
validateInitialization(TUIChatEngine, tuiReport, ValidateAPIList.ENGINE);
// 输出 service 方法参数和 model 声明文件
export * from './type';
export * from './interface/model';
// 输出产物
export {
TUIChatEngine,
tuiGlobal as TUIGlobal,
tuiStore as TUIStore,
tuiConversation as TUIConversationService,
tuiGroup as TUIGroupService,
tuiUser as TUIUserService,
tuiChat as TUIChatService,
tuiReport as TUIReportService,
StoreName,
};
export default TUIChatEngine;
@@ -0,0 +1,102 @@
import type { ChatSDK } from '@tencentcloud/lite-chat/basic';
import type { LoginParams } from '../../type';
import type { IEventCenter } from './event-center';
import type {
ITUIConversationService,
ITUIChatService,
ITUIGroupService,
ITUIUserService,
} from '../service';
import type { ITUIStore } from '../store';
/**
* @interface ITUIChatEngine
* @property {Object} EVENT {@link https://web.sdk.qcloud.com/im/doc/v3/zh-cn/module-EVENT.html Chat SDK 定义的事件列表 }
* @property {Object} TYPES {@link https://web.sdk.qcloud.com/im/doc/v3/zh-cn/module-TYPES.html Chat SDK 定义的类型常量}
*/
export interface ITUIChatEngine {
isInited: boolean;
chat: ChatSDK;
EVENT: any;
TYPES: any;
eventCenter: IEventCenter;
TUIStore: ITUIStore;
TUIConversation: ITUIConversationService;
TUIChat: ITUIChatService;
TUIGroup: ITUIGroupService;
TUIUser: ITUIUserService;
/**
* 创建 Chat SDK 实例 & 登录 Chat SDK
* @function
* @param {LoginParams} options 登录参数
* @example
* let promise = TUIChatEngine.login({
* SDKAppID: xxx,
* userID: 'xxx',
* userSig: 'xxx',
* useUploadPlugin: true, // 使用文件上传插件
* });
* promise.then(() => {
* // 登录成功后进行相关业务逻辑处理
* })
*/
login(options: LoginParams): Promise<any>;
/**
* 登出 Chat SDK
* @function
* @example
* let promise = TUIChatEngine.logout();
* promise.then(() => {
* // 登出成功后进行相关业务逻辑处理
* })
*/
logout(): Promise<any>;
/**
* Chat SDK 是否 ready。SDK ready 后,开发者可调用 SDK 发送消息等 API,使用 SDK 的各项功能。
* @function
* @example
* let isReady = TUIChatEngine.isReady();
*/
isReady(): boolean;
/**
* 销毁 Chat SDK 实例,SDK 会先 logout,然后断开 WebSocket 长连接,并释放资源。
* @function
* @example
* let promise = TUIChatEngine.destroy();
*/
destroy(): Promise<any>;
/**
* 设置 SDK 日志级别
* @function
* @param {number} level 日志级别
* - 0 普通级别,日志量较多,接入时建议使用
* - 1 release级别,SDK 输出关键信息,生产环境时建议使用
* - 2 告警级别,SDK 只输出告警和错误级别的日志
* - 3 错误级别,SDK 只输出错误级别的日志
* - 4 无日志级别,SDK 将不打印任何日志
* @example
* TUIChatEngine.setLogLevel(0)
*/
setLogLevel(level: number): void;
/**
* 模块挂载
* @function
* @param {string} name 模块名
* @param {any} instance 类的实例
* @private
*/
mount(name: string, instance: any): void;
/**
* 获取当前用户 ID
* @function
* @private
*/
getMyUserID(): string;
}
@@ -0,0 +1,21 @@
import type { func } from '../../type';
export interface IEventCenter {
/**
* Service 添加监听事件
* @param {string} event 事件名
* @param {Function} callback 回调函数
*/
addEvent(event: string, callback: func): void;
/**
* 移除 chatEngine 内部注册的事件回调
*/
removeEvents(): void;
/**
* 解绑 IM SDK 事件
*/
unbindIMEvents(): void;
}
@@ -0,0 +1,2 @@
export * from './event-center';
export * from './engine';
@@ -0,0 +1,12 @@
/**
* @interface TUIGlobal
*/
export interface ITUIGlobal {
global: any; // 挂在 wx、uni、window 对象
isOfficial: boolean;
[key: string]: any;
initOfficial(isOfficial: boolean): void;
getPlatform(): string;
}
@@ -0,0 +1,137 @@
import type { Conversation } from '@tencentcloud/lite-chat/basic';
/**
* @description ConversationModel 主要负责会话操作和会话数据处理,ConversationModel 不需要开发者进行开发,由 TUIChatEngine 通过 {@link https://web.sdk.qcloud.com/im/doc/chat-engine/global.html#ConversationStore ConversationStore} 的 conversationList 提供给开发者直接使用。
* @interface ConversationModel
* @property {String} conversationID - 会话 ID。会话ID组成方式:<br/>
* - `C2C${userID}`(单聊)
* - `GROUP${groupID}`(群聊)
* @property {String} type - 会话类型,具体如下:<br/>
* | 类型 | 含义 |
* | :--- | :---- |
* | TUIChatEngine.TYPES.CONV_C2C | C2CClient to Client, 端到端)会话 |
* | TUIChatEngine.TYPES.CONV_GROUP | GROUP(群组)会话 |
* | TUIChatEngine.TYPES.CONV_SYSTEM | SYSTEM(系统)会话。该会话只能接收来自系统的通知消息,不能发送消息。 |
* @property {String} subType - 群组会话的群组类型,具体如下:<br/>
* | 类型 | 含义 |
* | :--- | :---- |
* | TUIChatEngine.TYPES.GRP_WORK | 好友工作群 |
* | TUIChatEngine.TYPES.GRP_PUBLIC | 陌生人社交群 |
* | TUIChatEngine.TYPES.GRP_MEETING | 临时会议群 |
* | TUIChatEngine.TYPES.GRP_AVCHATROOM | 直播群 |
* @property {Number} unreadCount - 未读计数。TUIChatEngine.TYPES.GRP_MEETING / TUIChatEngine.TYPES.GRP_AVCHATROOM 类型的群组会话不记录未读计数,该字段值为0
* @property {Object} lastMessage - 会话最新的消息
* @property {String} lastMessage.nick - 群会话最新消息的发送者的昵称,C2C 会话为 ''
* @property {String} lastMessage.nameCard - 群会话最新消息的发送者的群名片,C2C 会话为 ''
* @property {Number} lastMessage.lastTime - 当前会话最新消息的时间戳,单位:秒
* @property {Number} lastMessage.lastSequence - 当前会话的最新消息的 Sequence
* @property {String} lastMessage.fromAccount - 最新消息来源用户的 userID
* @property {Boolean} lastMessage.isRevoked - 会话最新的消息是否已被撤回,true 表示已撤回,默认值为 false
* @property {String|null} lastMessage.revoker - 消息撤回者的 userID
* @property {Boolean} lastMessage.isPeerRead - 对端是否已读 C2C 会话的最新消息,默认值为 false
* @property {String} lastMessage.messageForShow - 最新消息的内容,用于展示。可能值:文本消息内容、"[图片]"、"[语音]"、"[位置]"、"[表情]"、"[文件]"、"[自定义消息]"。<br/>
* 若该字段不满足您的需求,您可以使用 payload 来自定义渲染。
* @property {String} lastMessage.type - 消息类型,具体如下:<br/>
* | 类型 | 含义 |
* | :--- | :---- |
* | TUIChatEngine.TYPES.MSG_TEXT | 文本消息 |
* | TUIChatEngine.TYPES.MSG_IMAGE | 图片消息 |
* | TUIChatEngine.TYPES.MSG_SOUND | 音频消息 |
* | TUIChatEngine.TYPES.MSG_AUDIO | 音频消息 |
* | TUIChatEngine.TYPES.MSG_VIDEO | 视频消息 |
* | TUIChatEngine.TYPES.MSG_FILE | 文件消息 |
* | TUIChatEngine.TYPES.MSG_LOCATION | 地理位置消息 |
* | TUIChatEngine.TYPES.MSG_CUSTOM | 自定义消息 |
* | TUIChatEngine.TYPES.MSG_GRP_TIP | 群提示消息 |
* | TUIChatEngine.TYPES.MSG_GRP_SYS_NOTICE | 群系统通知消息 |
* @property {Object} lastMessage.payload - 消息的内容,收到的音频 / 文件消息的 payload 中没有 url 字段。
* @property {Group} groupProfile - 群会话的群组资料
* @property {Profile} userProfile - C2C会话的用户资料
* @property {GroupAtInfo[]} groupAtInfoList - 群会话的 at 信息列表,接入侧可根据此信息在会话列表展示【有人@我】【@所有人】等效果。
* @property {String} remark - 好友备注,只有C2C会话且对端是我的好友,且我设置过此好友的备注才有值
* @property {Boolean} isPinned - 会话是否置顶
* @property {String} messageRemindType - 消息提醒类型,具体如下:<br/>
* - TUIChatEngine.TYPES.MSG_REMIND_ACPT_AND_NOTE 在线正常接收消息,离线时会有厂商的离线推送通知(Web 和小程序端无离线推送)
* - TUIChatEngine.TYPES.MSG_REMIND_DISCARD 在线和离线都拒收消息
* - TUIChatEngine.TYPES.MSG_REMIND_ACPT_NOT_NOTE 在线正常接收消息,离线不会有推送通知(消息免打扰)
* @property {Array} markList - 会话标记列表,具体如下:<br/>
* - TUIChatEngine.TYPES.CONV_MARK_TYPE_STAR 会话标星
* - TUIChatEngine.TYPES.CONV_MARK_TYPE_UNREAD 会话标记未读(重要会话)
* - TUIChatEngine.TYPES.CONV_MARK_TYPE_FOLD 会话折叠
* - TUIChatEngine.TYPES.CONV_MARK_TYPE_HIDE 会话隐藏
* @property {String} customData - 会话自定义数据
* @property {Array} conversationGroupList - 会话所属分组列表
* @property {String} draftText - 会话草稿
* @property {Boolean} isMuted 会话是否已设置免打扰,默认为 false
* @property {Number} operationType 群组操作类型, 默认 0。4-被踢出群组 5-群组被解散 8-主动退群
*/
export interface IConversationModel {
conversationID: string;
type: string;
subType: string;
unreadCount: number;
lastMessage?: {
nick: string;
nameCard: string;
lastTime: number | string;
lastSequence: string;
fromAccount: string;
isRevoked: boolean;
revoker?: string;
isPeerRead: boolean;
messageForShow: string;
type: string;
payload: any;
};
groupProfile?: any;
userProfile?: any;
groupAtInfoList?: any[];
remark: string;
isPinned: boolean;
messageRemindType: string;
markList: string[];
customData: string;
conversationGroupList: any[];
draftText: string;
isMuted: boolean;
operationType: number; // 群组会话的操作类型
_conversation: any; // 保存原始 conversation 实例数据
/**
* 更新 conversationModel 属性
* @function
* @private
*/
updateProperties(options: Conversation): void;
/**
* 更新 operationType 属性
* @function
* @private
*/
updateOperationType(operationType: number): void;
/**
* 获取 IM SDK 提供的 conversation 对象
* @function
* @private
*/
getConversation(): any;
// 以下方法用于处理 conversation 需要展示的数据
/**
* 获取会话头像
* @function
* @example
* let avatar = conversationModel.getAvatar();
*/
getAvatar(): string;
/**
* 获取会话名称
* @function
* @example
* let name = conversationModel.getShowName();
*/
getShowName(): string;
}
@@ -0,0 +1,39 @@
import type TencentCloudChat from '@tencentcloud/lite-chat/basic';
/**
* @description GroupModel 的属性与 IM SDK Group 属性字段保持一致,详情可参考:{@link https://web.sdk.qcloud.com/im/doc/zh-cn/Group.html Group}。
* @interface GroupModel
*/
export interface IGroupModel {
groupID: string;
name: string;
avatar: string;
type: TencentCloudChat.TYPES.GRP_WORK
| TencentCloudChat.TYPES.GRP_PUBLIC
| TencentCloudChat.TYPES.GRP_MEETING
| TencentCloudChat.TYPES.GRP_AVCHATROOM
| TencentCloudChat.TYPES.GRP_COMMUNITY;
introduction: string;
notification: string;
ownerID: string;
createTime: number;
infoSequence?: number;
lastInfoTime?: number;
selfInfo?: {
role?: string;
messageRemindType?: string;
joinTime?: number;
nameCard?: string;
userID?: string;
memberCustomField?: any[];
};
lastMessage?: any;
nextMessageSeq: number;
memberCount: number;
maxMemberCount: number;
muteAllMembers: boolean;
joinOption: string;
groupCustomField?: any[];
isSupportTopic: boolean;
groupAttributes: any;
groupCounters: any;
}
@@ -0,0 +1,3 @@
export * from './conversation';
export * from './group';
export * from './message';
@@ -0,0 +1,326 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type TencentCloudChat from '@tencentcloud/lite-chat/basic';
import type { ModifyMessageParams, ReactionInfo } from '../../type';
/**
* @description MessageModel 主要负责消息操作和消息上屏数据处理,MessageModel 不需要开发者进行开发,由 TUIChatEngine 通过 {@link https://web.sdk.qcloud.com/im/doc/chat-engine/global.html#ChatStore ChatStore} 的 messageList 提供给开发者直接使用。<br/>
* @interface IMessageModel
* @property {String} ID - 消息 ID
* @property {String} type - 消息类型,具体如下:<br/>
* | 类型 | 含义 |
* | :--- | :---- |
* | TUIChatEngine.TYPES.MSG_TEXT | 文本消息 |
* | TUIChatEngine.TYPES.MSG_IMAGE | 图片消息 |
* | TUIChatEngine.TYPES.MSG_SOUND | 音频消息 |
* | TUIChatEngine.TYPES.MSG_AUDIO | 音频消息 |
* | TUIChatEngine.TYPES.MSG_VIDEO | 视频消息 |
* | TUIChatEngine.TYPES.MSG_FILE | 文件消息 |
* | TUIChatEngine.TYPES.MSG_CUSTOM | 自定义消息 |
* | TUIChatEngine.TYPES.MSG_MERGER | 合并消息 |
* | TUIChatEngine.TYPES.MSG_LOCATION | 位置消息 |
* | TUIChatEngine.TYPES.MSG_FACE | 表情消息 |
* | TUIChatEngine.TYPES.MSG_GRP_TIP | 群提示消息 |
* | TUIChatEngine.TYPES.MSG_GRP_SYS_NOTICE | 群系统通知消息 |
* @property {Object} payload - 消息的内容,详细内容请查看: [Message](https://web.sdk.qcloud.com/im/doc/v3/zh-cn/Message.html)
* @property {String} conversationID - 消息所属的会话 ID
* @property {String} conversationType - 消息所属会话的类型,具体如下:<br/>
* | 类型 | 含义 |
* | :--- | :---- |
* | TUIChatEngine.TYPES.CONV_C2C | C2C(Client to Client, 端到端) 会话 |
* | TUIChatEngine.TYPES.CONV_GROUP | GROUP(群组) 会话 |
* | TUIChatEngine.TYPES.CONV_SYSTEM | SYSTEM(系统) 会话 |
* @property {String} to - 接收方的 userID
* @property {String} from - 发送方的 userID,在消息发送时,会默认设置为当前登录的用户
* @property {String} flow - 消息的流向<br/>
* - in 为收到的消息
* - out 为发出的消息
* @property {Number} time - 消息时间戳。单位:秒
* @property {String} status - 消息状态
* - unSend(未发送)
* - success(发送成功)
* - fail(发送失败)
* @property {Boolean} isRevoked =false - 是否被撤回的消息,true 标识被撤回的消息
* @property {String} priority=TUIChatEngineTYPES.MSG_PRIORITY_NORMAL - 消息优先级,用于群聊
* @property {String} nick='' 消息发送者的昵称(在 AVChatRoom 内支持,需提前调用 [TUIUserService.updateMyProfile](https://web.sdk.qcloud.com/im/doc/chat-engine/ITUIUserService.html#updateMyProfile) 设置)
* @property {String} avatar='' 消息发送者的头像地址(在 AVChatRoom 内支持,需提前调用 [TUIUserService.updateMyProfile](https://web.sdk.qcloud.com/im/doc/chat-engine/ITUIUserService.html#updateMyProfile) 设置)
* @property {Boolean} isPeerRead=false - C2C 消息对端是否已读,true 标识对端已读
* @property {String} nameCard='' 非直播群消息发送者的群名片(也可称之为消息发送者的群昵称),需提前调用 [TUIGroupService.setGroupMemberNameCard](https://web.sdk.qcloud.com/im/doc/chat-engine/ITUIGroupService.html#setGroupMemberNameCard) 设置
* @property {Array} atUserList 群聊时此字段存储被 at 的群成员的 userID
* @property {String} cloudCustomData='' - 消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到)
* @property {Boolean} isDeleted=false - 是否被删除的消息,true 标识被删除的消息
* @property {Boolean} isModified=false - 是否被修改过,true 标识被修改过的消息
* @property {Boolean} needReadReceipt=false - 是否需要已读回执,true 标识需要(需要您购买旗舰版套餐)
* @property {Object} readReceiptInfo={readCount,unreadCount,isPeerRead} - 消息已读回执信息
* @property {Number|undefined} readReceiptInfo.readCount - 群消息已读数<br/>
* 如果想要查询哪些群成员已读了消息,可调用 [TUIChatService.getGroupMessageReadMemberList](https://web.sdk.qcloud.com/im/doc/chat-engine/ITUIChatService.html#getGroupMessageReadMemberList)
* @property {Number|undefined} readReceiptInfo.unreadCount - 群消息未读数
* @property {Boolean|undefined} readReceiptInfo.isPeerRead - C2C消息对端是否已发送已读回执,消息发送方收到已读回执通知或拉漫游时会更新此属性
* @property {Boolean} isBroadcastMessage=false - 对所有直播群广播消息,true 标识直播群广播消息(需要您购买旗舰版套餐)
* @property {Boolean} isSupportExtension=false - 是否支持消息扩展,true 支持 false 不支持(需要您购买旗舰版套餐)
* @property {String|null} revoker - 消息撤回者的 userID
* @property {Number} progress - 图片、视频、语音、文件消息上传进度, 默认 0
* @property {Object} revokerInfo - 消息撤回者的信息。
* @property {String} revokerInfo.avatar - 消息撤回者的头像
* @property {String} revokerInfo.nick - 消息撤回者的昵称
* @property {String} revokerInfo.userID - 消息撤回者的 userID
* @property {String} revokeReason - 消息撤回的原因。
* @property {Boolean} hasRiskContent - 语音、视频消息是否被标记为有安全风险的消息,默认为 false。
* - 只有在开通【云端审核】功能后才生效,【云端审核】开通流程请参考 [云端审核功能](https://cloud.tencent.com/document/product/269/83795#.E4.BA.91.E7.AB.AF.E5.AE.A1.E6.A0.B8.E5.8A.9F.E8.83.BD)。
* @property {Array<ReactionInfo>} reactionList - 需要您购买旗舰版才支持此功能,购买旗舰版能力并重新登录后,拉漫游时会自动获取表情回复摘要信息。
*/
export interface IMessageModel {
ID: string;
type: TencentCloudChat.TYPES;
payload: any;
conversationID: string;
conversationType: TencentCloudChat.TYPES;
to: string;
from: string;
flow: string;
time: number;
status: string;
isRevoked: boolean;
priority: TencentCloudChat.TYPES;
nick: string;
avatar: string;
isPeerRead: boolean;
nameCard: string;
atUserList: string[];
cloudCustomData: string;
isDeleted: boolean;
isModified: boolean;
needReadReceipt: boolean;
readReceiptInfo: any;
isBroadcastMessage: boolean;
isSupportExtension: boolean;
receiverList?: string[];
revoker: string;
sequence: number;
progress: number;
revokerInfo: { userID: string; nick: string; avatar: string };
revokeReason: string;
hasRiskContent: boolean;
reactionList: ReactionInfo[];
/**
* 更新 messageModel 属性
* @function
* @private
*/
updateProperties(options: Message): void;
/**
* 获取 Chat SDK 提供的 message 实例
* @function
* @private
* @example
* let message = messageModel.getMessage();
*/
getMessage(): Message;
/**
* 修改消息
*
* @param {ModifyMessageParams} options 修改的内容
* @function
* @example
* let promise = messageModel.modifyMessage(options);
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
modifyMessage(options: ModifyMessageParams): Promise<any>;
/**
* 撤回单聊消息或者群聊消息。撤回成功后,消息对象的 isRevoked 属性值为 true。
* @function
* @example
* let promise = messageModel.revokeMessage();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
revokeMessage(): Promise<any>;
/**
* 重发消息的接口,当消息发送失败时,可调用该接口进行重发
* @function
* @example
* let promise = messageModel.resendMessage();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
resendMessage(): Promise<any>;
/**
* 删除消息的接口。删除成功后,被删除消息的 isDeleted 属性值为 true。
* @function
* @example
* let promise = messageModel.deleteMessage();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
deleteMessage(): Promise<any>;
/**
* 引用当前消息
* @function
* @example
* let message = messageModel.quoteMessage();
*/
quoteMessage(): Message;
/**
* 回复当前消息
* @function
* @example
* let message = messageModel.replyMessage();
*/
replyMessage(): Message;
/**
* 设置消息扩展
* @function
* @param {Array<object>} extensions 扩展信息
* @private
* @example
* let promise = messageModel.setMessageExtensions([
* { key: 'a', value: '1' },
* { key: 'b', value: '2' }
* ]);
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setMessageExtensions(extensions: object[]): Promise<any>;
/**
* 删除消息扩展
* @function
* @param {Array<string>|undefined} keyList 消息扩展 key 列表,不传 keyList 表示删除所有扩展 key
* @private
* @example
* // 删除部分 key
* let promise = messageModel.deleteMessageExtensions(['a', 'b']);
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
* // 删除全部 key
* let promise = messageModel.deleteMessageExtensions();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
deleteMessageExtensions(keyList?: string[]): Promise<any>;
/**
* 获取消息扩展
* @function
* @private
* @example
* let promise = messageModel.getMessageExtensions();
* promise.then((chatResponse) => {
* // 消息扩展信息
* const { extensions } = chatResponse.data;
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getMessageExtensions(): Promise<any>;
/**
* 获取信令信息
* @function
* @example
* let signaling = messageModel.getSignalingInfo();
* // signaling != null 说明是信令消息,可以进行信令相关的业务逻辑处理
* // signaling = null 说明是非信令消息
* if (signaling) {
* console.log(signaling)
* }
*/
getSignalingInfo(): Record<string, any> | null;
/**
* 获取消息展示内容 <br/>
* 注意1:以下解析为默认解析行为,如果需要自定义解析,可以通过 messageModel 结构体内容自行解析。<br/>
* 注意2:该接口不支持群系统消息解析。<br/>
* 注意3:群提示消息不支持返回 showName。<br/>
* 注意4:showName 显示优先级如下:
* - 单聊:remark > nick > userID
* - 群聊:remark > nameCard > nick > userID
* @function
* @example
* // 文本消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.text - 文本消息展示内容
* // result.name - 文本消息名称,值为 text 或 img, 返回 img 时标识是 emoji 表情消息
* // result.type - 文本消息类型,自定义 emoji 表情消息时返回 customUIKit 默认的 emoji 表情和普通文本消息不返回 type 字段(v2.2.0 起支持)
* // result.emojiKey - emoji 表情的 keyv2.2.0 起支持)
* // result.src - emoji 表情消息展示链接,type = custom 时返回空字符串,需要 UIKit 层根据 baseUrl 和 emojiKey 拼接完整链接
* @example
* // 表情消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.name - 表情消息名称
* // retsult.type - 表情消息类型,自定义 face 表情消息时返回 customUIKit 默认 face 表情包 type 返回 ‘’(v2.2.0 起支持)
* // result.url - 表情消息展示链接,type = custom 时返回空字符串,需要 UIKit 层根据 baseUrl 和 name 拼接完整链接
* @example
* // 地理位置消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.lon - 经度
* // result.lat - 纬度
* // result.href - 地图跳转链接
* // result.url - 地图展示链接
* // result.description - 描述信息
* @example
* // 图片消息返回结果,默认返回的是原图信息,如果需要缩略图或大图信息请从 messageModel.payload 中获取
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.url - 图片访问链接
* // result.width - 图片宽度
* // result.width - 图片高度
* @example
* // 语音消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.url - 语音播放链接
* // result.second - 语音时长
* @example
* // 视频消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.url - 视频播放链接
* // result.snapshotUrl - 视频封面图链接
* // result.snapshotWidth - 视频封面图宽度
* // result.snapshotHeight - 视频封面图高度
* @example
* // 文件消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.url - 文件下载链接
* // result.name - 文件名称
* // result.size - 文件大小
* @example
* // 自定义消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // result.custom - 自定义消息展示内容
* // result.businessID - 业务标识信息,创建群组自定义消息的 businessID 是 group_create,其他的自定义消息返回为空字符串
* @example
* // 合并消息返回结果
* const result = messageModel.getMessageContent();
* // result.showName - 消息发送方名称
* // 合并消息返回内容是 message.payload 内容
* @example
* // 群提示消息返回结果
* const result = messageModel.getMessageContent();
* // result.text - 群提示消息展示内容
*/
getMessageContent(): Record<string, any>;
}
@@ -0,0 +1,7 @@
export * from './tui-conversation';
export * from './tui-chat';
export * from './message-handler';
export * from './tui-group';
export * from './tui-user';
export * from './tui-report';
@@ -0,0 +1,46 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
// message handler 因为需要在 MessageModel 中调用,需要保留声明文件
export interface IMessageHandler {
/**
* 处理文本消息
* @param {Message} message 文本消息
*/
handleTextMessage(message: Message): object;
/**
* 处理图片消息
* @param {Message} message 图片消息
*/
handleImageMessage(message: Message): object;
/**
* 处理视频消息
* @param {Message} message 视频消息
*/
handleVideoMessage(message: Message): object;
/**
* 处理自定义消息
* @param {Message} message 自定义消息
*/
handleCustomMessage(message: Message): object;
/**
* 处理群提示消息
* @param {Message} message 群 tips 消息
*/
handleGroupTipsMessage(message: Message): object;
/**
* 处理群系统消息
* @param {Message} message 群系统消息
*/
handleGroupSystemMessage(message: Message): object;
/**
* 处理音视频通话信令
* @param {any} message 会话最后一条消息或漫游消息
*/
handleCallKitSignaling(message: any): string | undefined;
}
@@ -0,0 +1,212 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type TUIBase from '../../tui-base';
import type {
SendMessageParams,
GetMessageListParams,
SendMessageOptions,
GetMessageListHoppingParams,
} from '../../type';
import type { IMessageHandler } from './message-handler';
/**
* @interface TUIChatService
*/
export interface ITUIChatService extends TUIBase {
messageHandler: IMessageHandler;
/**
* 初始化 Service
* @function
* @private
*/
init: () => void;
/**
* 更新 store 里的 messageList
* @param messageList 要进行更新的 messageList
* @param type 更新类型
* @private
*/
updateMessageList(messageList: Message[], type: string): void;
/**
* 发送文本消息
* @function
* @param {SendMessageParams} options 文本消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送普通文本消息
* let promise = TUIChatService.sendTextMessage({
* payload: {
* text: 'Hello world!'
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
* @example
* // 发送普通文本消息并携带消息自定义数据
* let promise = TUIChatService.sendTextMessage({
* payload: {
* text: 'Hello world!'
* },
* cloudCustomData: 'your cloud custom data'
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendTextMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 发送附带 @ 提醒功能的文本消息
* @function
* @param {SendMessageParams} options 附带 @ 提醒功能的文本消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送 @ 消息
* let promise = TUIChatService.sendTextAtMessage({
* payload: {
* text: '@denny @lucy @所有人 今晚聚餐,收到的请回复1',
* atUserList: ['denny', 'lucy', TUIChatEngine.TYPES.MSG_AT_ALL] // 'denny' 'lucy' 都是 userID,而非昵称
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendTextAtMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 发送图片消息
* @function
* @param {SendMessageParams} options 图片消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送图片消息
* let promise = TUIChatService.sendImageMessage({
* payload: {
* file: file
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendImageMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 发送音频消息
* @function
* @param {SendMessageParams} options 音频消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送音频消息
* let promise = TUIChatService.sendAudioMessage({
* payload: {
* file: file
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendAudioMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 发送视频消息
* @function
* @param {SendMessageParams} options 视频消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送视频消息
* let promise = TUIChatService.sendVideoMessage({
* payload: {
* file: file
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendVideoMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 发送自定义消息
* @function
* @param {SendMessageParams} options 自定义消息相关参数
* @param {SendMessageOptions}[sendMessageOptions] 消息发送选项
* @example
* // 发送自定义消息
* let promise = TUIChatService.sendCustomMessage({
* payload: {
* data: 'dice', // 用于标识该消息是骰子类型消息
* description: String(random(1,6)), // 获取骰子点数
* extension: ''
* },
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
sendCustomMessage(
options: SendMessageParams,
sendMessageOptions?: SendMessageOptions,
): Promise<any>;
/**
* 重发消息的接口,当消息发送失败时,可调用该接口进行重发
* @function
* @param {Message} message 需要重发的消息对象
* @private
*/
resendMessage(message: Message): Promise<any>;
/**
* 分页拉取指定会话的消息列表的接口,当用户进入会话首次渲染消息列表或者用户“下拉查看更多消息”时,需调用该接口。
* @function
* @param {GetMessageListParams} [options] 获取漫游消息参数
* @example
* let promise = TUIChatService.getMessageList();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getMessageList(options?: GetMessageListParams): Promise<any>;
/**
* 根据指定的消息 sequence 或 消息时间拉取会话的消息列表
* @function
* @param {GetMessageListHoppingParams} options 消息实例
* @private
*/
getMessageListHopping(options?: GetMessageListHoppingParams): Promise<any>;
/**
* 清空单聊或群聊本地及云端的消息(不删除会话)
* @function
* @param {conversationID} string 会话 ID
* @example
* // 清空群聊 groupID 为 test 的历史消息
* let promise = TUIChatService.clearHistoryMessage('GROUPtest');
* promise.then((chatResponse) => {
* const { result } = chatResponse.data;
* });
*/
clearHistoryMessage(conversationID: string): Promise<any>;
}
@@ -0,0 +1,71 @@
import type { PinConversationParams, MuteConversationParams, SetConversationDraftParams, MarkConversationParams, DeleteConversationParams } from '../../type';
/**
* @interface TUIConversationService
*/
export interface ITUIConversationService {
/**
* 初始化 Service
* @private
*/
init(): void;
/**
* 切换会话
* @function
* @param {String} conversationID 会话 IDconversationID 生成规则:单聊(`C2C${userID}`),群聊(`GROUP${groupID}`)。
* @example
* // 切换到 public001 群会话
* let promise = TUIConversationService.switchConversation('GROUPpublic001');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
switchConversation(conversationID: string): Promise<any>;
/**
* 获取会话列表
* @function
* @private
*/
getConversationList(): Promise<any>;
/**
* 获取会话资料
* 注意:Service API 主要用于跨组件时调用
* @function
* @param {String} conversationID 会话 ID
* @example
* // 获取 public001 群会话资料
* let promise = TUIConversationService.getConversationProfile('GROUPpublic001');
* promise.then((chatResponse) => {
* console.log(chatResponse.data.conversation); // 会话资料
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getConversationProfile(conversationID: string): Promise<any>;
/**
* 设置会话已读上报
* @function
* @param {String} conversationID 会话 ID
* @example
* let promise = TUIConversationService.setMessageRead('GROUPpublic001');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setMessageRead(conversationID: string): Promise<any>;
/**
* 一键清空历史消息
* @function
* @param {String} conversationID 会话 ID
* @example
* @private
*/
clearHistoryMessage(conversationID: string): Promise<any>;
}
@@ -0,0 +1,528 @@
import type {
AddMemberParams,
ChangGroupOwnerParams,
CountersParams,
CreateGroupParams,
DeleteMemberParams,
GetGroupProfileParams,
GetMemberListParams,
GetMemberProfileParams,
GroupAttrParams,
JoinGroupParams,
KeyListParams,
MarkMemberParams,
SetCountersParams,
SetMemberCustomFiledParams,
SetMemberMuteParams,
SetMemberNameCardParams,
SetMemberRoleParams,
UpdateGroupParams,
handleGroupApplicationParams,
} from '../../type';
/**
* @interface TUIGroupService
*/
export interface ITUIGroupService {
/**
* 初始化 Service
* @function
* @private
*/
init(): void;
/**
* 切换群组
* @function
* @param {String} groupID 群组ID
* @example
* // 切换到 group1 群
* let promise = TUIGroupService.switchGroup('group1');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
switchGroup(groupID: string): void;
/**
* 获取群详细资料
* @function
* @param {GetGroupProfileParams} options 获取群详细资料参数
* @example
* let promise = TUIGroupService.getGroupProfile({
* groupID: 'group1',
* groupCustomFieldFilter: ['key1','key2']
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupProfile(options: GetGroupProfileParams): Promise<any>;
/**
* 更新群详细资料
* @function
* @param {UpdateGroupParams} options 更新群详细资料参数
* @example
* let promise = TUIGroupService.updateGroupProfile({
* groupID: 'group1',
* name: 'new name', // 修改群名称
* introduction: 'this is introduction.', // 修改群简介
* groupCustomField: [{ key: 'group_level', value: 'high'}] // 修改群组维度自定义字段
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
* @example
* let promise = TUIGroupService.updateGroupProfile({
* groupID: 'group1',
* muteAllMembers: true, // true 表示全体禁言,false表示取消全体禁言
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
updateGroupProfile(options: UpdateGroupParams): Promise<any>;
/**
* 创建群组
* @function
* @param {CreateGroupParams} options 创建群组参数
* @example
* let promise = TUIGroupService.createGroup({
* type: TUIChatEngine.TYPES.GRP_WORK,
* name: 'newGroup',
* memberList: [{
* userID: 'user1',
* // 群成员维度的自定义字段,默认情况是没有的,需要开通,详情请参阅自定义字段
* memberCustomField: [{key: 'group_member_test', value: 'test'}]
* }, {
* userID: 'user2'
* }] // 如果填写了 memberList,则必须填写 userID
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
createGroup(options: CreateGroupParams): Promise<any>;
/**
* 解散群组
* @function
* @param {string} groupID 群组ID
* @example
* let promise = TUIGroupService.dismissGroup('group1');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
dismissGroup(groupID: string): Promise<any>;
/**
* 通过 groupID 搜索群组
* @function
* @param {string} groupID 群组ID
* @example
* let promise = TUIGroupService.searchGroupByID('group1');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
searchGroupByID(groupID: string): Promise<any>;
/**
* 申请加群
* @function
* @param {JoinGroupParams} options 加群参数
* @example
* let promise = TUIGroupService.joinGroup({
* groupID: 'group1',
* applyMessage: 'xxxx'
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
joinGroup(options: JoinGroupParams): Promise<any>;
/**
* 退出群组
* @function
* @param {string} groupID 群组ID
* @example
* let promise = TUIGroupService.quitGroup('group1');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
quitGroup(groupID: string): Promise<any>;
/**
* 获取加群申请和邀请进群申请列表
* @function
* @example
* let promise = TUIGroupService.getGroupApplicationList();
* promise.then(function(chatResponse) {
* const { applicationList } = chatResponse.data;
* applicationList.forEach((item) => {
* // item.applicant - 申请者 userID
* // item.applicantNick - 申请者昵称
* // item.groupID - 群 ID
* // item.groupName - 群名称
* // item.applicationType - 申请类型:0 加群申请,2 邀请进群申请
* // item.userID - applicationType = 2 时,是被邀请人的 userID
* // 接入侧可调用 handleGroupApplication 接口同意或拒绝加群申请
* TUIGroupService.handleGroupApplication({
* handleAction: 'Agree',
* application: {...item},
* });
* });
* }).catch(function(imError) {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupApplicationList(): Promise<any>;
/**
* 处理加群申请和邀请进群申请
* @function
* @param {handleGroupApplicationParams} options 处理申请加群参数
* @example
* // 通过获取未决列表处理加群申请
* let promise = TUIGroupService.getGroupApplicationList();
* promise.then(function(chatResponse) {
* const { applicationList } = chatResponse.data;
* applicationList.forEach((item) => {
* if (item.applicationType === 0) {
* TUIGroupService.handleGroupApplication({
* handleAction: 'Agree',
* application: {...item},
* });
* }
* });
* }).catch(function(imError) {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
* @example
* // 通过获取未决列表处理邀请进群申请
* let promise = TUIGroupService.getGroupApplicationList();
* promise.then(function(chatResponse) {
* const { applicationList } = chatResponse.data;
* applicationList.forEach((item) => {
* if (item.applicationType === 2) {
* TUIGroupService.handleGroupApplication({
* handleAction: 'Agree',
* application: {...item},
* });
* }
* });
* }).catch(function(imError) {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
handleGroupApplication(options: handleGroupApplicationParams): Promise<any>;
/**
* 获取群在线人数
* - 注意:仅支持获取直播群在线人数
* @function
* @param {string} groupID 群组ID
* @example
* let promise = TUIGroupService.getGroupOnlineMemberCount('group1');
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupOnlineMemberCount(groupID: string): Promise<any>;
/**
* 转让群组
* @function
* @param {ChangGroupOwnerParams} options 群资料信息
* @example
* let promise = TUIGroupService.changeGroupOwner({
* groupID: 'group1',
* newOwnerID: 'user2'
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
changeGroupOwner(options: ChangGroupOwnerParams): Promise<any>;
/**
* 初始化群属性
* @function
* @param {GroupAttrParams} groupAttributes 群属性信息
* @example
* let promise = TUIGroupService.initGroupAttributes({
* groupID: 'group1',
* groupAttributes: { key1: 'value1', key2: 'value2' }
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
initGroupAttributes(groupAttributes: GroupAttrParams): Promise<any>;
/**
* 设置群属性
* @function
* @param {GroupAttrParams} groupAttributes 群属性信息
* @example
* let promise = TUIGroupService.setGroupAttributes({
* groupID: 'group1',
* groupAttributes: { key1: 'value1', key2: 'value2' }
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupAttributes(groupAttributes: GroupAttrParams): Promise<any>;
/**
* 删除群属性
* @function
* @param {KeyListParams} options 群属性参数
* @example
* let promise = TUIGroupService.deleteGroupAttributes({
* groupID: 'group1',
* keyList: ['key1', 'key2']
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
deleteGroupAttributes(options: KeyListParams): Promise<any>;
/**
* 获取群属性
* @function
* @param {KeyListParams} options 群属性参数
* @example
* let promise = TUIGroupService.getGroupAttributes({
* groupID: 'group1',
* keyList: ['key1', 'key2']
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupAttributes(options: KeyListParams): Promise<any>;
/**
* 设置计数器
* @function
* @param {SetCountersParams} counters 计数器信息
* @example
* let promise = TUIGroupService.setGroupCounters({
* groupID: 'group1',
* counters: { key1: 1, key2: 2 }
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupCounters(counters: SetCountersParams): Promise<any>;
/**
* 递增计数器
* @function
* @param {CountersParams} options 计数器信息
* @example
* let promise = TUIGroupService.increaseGroupCounter({
* groupID: 'group1',
* key: 'key1',
* value: 1,
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
increaseGroupCounter(options: CountersParams): Promise<any>;
/**
* 递减计数器
* @function
* @param {CountersParams} options 计数器信息
* @example
* let promise = TUIGroupService.decreaseGroupCounter({
* groupID: 'group1',
* key: 'key1',
* value: 2,
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
decreaseGroupCounter(options: CountersParams): Promise<any>;
/**
* 获取计数器
* @function
* @param {KeyListParams} options 计数器参数
* @example
* let promise = TUIGroupService.getGroupCounters({
* groupID: 'group1',
* keyList: ['key1', 'key2']
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupCounters(options: KeyListParams): Promise<any>;
// group member
/**
* 获取群成员列表
* @function
* @param {GetMemberListParams} options 参数选项
* @example
* let promise = TUIGroupService.getGroupMemberList({
* groupID: 'group1',
* count: 15,
* offset: 0
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupMemberList(options: GetMemberListParams): Promise<any>;
/**
* 获取群成员资料
* @function
* @param {GetMemberProfileParams} options 参数选项
* @example
* let promise = TUIGroupService.getGroupMemberProfile({
* groupID: 'group1',
* userIDList: ['user1', 'user2'], // 请注意:即使只拉取一个群成员的资料,也需要用数组类型,例如:userIDList: ['user1']
* memberCustomFieldFilter: ['group_member_custom'], // 筛选群成员自定义字段:group_member_custom
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getGroupMemberProfile(options: GetMemberProfileParams): Promise<any>;
/**
* 邀请群成员
* @function
* @param {AddMemberParams} options 参数选项
* @example
* let promise = TUIGroupService.addGroupMember({
* groupID: 'group1',
* userIDList: ['user1','user2','user3']
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
addGroupMember(options: AddMemberParams): Promise<any>;
/**
* 删除群成员,群主可移除群成员。
* - 注意1:旗舰版套餐包支持删除直播群群成员。您可以通过调用此接口实现直播群【封禁群成员】的效果。
* - 注意2:踢出时长字段仅直播群(AVChatRoom)支持。
* - 注意3:群成员被移除出直播群后,在【踢出时长内】如果用户想再次进群,需要 APP 管理员调用 restapi 解封。过了【踢出时长】,用户可再次主动加入直播群。
* @function
* @param {DeleteMemberParams} options 参数选项
* @example
* let promise = TUIGroupService.deleteGroupMember({
* groupID: 'group1',
* userIDList: ['user1'],
* reason: '你违规了,我要踢你!',
* duration: 60
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
deleteGroupMember(options: DeleteMemberParams): Promise<any>;
/**
* 设置群成员禁言
* @function
* @param {SetMemberMuteParams} options 参数选项
* @example
* let promise = TUIGroupService.setGroupMemberMuteTime({
* groupID: 'group1',
* userID: 'user1',
* muteTime: 600 // 禁言10分钟;设为0,则表示取消禁言
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupMemberMuteTime(options: SetMemberMuteParams): Promise<any>;
/**
* 设置群成员角色
* @function
* @param {SetMemberRoleParams} options 参数选项
* @example
* let promise = TUIGroupService.setGroupMemberRole({
* groupID: 'group1',
* userID: 'user1',
* role: TUIChatEngine.TYPES.GRP_MBR_ROLE_ADMIN
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupMemberRole(options: SetMemberRoleParams): Promise<any>;
/**
* 设置群成员名片
* - 注意1:群主可设置所有群成员的名片。
* - 注意2: 群管理员可设置自身和其他普通群成员的群名片。
* - 注意3:普通群成员只能设置自身群名片。
* @function
* @param {SetMemberNameCardParams} options 参数选项
* @example
* let promise = TUIGroupService.setGroupMemberNameCard({
* groupID: 'group1',
* userID: 'user1',
* nameCard: '用户名片'
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupMemberNameCard(options: SetMemberNameCardParams): Promise<any>;
/**
* 设置群成员自定义字段
* @function
* @param {SetMemberCustomFiledParams} options 参数选项
* @example
* let promise = TUIGroupService.setGroupMemberCustomField({
* groupID: 'group1',
* memberCustomField: [{key: 'group_member_test', value: 'test'}]
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
setGroupMemberCustomField(options: SetMemberCustomFiledParams): Promise<any>;
/**
* 标记群成员
* - 注意1:仅支持直播群。
* - 注意2:只有群主才有权限标记群组中其他成员。
* - 注意3:使用该接口需要您购买旗舰版套餐。
* @function
* @param {MarkMemberParams} options 参数选项
* @example
* let promise = TUIGroupService.markGroupMemberList({
* groupID: 'group1',
* userIDList: ['user1', 'user2'],
* markType: 1000,
* enableMark: true,
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
markGroupMemberList(options: MarkMemberParams): Promise<any>;
}
@@ -0,0 +1,11 @@
/**
* @interface TUITranslateService
*/
export interface ITUIReportService {
/**
* Report key feature usage for analytics
* @param code - Event code for analytics
* @param feature - Feature name or identifier
*/
reportFeature(code: number, feature?: string): void;
}
@@ -0,0 +1,49 @@
import type TUIBase from '../../tui-base';
import type { SwitchUserStatusParams, UserIDListParams, UpdateMyProfileParams } from '../../type';
/**
* @interface TUIUserService
*/
export interface ITUIUserService extends TUIBase {
/**
* 初始化 Service
* @function
* @private
*/
init(): void;
/**
* 获取用户资料
* @function
* @param {UserIDListParams} options [options = undefined] 用户 ID 列表,不传 options 默认查询自己的资料
* @example
* // 查询自己的资料
* let promise = TUIUserService.getUserProfile();
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
* @example
* // 查询其他用户的资料
* let promise = TUIUserService.getUserProfile({ userIDList: ['user1', 'user2'] });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
getUserProfile(options?: UserIDListParams): Promise<any>;
/**
* 更新自己的资料信息
* @function
* @param {UpdateMyProfileParams} options 参数选项
* @example
* // 更新自己的昵称
* let promise = TUIUserService.updateMyProfile({
* nick: 'newNick'
* });
* promise.catch((error) => {
* // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
* });
*/
updateMyProfile(options: UpdateMyProfileParams): Promise<any>;
}
@@ -0,0 +1,47 @@
export interface ITasks {
sendMessage: boolean; // 发送一条消息
revokeMessage: boolean; // 撤回一条消息
modifyNickName: boolean; // 修改一次我的昵称
groupChat: boolean; // 发起一个群聊
muteGroup: boolean; // 开启一次群禁言
dismissGroup: boolean; // 解散一个群聊
call: boolean; // 发起一次通话
searchCloudMessage: boolean; // 进行一次消息云端搜索
customerService: boolean; // 进行一次客服会话
translateTextMessage: boolean; // 进行一次文本消息翻译
}
export interface IAppStore {
store: {
enabledMessageReadReceipt: boolean; // 消息已读回执功能是否已开启
enabledEmojiPlugin: boolean; // 表情回复插件能力是否已开启
enabledOnlineStatus: boolean; // 用户在线状态能力是否已开启
enabledCustomerServicePlugin: boolean; // 客服插件能力是否已开启
enabledTranslationPlugin: boolean; // 文本消息翻译能力是否已开启
enabledVoiceToText: boolean; // 语音转文字能力是否已开启
enableTyping: boolean; // 正在输入能力开关
enableConversationDraft: boolean; // 会话草稿能力开关
enableAutoMessageRead: boolean; // 接收消息自动已读上报能力开关
isOfficial: boolean; // 是否是官网 SDKAppID
SDKVersion: string; // Chat SDK 版本号
tasks: ITasks; // sample demo 任务列表
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,47 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type { IMessageModel } from '../model';
interface ITextInfo {
messageID: string;
visible: boolean;
}
export interface IChatStore {
store: {
messageList: IMessageModel[];
isCompleted: boolean;
nextReqMessageID: string;
quoteMessage: Message | any;
typingStatus: boolean;
messageSource: IMessageModel | undefined;
newMessageList: Message[];
translateTextInfo: Map<string, ITextInfo[]> | undefined;
voiceToTextInfo: Map<string, ITextInfo[]> | undefined;
userInfo: Record<string, any>;
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* 从 messageList 中获取 messageModel
* @param {Sting} id 需要获取的 messageID
*/
getModel(id: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,35 @@
import type { IConversationModel } from '../model';
export interface IConversationStore {
store: {
currentConversationID: string;
currentConversation: IConversationModel | null;
totalUnreadCount: number;
conversationList: IConversationModel[];
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* 从 conversationList 中获取 conversationModel
* @param {Sting} conversationID 需要获取的会话 ID
*/
getModel(id: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,23 @@
export interface ICustomStore {
store: any; // 自定义 store,属性可根据需要进行自定义
/**
* 更新自定义 store
* @param {Sting} key 待更新的 key,没有则写入
* @param {any} value 待更新的数据
*/
update(key: string, value: any): void;
/**
* 从自定义 store 中获取数据
* @param {Sting} store store 的名称
* @param {Sting} key 待获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,29 @@
import type { Friend, FriendApplication, FriendGroup } from '../../type';
export interface IFriendStore {
store: {
friendList: Friend[];
friendApplicationList: FriendApplication[];
friendApplicationUnreadCount: number; // 好友申请的未读数
friendGroupList: FriendGroup[];
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,46 @@
import type { Message } from '@tencentcloud/lite-chat/basic';
import type { IGroupModel } from '../model';
export interface GroupMember {
userID: string;
avatar: string;
nick: string;
role: string;
joinTime: number;
nameCard: string;
muteUntil: number;
memberCustomField: Record<string, any>[];
}
export interface IGroupStore {
store: {
currentGroupID: string;
currentGroup: IGroupModel | undefined;
currentGroupMemberList: GroupMember[];
currentGroupAttributes: Record<string, any>;
currentGroupCounters: Record<string, number>;
groupList: IGroupModel[];
groupSystemNoticeList: Message[];
isCompleted: boolean;
offset: number | string;
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,9 @@
export * from './tui-store';
export * from './user';
export * from './conversation';
export * from './chat';
export * from './group';
export * from './custom';
export * from './friend';
export * from './app';
export * from './search';
@@ -0,0 +1,37 @@
import type { ISearchParamsMap, ISearchResult, ISearchType } from '../../type';
export interface ISearchStoreStore {
searchMessagesResult: ISearchResult<ISearchType.MESSAGE>;
searchChatMessagesResult: ISearchResult<ISearchType.MESSAGE>;
searchUserResult: ISearchResult<ISearchType.USER>;
searchGroupResult: ISearchResult<ISearchType.GROUP>;
searchMessageParams: ISearchParamsMap[ISearchType.MESSAGE];
searchChatMessageParams: ISearchParamsMap[ISearchType.MESSAGE];
searchUserParams: ISearchParamsMap[ISearchType.USER];
searchGroupParams: ISearchParamsMap[ISearchType.GROUP];
error: Error | null;
[key: string]: any;
}
export interface ISearchStore {
store: ISearchStoreStore;
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: string[]): void;
}
@@ -0,0 +1,138 @@
/**
* @property {Boolean} enableTyping 正在输入功能是否开启,默认开启 true
* @property {Boolean} enabledMessageReadReceipt 消息已读回执功能是否已开启,默认 false,购买旗舰版套餐后开启
* @property {Boolean} enabledEmojiPlugin 表情回复插件能力是否已开启,默认 false,购买旗舰版套餐后开启
* @property {Boolean} enabledOnlineStatus 用户在线状态能力是否已开启,默认 false,购买旗舰版套餐后开启
* @property {Boolean} enabledCustomerServicePlugin 客服插件能力是否已开启,默认 false,购买客服插件后开启
* @property {Boolean} enabledTranslationPlugin 文本消息翻译能力是否已开启,默认 false,购买翻译插件后开启
* @property {Boolean} enableConversationDraft 会话草稿功能是否开启,默认开启 true
* @example
* // UI 层调用以下逻辑关闭正在输入功能
* TUIStore.update(StoreName.APP, 'enableTyping', false);
* @example
* // UI 层调用以下逻辑关闭会话草稿功能
* TUIStore.update(StoreName.APP, 'enableConversationDraft', false);
*/
export enum AppStore {}
/**
* @property {String} currentConversationID 当前会话ID
* @property {Array<IConversationModel>} conversationList 会话列表
* @property {Number} totalUnreadCount 会话未读总数
* @example
* // UI 层监听会话列表更新通知
* let onConversationListUpdated = function(conversationList) {
* console.warn(conversationList);
* }
* TUIStore.watch(StoreName.CONV, {
* conversationList: onConversationListUpdated,
* })
*/
export enum ConversationStore {}
/**
* @property {Array<IMessageModel>} messageList 消息列表
* @property {Boolean} isCompleted 漫游是否拉完(用于控制‘查看更多’按钮显示)
* @property {Message | any} quoteMessage 被引用的消息信息,引用消息时会触发更新
* @property {Boolean} typingStatus 正在输入的状态标识, 默认 false,开启正在输入状态后,输入消息时会触发更新
* @property {IMessageModel} messageSource 用于消息云端搜索结果跳转至指定消息标识
* @property {Array<Message>} newMessageList 新消息通知列表,提供给 TUINotification 组件使用
* @property {Record<string, string | undefined | boolean>} translateTextInfo 文本消息翻译信息
* @example
* // UI 层监听当前会话消息列表更新通知
* let onMessageListUpdated = function(messageList) {
* console.warn(messageList);
* }
* TUIStore.watch(StoreName.CHAT, {
* messageList: onMessageListUpdated,
* })
* @example
* // UI 层更新消息云端搜索结果跳转至指定消息标识
* TUIStore.update(StoreName.CHAT, 'messageSource', message);
* // UI 层监听消息云端搜索结果跳转至指定消息标识
* let onMessageSourceUpdated = function(message) {
* console.warn(message);
* }
* TUIStore.watch(StoreName.CHAT, {
* messageSource: onMessageSourceUpdated,
* })
* @example
* // UI 层更新文本消息翻译信息
* TUIStore.update(StoreName.CHAT, 'translateTextInfo', {
* conversationID: 'xxx',
* messageID: 'xxx',
* visible: false,
* });
* // UI 层监听文本消息翻译更新通知
* let onTranslateTextInfoUpdated = function(info) {
* // info 返回的是 map 或 undefined
* if (info) {
* const list = info.get('conversationID') || [];
* list.forEach(item => {
* const { messageID, visible } = item;
* // messageID - 当前操作的消息的 ID
* // visible - 是否显示翻译文本
* }
* }
* }
* TUIStore.watch(StoreName.CHAT, {
* translateTextInfo: onTranslateTextInfoUpdated,
* })
*/
export enum ChatStore {}
/**
* @property {String} currentGroupID 当前群组ID
* @property {Group} currentGroup 当前群组信息
* @property {Array} currentGroupMemberList 当前群组群成员列表
* @property {Object} currentGroupAttributes 当前群组群属性信息
* @property {Object} currentGroupCounters 当前群组计数器信息
* @property {Array<Group>} groupList 群组列表
* @property {Array<Message>} groupSystemNoticeList 群组系统通知列表(注意:Store 中不会存储群系统通知,即时通知)
* @example
* // UI 层监听群组列表更新通知
* let onGroupListUpdated = function(groupList) {
* console.warn(groupList);
* }
* TUIStore.watch(StoreName.GRP, {
* groupList: onGroupListUpdated,
* })
*/
export enum GroupStore {}
/**
* @property {Object} userProfile 当前登录用户的资料信息
* @property {Boolean} displayOnlineStatus 是否开启用户状态显示,默认 false:关闭
* @property {Boolean} displayMessageReadReceipt 是否开启消息阅读状态显示,默认 true:开启
* @property {String} kickedOut 用户被踢的类型信息
* @property {String} netStateChange 网络状态变更信息
* @property {Map<key, statusInfo>} userStatusList 订阅用户的状态信息的列表
* - key 用户 userID
* - statusInfo.statusType 用户当前状态
* - statusInfo.customStatus 用户自定义状态
* @property {Array<string>} userBlacklist 用户黑名单列表,UI 组件层可以通过监听该属性来获取用户黑名单列表
* @example
* // UI 层监听网络变更通知
* let onNetStateChange = function(state) {
* console.warn(state);
* }
* TUIStore.watch(StoreName.USER, {
* netStateChange: onNetStateChange,
* })
*/
export enum UserStore {}
/**
* @property {Array<Friend>} friendList 好友列表,UI 组件层可以通过监听该属性来获取好友列表
* @property {Array<FriendApplication>} friendApplicationList 好友申请列表,UI 组件层可以通过监听该属性来获取好友申请列表
* @property {number} friendApplicationUnreadCount 好友申请未读数,UI 组件层可以通过监听该属性来获取好友申请未读数
* @example
* // UI 层监听好友列表更新变更通知
* let onFriendListUpdated = function(friendList) {
* console.warn(friendList);
* }
* TUIStore.watch(StoreName.FRIEND, {
* friendList: onFriendListUpdated ,
* })
*/
export enum FriendStore {}
@@ -0,0 +1,102 @@
import type { StoreName } from '../../const';
import type { IConversationModel, IMessageModel } from '../model';
// 此处 Map 的 value = 1 为占位作用
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Task = Record<StoreName, Record<string, Map<(data?: unknown) => void, 1>>>;
export interface IOptions {
[key: string]: (newData?: any) => void;
}
// 自定义 store 暂不支持
// @property {ICustomStore} customStore 自定义 store,可根据业务需要通过以下 API 进行数据操作。
/**
* @class TUIStore
*/
export interface ITUIStore {
task: Task;
/**
* UI 组件注册监听
* @function
* @param {StoreName} storeName store 名称
* @param {IOptions} options UI 组件注册的监听信息
* @example
* // UI 层监听会话列表更新通知
* let onConversationListUpdated = function(conversationList) {
* console.warn(conversationList);
* }
* TUIStore.watch(StoreName.CONV, {
* conversationList: onConversationListUpdated,
* })
*/
watch(storeName: StoreName, options: IOptions): void;
/**
* UI 组件取消监听回调
* 注意:取消监听时传入的回调函数和注册监听的回调函数是同一个
* @function
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息,包含需要取消的回调等
* @example
* // UI 层取消监听会话列表更新通知
* TUIStore.unwatch(StoreName.CONV, {
* conversationList: onConversationListUpdated,
* })
*/
unwatch(storeName: StoreName, options: IOptions): void;
/**
* 更新 store
* - 使用自定义 store 时,可以用此 API 更新自定义数据
* @function
* @param {StoreName} storeName store 名称
* @param {String} key 需要更新的 key
* @param {any} data 待更新的数据
* @example
* // UI 层更新自定义 Store 数据
* const data: string = '自定义数据'
* TUIStore.update(StoreName.CUSTOM, 'customKey', data)
*/
update(storeName: StoreName, key: string, data: any): void;
/**
* 获取 store 中的数据
* @function
* @param {StoreName} storeName store 名称
* @param {String} key 需要获取的 key
* @private
*/
getData(storeName: StoreName, key: string): any;
/**
* 获取 conversationModel
* @function
* @param {string} conversationID 会话 ID
* @example
* // 获取 conversationModel
* const conversationID = 'C2C9241'
* const conversationModel = TUIStore.getConversationModel(conversationID);
*/
getConversationModel(conversationID: string): IConversationModel;
/**
* 获取 messageModel
* @function
* @param {string} messageID 消息 ID
* @example
* // 获取 messageModel
* const messageID = '144115225790497300-1686557023-31267600'
* const messageModel = TUIStore.getMessageModel(messageID);
*/
getMessageModel(messageID: string): IMessageModel;
/**
* 重置 store 内数据
* @function
* @param {StoreName} storeName store 名称
* @param {Array<string>} keyList 需要 reset 的 keyList
* @param {boolean} isNotificationNeeded 是否需要触发更新
* @private
*/
reset(storeName: StoreName, keyList?: string[], isNotificationNeeded?: boolean): void;
}
@@ -0,0 +1,34 @@
export interface IUserStore {
store: {
userProfile: object;
displayOnlineStatus: boolean; // 是否开启用户状态显示
displayMessageReadReceipt: boolean; // 是否开启消息阅读状态显示
userStatusList: Map<string, {
statusType: number;
customStatus: string;
}>; // 订阅用户的状态信息的列表
kickedOut: string; // 用户被踢的类型信息
netStateChange: string; // 网络状态变更信息
userBlacklist: any[]; // 用户黑名单列表
targetLanguage: string; // 文本消息翻译目标语言,内部使用
};
/**
* 更新 store
* @param {Sting} key 待更新的 key
* @param {any} data 更新的数据
*/
update(key: string, data: any): void;
/**
* 从 store 中获取数据
* @param {Sting} key 需要获取的 key
*/
getData(key: string): any;
/**
* reset Store 内数据
* @param { Array<string>} keyList 需要 reset 的 keyList
*/
reset(keyList?: any[]): void;
}
@@ -0,0 +1,133 @@
import TUIBase from '../tui-base';
import type { IConversationModel } from '../interface/model';
import { isPrivateKey, isUrl, calculateTimeAgo, JSONToObject } from '../utils/common-utils';
import type { MuteConversationParams } from '../type';
import { AVATAR } from '../config';
import isEmpty from '../utils/is-empty';
import { IN_WX_MINI_APP } from '../utils/env';
const LAST_MSG_FOR_SHOW = [
'[图片]',
'[语音]',
'[视频]',
'[文件]',
'[位置]',
'[地理位置]',
'[动画表情]',
'[自定义消息]',
'[群提示消息]',
'[聊天记录]',
];
export default class ConversationModel extends TUIBase implements IConversationModel {
public conversationID!: string;
public type!: string;
public subType!: string;
public unreadCount!: number;
public lastMessage!: {
nick: string;
nameCard: string;
lastTime: number;
lastSequence: string;
fromAccount: string;
isRevoked: boolean;
revoker?: string | undefined;
isPeerRead: boolean;
messageForShow: string;
type: string;
payload: any;
};
public groupProfile?: any;
public userProfile?: any;
public groupAtInfoList!: any[];
public remark!: string;
public isPinned!: boolean;
public messageRemindType!: string;
public markList!: string[];
public customData!: string;
public conversationGroupList!: any[];
public draftText!: string;
public isMuted!: boolean;
public operationType!: number;
public _conversation: any;
constructor(options: any) {
super();
this.initProxy(options);
this.isMuted = (this.messageRemindType === this.getEngine().TYPES.MSG_REMIND_ACPT_NOT_NOTE)
|| (this.messageRemindType === this.getEngine().TYPES.MSG_REMIND_DISCARD);
this.operationType = 0;
this._conversation = options;
}
/**
* 初始化挂载 conversation 字段
*/
private initProxy(options: any) {
Object.keys(options).forEach((key) => {
if (!isPrivateKey(key)) {
(this as any)[key] = options[key];
}
});
}
updateProperties(options: any) {
Object.keys(options).forEach((key) => {
if (!isPrivateKey(key)) {
(this as any)[key] = options[key];
}
});
}
updateOperationType(operationType: number) {
this.operationType = operationType;
}
getConversation() {
return this._conversation;
}
setMessageRead() {
return this.getEngine().TUIConversation.setMessageRead(this.conversationID);
}
// 以下方法用于处理 conversation 需要展示的数据
getAvatar() {
const chatEngine = this.getEngine();
let avatar = '';
switch (this.type) {
case chatEngine.TYPES.CONV_C2C:
avatar = isUrl(this.userProfile?.avatar) ? this.userProfile?.avatar : AVATAR.C2C;
break;
case chatEngine.TYPES.CONV_GROUP:
avatar = isUrl(this.groupProfile?.avatar) ? this.groupProfile?.avatar : AVATAR.GROUP;
break;
case chatEngine.TYPES.CONV_SYSTEM:
avatar = isUrl(this.groupProfile?.avatar) ? this.groupProfile?.avatar : AVATAR.SYSTEM;
break;
default:
break;
}
return avatar;
}
getShowName() {
const chatEngine = this.getEngine();
let name = '';
switch (this.type) {
case chatEngine.TYPES.CONV_C2C:
name = this.remark || this.userProfile?.nick || this.userProfile?.userID || '';
break;
case chatEngine.TYPES.CONV_GROUP:
name = this.groupProfile?.name || this.groupProfile?.groupID || '';
break;
case chatEngine.TYPES.CONV_SYSTEM:
name = '系统通知';
break;
default:
break;
}
return name;
}
}
@@ -0,0 +1,59 @@
// @ts-nocheck
import type { Group } from '@tencentcloud/chat';
import type TencentCloudChat from '@tencentcloud/chat';
import type { IGroupModel } from '../interface/model';
import { isPrivateKey } from '../utils/common-utils';
export default class GroupModel implements IGroupModel {
public groupID!: string;
public name!: string;
public avatar!: string;
public type!: TencentCloudChat.TYPES.GRP_WORK
| TencentCloudChat.TYPES.GRP_PUBLIC
| TencentCloudChat.TYPES.GRP_MEETING
| TencentCloudChat.TYPES.GRP_AVCHATROOM
| TencentCloudChat.TYPES.GRP_COMMUNITY;
public introduction!: string;
public notification!: string;
public ownerID!: string;
public createTime!: number;
public infoSequence?: number | undefined;
public lastInfoTime?: number | undefined;
public selfInfo?: {
role?: string | undefined;
messageRemindType?: string | undefined;
joinTime?: number | undefined;
nameCard?: string | undefined;
userID?: string | undefined;
memberCustomField?: any[] | undefined;
} | undefined;
public lastMessage?: any;
public nextMessageSeq!: number;
public memberCount!: number;
public maxMemberCount!: number;
public muteAllMembers!: boolean;
public joinOption!: string;
public inviteOption!: string;
public groupCustomField?: any[] | undefined;
public isSupportTopic!: boolean;
public groupAttributes: any;
public groupCounters: any;
constructor(options: Group) {
this.groupAttributes = {};
this.groupCounters = {};
this.initProxy(options);
}
/**
* 初始化挂载 group 字段
*/
private initProxy(options: Group) {
Object.keys(options).forEach((key) => {
if (!isPrivateKey(key)) {
(this as any)[key] = options[key as keyof Group];
}
});
}
}
@@ -0,0 +1,177 @@
/* eslint-disable prefer-promise-reject-errors */
// @ts-nocheck
import type { Conversation, Message } from '@tencentcloud/chat';
import type TencentCloudChat from '@tencentcloud/chat';
import { ERROR_CODE_ENGINE, ERROR_MSG_ENGINE } from '../const';
import type { IConversationModel, IMessageModel } from '../interface/model';
import { isPrivateKey, isUndefined } from '../utils/common-utils';
import TUIBase from '../tui-base';
import type { ModifyMessageParams, ReactionInfo } from '../type';
export default class MessageModel extends TUIBase implements IMessageModel {
public ID!: string;
public type!: TencentCloudChat.TYPES;
payload: any;
conversationID!: string;
conversationType!: TencentCloudChat.TYPES;
to!: string;
from!: string;
flow!: string;
time!: number;
status!: string;
isRevoked!: boolean;
priority!: TencentCloudChat.TYPES;
nick!: string;
avatar!: string;
isPeerRead!: boolean;
nameCard!: string;
atUserList!: string[];
cloudCustomData!: string;
isDeleted!: boolean;
isModified!: boolean;
needReadReceipt!: boolean;
readReceiptInfo!: {
readCount?: number;
unreadCount?: number;
isPeerRead?: boolean;
};
isBroadcastMessage!: boolean;
isSupportExtension!: boolean;
receiverList!: string[];
revoker!: string;
sequence!: number;
progress!: number;
revokerInfo!: { userID: string; nick: string; avatar: string };
revokeReason!: string;
hasRiskContent!: boolean;
reactionList!: ReactionInfo[];
private _message: Message;
private _signalingInfo!: any; // 默认值 undefined_signalingInfo 的类型是 undefinednull|object,因为 getSignalingInfo 返回值是 null|object,枚举会引起编译报错,所以此处用 any 代替。
// 维护映射关系,方便使用
private messageHandlers = {
[this.getEngine().TYPES.MSG_TEXT]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleTextMessage(message),
[this.getEngine().TYPES.MSG_FACE]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleFaceMessage(message),
[this.getEngine().TYPES.MSG_LOCATION]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleLocationMessage(message),
[this.getEngine().TYPES.MSG_IMAGE]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleImageMessage(message),
[this.getEngine().TYPES.MSG_AUDIO]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleAudioMessage(message),
[this.getEngine().TYPES.MSG_VIDEO]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleVideoMessage(message),
[this.getEngine().TYPES.MSG_FILE]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleFileMessage(message),
[this.getEngine().TYPES.MSG_CUSTOM]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleCustomMessage(message),
[this.getEngine().TYPES.MSG_MERGER]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleMergeMessage(message),
[this.getEngine().TYPES.MSG_GRP_TIP]: (message: Message) => this.getEngine().TUIChat.messageHandler.handleGroupTipsMessage(message),
};
constructor(options: Message) {
super();
this._message = options;
this._signalingInfo = undefined;
this.progress = 0;
this.reactionList = [];
this.initProperties(options);
}
/**
* 初始化 messageModel 属性
*/
private initProperties(options: Message): void {
Object.keys(options).forEach((key) => {
if (!isPrivateKey(key)) {
(this as any)[key] = options[key as keyof Message];
}
});
}
updateProperties(options: Message): void {
this._message = options;
Object.keys(options).forEach((key) => {
if (!isPrivateKey(key)) {
(this as any)[key] = options[key as keyof Message];
}
});
}
getMessage() {
return this._message;
}
modifyMessage(options: ModifyMessageParams) {
if (options.type && this._message.type !== options.type && !options.payload) {
return Promise.reject({
code: ERROR_CODE_ENGINE.MISMATCH_TYPE_AND_PAYLOAD,
message: ERROR_MSG_ENGINE.MISMATCH_TYPE_AND_PAYLOAD,
});
}
this._message.type = options.type || this._message.type;
this._message.payload = options.payload || this._message.payload;
this._message.cloudCustomData = options.cloudCustomData || this._message.cloudCustomData;
return this.getEngine().TUIChat.modifyMessage(this._message);
}
revokeMessage() {
return this.getEngine().TUIChat.revokeMessage(this._message);
}
resendMessage() {
return this.getEngine().TUIChat.resendMessage(this._message);
}
deleteMessage() {
return this.getEngine().TUIChat.deleteMessage([this._message]);
}
quoteMessage() {
return this.getEngine().TUIChat.quoteMessage(this._message);
}
replyMessage() {
return this.getEngine().TUIChat.replyMessage(this._message);
}
setMessageExtensions(extensions: Record<string, any>[]) {
return this.getEngine().TUIChat.setMessageExtensions(this._message, extensions);
}
getMessageExtensions() {
return this.getEngine().TUIChat.getMessageExtensions(this._message);
}
deleteMessageExtensions(keyList?: string[]) {
return this.getEngine().TUIChat.deleteMessageExtensions(this._message, keyList);
}
getSignalingInfo() {
if (this.type !== this.getEngine().TYPES.MSG_CUSTOM) {
return null;
}
if (isUndefined(this._signalingInfo)) {
this._signalingInfo = this.getEngine().chat.getSignalingInfo(this._message);
return this._signalingInfo;
}
return this._signalingInfo;
}
getMessageContent() {
const handler = this.messageHandlers[this.type as any];
if (isUndefined(handler)) {
return {};
}
// 群提示消息不支持 showName
if (this.type === this.getEngine().TYPES.MSG_GRP_TIP) {
return handler(this._message);
}
// 普通消息发送方名称显示优先级(对齐微信体验)
// C2C 会话:remark > nick > userID
// 群会话:remark > nameCard > nick > userID
// const friendRemark: any = this.getEngine().TUIFriend.getFriendRemark([this.from]);
return {
...handler(this._message),
showName: this.nick || this.from,
};
}
sendForwardMessage(conversationList: Conversation[] | IConversationModel[]) {
return this.getEngine().TUIChat.sendForwardMessage(conversationList, [this._message]);
}
}
@@ -0,0 +1,4 @@
declare module 'tim-profanity-filter-plugin' {
const e: any;
export = e;
}
@@ -0,0 +1,7 @@
import ChatEngine from './TUIEngine/engine';
export default class TUIBase {
public getEngine() {
return ChatEngine.getInstance();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
import type { func } from '../type';
export const isUndefined = function (input: any) {
return typeof input === 'undefined';
};
export const isPlainObject = function (input: any) {
// 注意不能使用以下方式判断,因为IE9/IE10下,对象的__proto__是 undefined
// return isObject(input) && input.__proto__ === Object.prototype;
if (typeof input !== 'object' || input === null) {
return false;
}
const proto = Object.getPrototypeOf(input);
if (proto === null) { // edge case Object.create(null)
return true;
}
let baseProto = proto;
while (Object.getPrototypeOf(baseProto) !== null) {
baseProto = Object.getPrototypeOf(baseProto);
}
// 原型链第一个和最后一个比较
return proto === baseProto;
};
export const isArray = function (input: any) {
if (typeof Array.isArray === 'function') {
return Array.isArray(input);
}
return (Object as any).prototype.toString.call(input).match(/^\[object (.*)\]$/)[1].toLowerCase() === 'array';
};
export const isPrivateKey = function (key: string) {
return key.startsWith('_');
};
export const isUrl = function (url: string) {
return /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+[a-zA-Z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url);
};
export const calculateTimeAgo = function (dateTimeStamp: number, t: func): string {
const minute = 1000 * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const now = new Date().getTime();
const diffValue = now - dateTimeStamp;
let result = '';
if (diffValue < 0) {
return result;
}
const minC = diffValue / minute;
const hourC = diffValue / hour;
const dayC = diffValue / day;
const weekC = diffValue / week;
if (weekC >= 1 && weekC <= 4) {
result = ` ${parseInt(`${weekC}`, 10)} ${t('time.周')}${t('time.前')}`;
} else if (dayC >= 1 && dayC <= 6) {
result = ` ${parseInt(`${dayC}`, 10)} ${t('time.天')}${t('time.前')}`;
} else if (hourC >= 1 && hourC <= 23) {
result = ` ${parseInt(`${hourC}`, 10)} ${t('time.小时')}${t('time.前')}`;
} else if (minC >= 1 && minC <= 59) {
result = ` ${parseInt(`${minC}`, 10)} ${t('time.分钟')}${t('time.前')}`;
} else if (diffValue >= 0 && diffValue <= minute) {
result = `${t('time.刚刚')}`;
} else {
const dateTime = new Date();
dateTime.setTime(dateTimeStamp);
const _year = dateTime.getFullYear();
const _month = dateTime.getMonth() + 1 < 10 ? `0${dateTime.getMonth() + 1}` : dateTime.getMonth() + 1;
const _date = dateTime.getDate() < 10 ? `0${dateTime.getDate()}` : dateTime.getDate();
result = `${_year}-${_month}-${_date}`;
}
return result;
};
export function formatTime(secondTime: number) {
const time: number = secondTime;
let newTime = '';
let hour: number | string;
let minute: number | string;
let seconds: any;
if (time >= 3600) {
hour = parseInt(`${time / 3600}`, 10) < 10 ? `0${parseInt(`${time / 3600}`, 10)}` : parseInt(`${time / 3600}`, 10);
minute = parseInt(`${(time % 60) / 60}`, 10) < 10 ? `0${parseInt(`${(time % 60) / 60}`, 10)}` : parseInt(`${(time % 60) / 60}`, 10);
seconds = time % 3600 < 10 ? `0${time % 3600}` : time % 3600;
if (seconds > 60) {
minute = parseInt(`${seconds / 60}`, 10) < 10 ? `0${parseInt(`${seconds / 60}`, 10)}` : parseInt(`${seconds / 60}`, 10);
seconds = seconds % 60 < 10 ? `0${seconds % 60}` : seconds % 60;
}
newTime = `${hour}:${minute}:${seconds}`;
} else if (time >= 60 && time < 3600) {
minute = parseInt(`${time / 60}`, 10) < 10 ? `0${parseInt(`${time / 60}`, 10)}` : parseInt(`${time / 60}`, 10);
seconds = time % 60 < 10 ? `0${time % 60}` : time % 60;
newTime = `00:${minute}:${seconds}`;
} else if (time < 60) {
seconds = time < 10 ? `0${time}` : time;
newTime = `00:00:${seconds}`;
}
return newTime;
}
// Determine if it is a JSON string
export function isJSON(str: string) {
if (typeof str === 'string') {
try {
const data = JSON.parse(str);
if (data) {
return true;
}
return false;
} catch (error) {
return false;
}
}
return false;
}
// Determine if it is a JSON string
export const JSONToObject = function (str: string) {
if (!str || !isJSON(str)) {
return str;
}
return JSON.parse(str);
};
// formate file size
export const formateFileSize = function (fileSize: number) {
let ret = '';
if (fileSize >= 1024 * 1024) {
ret = `${(fileSize / (1024 * 1024)).toFixed(2)} Mb`;
} else if (fileSize >= 1024) {
ret = `${(fileSize / 1024).toFixed(2)} Kb`;
} else {
ret = `${fileSize.toFixed(2)}B`;
}
return ret;
};
/**
* 重试函数, catch 时,重试
*
* @param {Promise} promise 需重试的函数
* @param {number} num 需要重试的次数
* @param {number} time 间隔时间(s
* @returns {Promise<any>} im 接口的 response 原样返回
*/
export const retryPromise = (promise: Promise<any>, num = 6, time = 0.5) => {
let n = num;
const func = () => promise;
return func()
.catch((error: any) => {
if (n === 0) {
throw error;
}
const timer = setTimeout(() => {
func();
clearTimeout(timer);
n--;
}, time * 1000);
});
};
@@ -0,0 +1,65 @@
declare const wx: any;
declare const qq: any;
declare const tt: any;
declare const swan: any;
declare const my: any;
declare const jd: any;
declare const uni: any;
declare const window: any;
export const IN_WX_MINI_APP_DESK = (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function' && (wx.getSystemInfoSync().platform === 'mac' || wx.getSystemInfoSync().platform === 'windows'));
export const IN_WX_MINI_APP = (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function' && Boolean(wx.getSystemInfoSync().fontSizeSetting)) || IN_WX_MINI_APP_DESK;
export const IN_QQ_MINI_APP = (typeof qq !== 'undefined' && typeof qq.getSystemInfoSync === 'function' && Boolean(qq.getSystemInfoSync().fontSizeSetting));
export const IN_TT_MINI_APP = (typeof tt !== 'undefined' && typeof tt.getSystemInfoSync === 'function' && Boolean(tt.getSystemInfoSync().fontSizeSetting));
export const IN_BAIDU_MINI_APP = (typeof swan !== 'undefined' && typeof swan.getSystemInfoSync === 'function' && Boolean(swan.getSystemInfoSync().fontSizeSetting));
export const IN_ALIPAY_MINI_APP = (typeof my !== 'undefined' && typeof my.getSystemInfoSync === 'function' && Boolean(my.getSystemInfoSync().fontSizeSetting));
export const IN_JD_MINI_APP = (typeof jd !== 'undefined' && typeof jd.getSystemInfoSync === 'function');
export const IN_UNI_NATIVE_APP = (typeof uni !== 'undefined' && typeof window === 'undefined');
// eslint-disable-next-line @stylistic/max-len
export const IN_MINI_APP = IN_WX_MINI_APP || IN_QQ_MINI_APP || IN_TT_MINI_APP || IN_BAIDU_MINI_APP || IN_ALIPAY_MINI_APP || IN_JD_MINI_APP || IN_UNI_NATIVE_APP;
export const IN_UNI_APP = (typeof uni !== 'undefined');
export const IN_BROWSER = (function () {
if (typeof uni !== 'undefined') {
return !IN_MINI_APP;
}
return (typeof window !== 'undefined') && !IN_MINI_APP;
})();
// runtime env
export const APP_NAMESPACE = (() => {
if (IN_QQ_MINI_APP) {
return qq;
}
if (IN_TT_MINI_APP) {
return tt;
}
if (IN_BAIDU_MINI_APP) {
return swan;
}
if (IN_ALIPAY_MINI_APP) {
return my;
}
if (IN_WX_MINI_APP) {
return wx;
}
if (IN_UNI_NATIVE_APP) {
return uni;
}
if (IN_JD_MINI_APP) {
return jd;
}
if (IN_BROWSER) {
return window;
}
return {};
})();
const USER_AGENT = (IN_BROWSER && window && window.navigator && window.navigator.userAgent) || '';
const IS_ANDROID = /Android/i.test(USER_AGENT);
const IS_WIN_PHONE = /(?:Windows Phone)/.test(USER_AGENT);
const IS_SYMBIAN = /(?:SymbianOS)/.test(USER_AGENT);
const IS_IOS = /iPad/i.test(USER_AGENT) || /iPhone/i.test(USER_AGENT) || /iPod/i.test(USER_AGENT);
export const IS_H5 = IS_ANDROID || IS_WIN_PHONE || IS_SYMBIAN || IS_IOS;
export const IS_PC = IN_BROWSER && !IS_H5;
@@ -0,0 +1,30 @@
import { isPlainObject } from './common-utils';
const isEmpty = function (input: any) {
// Null and Undefined...
if (input === null || typeof (input) === 'undefined') return true;
// Booleans...
if (typeof input === 'boolean') return false;
// Numbers...
if (typeof input === 'number') return input === 0;
// Strings...
if (typeof input === 'string') return input.length === 0;
// Functions...
if (typeof input === 'function') return input.length === 0;
// Arrays...
if (Array.isArray(input)) return input.length === 0;
// Errors...
if (input instanceof Error) return input.message === '';
// plain object
if (isPlainObject(input)) {
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key)) {
return false;
}
}
return true;
}
return false;
};
export default isEmpty;
@@ -0,0 +1,40 @@
import type { func } from '../type';
class Middleware {
private cache: func[];
private middlewares: func[];
private options: any;
constructor() {
this.cache = [];
this.middlewares = [];
this.options = null; // 缓存options
}
use(fn: func) {
if (typeof fn !== 'function') {
console.error('middleware must be a function');
}
this.cache.push(fn);
return this;
}
next(): any {
if (this.middlewares && this.middlewares.length > 0) {
const ware: any = this.middlewares.shift();
return ware.call(this, this.options, this.next.bind(this));
}
return undefined;
}
/**
* @param {any} options - 数据的入口
* @returns {Function}
*/
run(options: any): any {
this.middlewares = this.cache.map(fn => fn);
this.options = options; // 缓存数据
return this.next();
}
}
export default Middleware;
@@ -0,0 +1,85 @@
import { IN_MINI_APP, APP_NAMESPACE } from './env';
/**
* storage 中写入一条数据。若key已存在,则会覆盖已有数据。
* @param {String} key - 缓存的key
* @param {*} value - 任何可JSON序列化的数据
* @param {Boolean} withPrefix 是否需要拼接 prefix 和 key,默认为 true,即需要拼接。
*/
export function setStorageItem(key: string, value: any, withPrefix = true) {
const realKey = withPrefix ? _getKey(key) : key;
_setStorageSync(realKey, value);
}
/**
* 获取storage中的一条数据
* @param {String} key - 缓存的key
* @param {Boolean} withPrefix 是否需要拼接 prefix 和 key,默认为 true,即需要拼接。
* @returns {any} 返回指定key的数据
*/
export function getStorageItem(key: string, withPrefix = true): any {
try {
const realKey: string = withPrefix ? _getKey(key) : key;
return _getStorageSync(realKey);
} catch (error) {
console.warn('Storage.getStorageItem error:', error);
return undefined;
}
}
/**
* 删除指定key的数据
* @param {String} key - 缓存的key
* @param {Boolean} withPrefix 是否需要拼接 prefix 和 key,默认为 true,即需要拼接。
*/
export function removeStorageItem(key: string, withPrefix = true) {
try {
const realKey = withPrefix ? _getKey(key) : key;
_removeStorageSync(realKey);
} catch (error) {
console.warn('Storage.removeStorageItem error:', error);
}
}
function _setStorageSync(key: string, data: string) {
if (IN_MINI_APP) {
APP_NAMESPACE.setStorageSync(key, data);
} else if (_canIUseCookies()) {
localStorage.setItem(key, JSON.stringify(data));
}
}
function _getStorageSync(key: string): any {
if (IN_MINI_APP) {
return APP_NAMESPACE.getStorageSync(key);
}
if (_canIUseCookies()) {
const data: any = localStorage.getItem(key);
if (data !== 'undefined') {
return JSON.parse(data);
}
}
return undefined;
}
function _removeStorageSync(key: string) {
if (IN_MINI_APP) {
APP_NAMESPACE.removeStorageSync(key);
} else if (_canIUseCookies()) {
localStorage.removeItem(key);
}
}
function _getKey(key: string) {
return `chat_engine_${key}`;
}
// 如果用户选择 block cookies,此时访问 localStorage 浏览器会抛错
// Uncaught SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document
// 通过 navigator.cookieEnabled 短路逻辑规避
function _canIUseCookies() {
// When the browser is configured to block third-party cookies, and navigator.cookieEnabled is invoked inside a third-party iframe,
// it returns true in Safari, Edge Spartan and IE (while trying to set a cookie in such scenario would fail).
// It returns false in Firefox and Chromium-based browsers.
return navigator && navigator.cookieEnabled && localStorage;
}
@@ -0,0 +1,35 @@
/* eslint-disable prefer-rest-params */
import type { ITUIChatEngine } from '../interface/engine';
import Middleware from './middleware';
import { ERROR_CODE_ENGINE, ERROR_MSG_ENGINE } from '../const';
export default function validateInitialization(
TUIChatEngine: ITUIChatEngine,
target: any,
APIList: object,
) {
const obj = Object.create(null);
Object.keys(APIList).forEach((api) => {
if (!target[api]) {
return;
}
obj[api] = target[api];
const validateMiddleware = new Middleware();
target[api] = function () {
const args = Array.from(arguments);
validateMiddleware
.use((options: any, next: any) => {
if (TUIChatEngine.isInited) {
return next();
}
return Promise.reject({
code: ERROR_CODE_ENGINE.NOT_INIT,
message: `${api} | ${ERROR_MSG_ENGINE.NOT_INIT}`,
});
})
.use((options: any) => obj[api].apply(target, options));
return validateMiddleware.run(args);
};
});
}