import { UseUrlPreview } from '../biz/hooks/useUrlPreview'; import { uilog } from '../const'; import { ErrorCode, UIKitError } from '../error'; import { ChatConversationType, ChatDownloadStatus, ChatFileMessageBody, ChatGroupMessageAck, ChatMessage, ChatMessageChatType, ChatMessagePinInfo, ChatMessageStatus, ChatMessageStatusCallback, ChatMessageType, ChatRecalledMessageInfo, ChatSearchDirection, ChatTextMessageBody, } from '../rename.chat'; import { asyncTask, getCurTs } from '../utils/function'; import { gCustomMessageRecallEventType, gMessageAttributeFileProgress, gMessageAttributeUrlPreview, } from './const'; import type { MessageCacheManager, MessageManagerListener, } from './messageManager.types'; import type { ChatService, ChatServiceListener } from './types'; import type { ConversationModel } from './types.ui'; import { userInfoFromMessage } from './utils'; let gListener: ChatServiceListener | undefined; const gUserListener: Map = new Map(); const gSendList: Map = new Map(); const gDownloadList: Map = new Map(); /** * Message Cache Manager Implementation. */ export class MessageCacheManagerImpl implements MessageCacheManager { _client: ChatService; _listener?: ChatServiceListener; _userListener: Map; _sendList: Map; _downloadList: Map; _recallTimeout: number; constructor(client: ChatService) { uilog.log('MessageCacheManager:constructor'); this._client = client; this._userListener = gUserListener; this._sendList = gSendList; this._downloadList = gDownloadList; this._recallTimeout = 120000; } init() { this.unInit(); uilog.log('MessageCacheManager:init'); gListener = { onMessagesReceived: this.bindOnMessagesReceived.bind(this), onMessagesRead: this.bindOnMessagesRead.bind(this), onGroupMessageRead: this.bindOnGroupMessageRead.bind(this), onMessagesDelivered: this.bindOnMessagesDelivered.bind(this), // onMessagesRecalled: this.bindOnMessagesRecalled.bind(this), onMessagesRecalledInfo: this.onMessagesRecalledInfo.bind(this), onMessageContentChanged: this.bindOnMessageContentChanged.bind(this), onMessagePinChanged: this.bindOnMessagePinChanged.bind(this), }; this._client.addListener(gListener); } unInit() { this.reset(); uilog.log('MessageCacheManager:unInit'); if (gListener) { this._client.removeListener(gListener); gListener = undefined; } } reset() { uilog.log('MessageCacheManager:reset'); this._userListener.clear(); this._sendList.clear(); this._downloadList.clear(); } addListener(key: string, listener: MessageManagerListener) { uilog.log('MessageCacheManager:addListener', key); this._userListener.set(key, listener); } removeListener(key: string) { uilog.log('MessageCacheManager:removeListener'); this._userListener.delete(key); } emitSendMessageChanged(msg: ChatMessage) { this._userListener.forEach((v) => { v.onSendMessageChanged?.(msg); }); } emitSendMessageProgressChanged(msg: ChatMessage) { this._userListener.forEach((v) => { v.onSendMessageProgressChanged?.(msg); }); } emitSendMessageBefore(msg: ChatMessage) { this._userListener.forEach((v) => { v.onSendMessageBefore?.(msg); }); } emitRecallMessageBefore(msg: ChatMessage) { this._userListener.forEach((v) => { v.onRecallMessageBefore?.(msg); }); } emitRecallMessageChanged(params: { isOk: boolean; orgMsg?: ChatMessage; tipMsg?: ChatMessage; }) { this._userListener.forEach((v) => { v.onRecallMessageResult?.(params); }); } emitRecvMessageStateChanged(msg: ChatMessage) { this._userListener.forEach((v) => { v.onRecvMessageStatusChanged?.(msg); }); } emitAttachmentChanged(msg: ChatMessage) { this._userListener.forEach((v) => { v.onMessageAttachmentChanged?.(msg); }); } emitAttachmentProgressChanged(msg: ChatMessage) { this._userListener.forEach((v) => { v.onMessageAttachmentProgressChanged?.(msg); }); } emitConversationUnreadCountChanged(): void { this._client.client.chatManager .getUnreadCount() .then((count) => { this._userListener.forEach((v) => { v.onAllConversationUnreadCountChanged?.(count); }); }) .catch(); } emitTipMessage(msg: ChatMessage): void { this._userListener.forEach((v) => { v.onAddTipMessage?.(msg); }); } bindOnMessagesReceived(messages: Array) { asyncTask(this.emitConversationUnreadCountChanged.bind(this)); messages.forEach((msg) => { this.parseUrlPreview(msg, false, (newMsg) => { this._userListener.forEach((v) => { v.onRecvMessage?.(newMsg); }); }); }); } bindOnMessagesRead(messages: Array) { messages.forEach((msg) => { this._userListener.forEach((v) => { v.onRecvMessageStatusChanged?.(msg); }); }); } bindOnGroupMessageRead(_groupMessageAcks: Array): void { // todo: get message for group // groupMessageAcks.forEach((ack) => { // this._userListener.forEach((v) => { // v.onRecvMessageStatusChanged?.(ack.message); // }); // }); } bindOnMessagesDelivered(messages: Array): void { messages.forEach((msg) => { this._userListener.forEach((v) => { v.onRecvMessageStatusChanged?.(msg); }); }); } // bindOnMessagesRecalled(messages: Array): void { // messages.forEach((msg) => { // const tipMsg = this.createRecallMessageTip(msg); // this._client.insertMessage({ // message: tipMsg, // onResult: () => { // this._userListener.forEach((v) => { // v.onRecvRecallMessage?.(msg, tipMsg); // }); // }, // }); // }); // } onMessagesRecalledInfo(messages: Array): void { messages.forEach((info) => { const tipMsg = this.createRecallMessageTip(info.recalledMessage!); this._client.insertMessage({ message: tipMsg, onResult: () => { this._userListener.forEach((v) => { v.onRecvRecallMessage?.(info.recalledMessage!, tipMsg); }); }, }); }); } bindOnMessageContentChanged( message: ChatMessage, lastModifyOperatorId: string, _lastModifyTime: number ): void { this._userListener.forEach((v) => { v.onRecvMessageContentChanged?.(message, lastModifyOperatorId); }); } bindOnMessagePinChanged(params: { messageId: string; convId: string; pinOperation: number; pinInfo: ChatMessagePinInfo; }) { this._client .getMessage({ messageId: params.messageId }) .then((msg) => { if (msg) { this._userListener.forEach((v) => { v.onPinMessageChanged?.(msg, params.pinOperation); }); } else { // todo: fetch pin message list. } }) .catch((e) => { uilog.warn('bindOnMessagePinChanged', e); }); } setCurrentConv(conv?: ConversationModel): void { this._client.setCurrentConversation({ conv }); } getCurrentConv(): ConversationModel | undefined { return this._client.getCurrentConversation(); } sendMessageReadAck(params: { message: ChatMessage }): void { this._client.sendMessageReadAck({ message: params.message, onResult: (result) => { if (result.isOk === true) { const hasReadAck = params.message.hasReadAck; if (hasReadAck !== true) { const tmp = { ...params.message, hasReadAck: true } as ChatMessage; this.emitRecvMessageStateChanged(tmp); } } }, }); } setMessageRead(params: { convId: string; convType: ChatConversationType; message: ChatMessage; }): void { this._client.setMessageRead({ convId: params.convId, convType: params.convType, msgId: params.message.msgId, onResult: (result) => { if (result.isOk === true) { const hasRead = params.message.hasRead; if (hasRead !== true) { const tmp = { ...params.message, hasRead: true } as ChatMessage; this.emitRecvMessageStateChanged(tmp); } } }, }); } async sendMessage(msg: ChatMessage): Promise { const callback: ChatMessageStatusCallback = { onSuccess: (message) => { const isExisted = this._sendList.get(message.localMsgId); if (isExisted) { this.parseUrlPreview(message, false, (newMsg) => { this.emitSendMessageChanged(newMsg); }); this._sendList.delete(message.localMsgId); } }, onProgress: (localMsgId, progress) => { const isExisted = this._sendList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; const p = { [gMessageAttributeFileProgress]: progress }; msg.attributes = { ...msg.attributes, ...p }; this.emitSendMessageProgressChanged(msg); } }, onError: (localMsgId, _error) => { const isExisted = this._sendList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; msg.status = ChatMessageStatus.FAIL; this.emitSendMessageChanged(msg); this._sendList.delete(localMsgId); } }, }; this._sendList.set(msg.localMsgId, { msg }); this.emitSendMessageBefore(msg); this._client.sendMessage({ message: msg, callback: callback }); } async resendMessage(msg: ChatMessage): Promise { const callback: ChatMessageStatusCallback = { onSuccess: (message) => { const isExisted = this._sendList.get(message.localMsgId); if (isExisted) { this.emitSendMessageChanged(message); this._sendList.delete(message.localMsgId); } }, onError: (localMsgId, e) => { this._client.sendError({ error: new UIKitError({ code: ErrorCode.chat_uikit, desc: e.toString(), }), from: 'resendMessage', }); const isExisted = this._sendList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; msg.status = ChatMessageStatus.FAIL; this.emitSendMessageChanged(msg); this._sendList.delete(localMsgId); } }, }; this._sendList.set(msg.localMsgId, { msg: msg }); this.emitSendMessageBefore(msg); this._client.resendMessage({ message: msg, callback: callback }); } createRecallMessageTip(msg: ChatMessage): ChatMessage { const userInfo = userInfoFromMessage(msg); const tipMsg = ChatMessage.createCustomMessage( msg.conversationId, gCustomMessageRecallEventType, msg.chatType, { params: { text: '_uikit_msg_tip_recall', self: this._client.userId ?? '', from: msg.from, fromName: userInfo?.remark ?? userInfo?.userName ?? msg.from, type: 'system', }, } ); tipMsg.status = ChatMessageStatus.SUCCESS; return tipMsg; } async recallMessage(msg: ChatMessage): Promise { this.emitRecallMessageBefore(msg); const currentTimestamp = getCurTs(); if (msg.localTime + this._recallTimeout < currentTimestamp) { this.emitRecallMessageChanged({ isOk: false }); this._client.sendError({ error: new UIKitError({ code: ErrorCode.msg_recall_error, }), }); return; } this._client.recallMessage({ message: msg, onResult: (value) => { if (value.isOk === true) { const tipMsg = this.createRecallMessageTip(msg); this._client.insertMessage({ message: tipMsg, onResult: (result) => { this.emitRecallMessageChanged({ isOk: result.isOk, orgMsg: msg, tipMsg: tipMsg, }); }, }); } else { this.emitRecallMessageChanged({ isOk: false }); this._client.sendError({ error: value.error! }); } }, }); } async downloadAttachment(msg: ChatMessage) { if ( msg.body.type !== ChatMessageType.IMAGE && msg.body.type !== ChatMessageType.VIDEO && msg.body.type !== ChatMessageType.FILE && msg.body.type !== ChatMessageType.VOICE ) { return; } const callback: ChatMessageStatusCallback = { onSuccess: (message) => { const isExisted = this._downloadList.get(message.localMsgId); if (isExisted) { this.emitAttachmentChanged(message); this._downloadList.delete(message.localMsgId); } }, onProgress: (localMsgId, progress) => { const isExisted = this._downloadList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; const p = { [gMessageAttributeFileProgress]: progress }; msg.attributes = { ...msg.attributes, ...p }; this.emitAttachmentProgressChanged(msg); } }, onError: (localMsgId, error) => { if (error) { uilog.warn('downloadAttachment:error', error); } const isExisted = this._downloadList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; msg.body = { ...msg.body, fileStatus: ChatDownloadStatus.FAILED, } as ChatFileMessageBody; this.emitAttachmentChanged(msg); this._downloadList.delete(localMsgId); } }, }; this._downloadList.set(msg.localMsgId, { msg }); this._client.downloadMessageAttachment({ message: msg, callback: callback, }); } async downloadAttachmentForThread(msg: ChatMessage) { if ( msg.body.type !== ChatMessageType.IMAGE && msg.body.type !== ChatMessageType.VIDEO && msg.body.type !== ChatMessageType.FILE && msg.body.type !== ChatMessageType.VOICE ) { return; } const callback: ChatMessageStatusCallback = { onSuccess: (message) => { const isExisted = this._downloadList.get(message.localMsgId); if (isExisted) { this.emitAttachmentChanged(message); this._downloadList.delete(message.localMsgId); } }, onProgress: (localMsgId, progress) => { const isExisted = this._downloadList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; const p = { [gMessageAttributeFileProgress]: progress }; msg.attributes = { ...msg.attributes, ...p }; this.emitAttachmentProgressChanged(msg); } }, onError: (localMsgId, error) => { if (error) { uilog.warn('downloadAttachment:error', error); } const isExisted = this._downloadList.get(localMsgId); if (isExisted) { const msg = { ...isExisted.msg } as ChatMessage; msg.body = { ...msg.body, fileStatus: ChatDownloadStatus.FAILED, } as ChatFileMessageBody; this.emitAttachmentChanged(msg); this._downloadList.delete(localMsgId); } }, }; this._downloadList.set(msg.localMsgId, { msg }); this._client.downloadMessageAttachmentForThread({ message: msg, callback: callback, }); } loadHistoryMessage(params: { convId: string; convType: ChatConversationType; startMsgId: string; loadCount: number; direction?: ChatSearchDirection; isChatThread?: boolean; }): Promise { const { convId, convType, startMsgId, loadCount, direction, isChatThread } = params; return this._client.getHistoryMessage({ convId, convType, startMsgId, direction: direction ?? ChatSearchDirection.UP, loadCount: loadCount, isChatThread: isChatThread, }); } setRecallMessageTimeout(recallTimeout?: number): void { if (recallTimeout) { this._recallTimeout = recallTimeout; } } parseUrlPreview( msg: ChatMessage, isForce: boolean, onResult: (msg: ChatMessage) => void ): void { if (msg.body.type !== ChatMessageType.TXT) { onResult(msg); return; } const body = msg.body as ChatTextMessageBody; if ( msg.attributes?.[gMessageAttributeUrlPreview] !== undefined && isForce !== true ) { onResult(msg); return; } const urls = UseUrlPreview.getUrlListFromText(body.content); if (!urls || urls.length === 0 || urls.length > 1) { onResult(msg); return; } UseUrlPreview.fetchUrlPreview(urls[0]!).then((data) => { if (data) { const newMsg = { ...msg } as ChatMessage; newMsg.attributes = { ...msg.attributes, [gMessageAttributeUrlPreview]: data, }; onResult(newMsg); this._client.updateMessage({ message: newMsg, }); } else { msg.attributes = { ...msg.attributes, [gMessageAttributeUrlPreview]: { url: urls[0]!, title: undefined }, }; onResult(msg); this._client.updateMessage({ message: msg, }); } }); } addTipMessage(params: { convId: string; convType: ChatMessageChatType; tipType: string; kvs: Record; isChatThread?: boolean; onResult?: (isOk: boolean) => void; }): void { const tipMsg = ChatMessage.createCustomMessage( params.convId, params.tipType, params.convType, { params: { ...params.kvs, type: 'system', }, isChatThread: params.isChatThread, } ); tipMsg.status = ChatMessageStatus.SUCCESS; this._client.insertMessage({ message: tipMsg, onResult: (result) => { this.emitTipMessage(tipMsg); params.onResult?.(result.isOk); }, }); } }