import type { DeepWritable, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { isVideoConfMessage } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import { CurrentEditingMessage } from './CurrentEditingMessage'; import { UserAction } from './UserAction'; import type { ChatAPI, ComposerAPI, DataAPI } from './chats/ChatAPI'; import { createDataAPI } from './chats/data'; import { processMessageEditing } from './chats/flows/processMessageEditing'; import { processMessageUploads } from './chats/flows/processMessageUploads'; import { processSetReaction } from './chats/flows/processSetReaction'; import { processSlashCommand } from './chats/flows/processSlashCommand'; import { processTooLongMessage } from './chats/flows/processTooLongMessage'; import { replyBroadcast } from './chats/flows/replyBroadcast'; import { requestMessageDeletion } from './chats/flows/requestMessageDeletion'; import { sendMessage } from './chats/flows/sendMessage'; import { uploadFiles } from './chats/flows/uploadFiles'; import { ReadStateManager } from './chats/readStateManager'; import { setHighlightMessage } from '../views/room/MessageList/providers/messageHighlightSubscription'; export class ChatMessages implements ChatAPI { public uid: string | undefined; public tmid?: IMessage['_id']; public composer: ComposerAPI | undefined; public setComposerAPI = (composer?: ComposerAPI): void => { this.composer?.release(); this.composer = composer; }; public data: DataAPI; public currentEditingMessage: CurrentEditingMessage; public readStateManager: ReadStateManager; public ActionManager: any; public emojiPicker: { open(el: Element, cb: (emoji: string) => void): void; close(): void; }; public action: { start(action: 'typing'): Promise | void; stop(action: 'typing' | 'recording' | 'uploading' | 'playing'): Promise | void; performContinuously(action: 'recording' | 'uploading' | 'playing'): Promise | void; }; public messageEditing: ChatAPI['messageEditing'] = { toPreviousMessage: async () => { if (!this.composer) { return; } const mid = this.currentEditingMessage.getMID(); if (!mid) { let lastMessage = await this.data.findPreviousOwnMessage(); // Videoconf messages should not be edited if (lastMessage && isVideoConfMessage(lastMessage)) { lastMessage = await this.data.findPreviousOwnMessage(lastMessage); } if (lastMessage) { await this.data.saveDraft(undefined, this.composer.text); await this.messageEditing.editMessage(lastMessage); } return; } const currentMessage = await this.data.findMessageByID(mid); let previousMessage = currentMessage ? await this.data.findPreviousOwnMessage(currentMessage) : undefined; // Videoconf messages should not be edited if (previousMessage && isVideoConfMessage(previousMessage)) { previousMessage = await this.data.findPreviousOwnMessage(previousMessage); } if (previousMessage) { await this.messageEditing.editMessage(previousMessage); return; } await this.currentEditingMessage.cancel(); await this.currentEditingMessage.stop(); }, toNextMessage: async () => { const mid = this.currentEditingMessage.getMID(); if (!this.composer || !mid) { return; } const currentMessage = await this.data.findMessageByID(mid); let nextMessage = currentMessage ? await this.data.findNextOwnMessage(currentMessage) : undefined; // Videoconf messages should not be edited if (nextMessage && isVideoConfMessage(nextMessage)) { nextMessage = await this.data.findNextOwnMessage(nextMessage); } if (nextMessage) { await this.messageEditing.editMessage(nextMessage, { cursorAtStart: true }); return; } await this.currentEditingMessage.cancel(); await this.currentEditingMessage.stop(); }, editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => { this.composer?.uploads.clear(); const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg; await this.currentEditingMessage.stop(); if (!this.composer || !(await this.data.canUpdateMessage(message))) { return; } this.currentEditingMessage.setMID(message._id); setHighlightMessage(message._id); this.composer.setEditingMode(true); this.composer.setText(text); cursorAtStart && this.composer.setCursorToStart(); !cursorAtStart && this.composer.setCursorToEnd(); this.composer.focus(); }, }; public flows: DeepWritable; public constructor(params: { rid: IRoom['_id']; tmid?: IMessage['_id']; uid: IUser['_id'] | undefined; actionManager: IActionManager }) { const { rid, tmid } = params; this.tmid = tmid; this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); this.ActionManager = params.actionManager; this.currentEditingMessage = new CurrentEditingMessage(this); const unimplemented = () => { throw new Error('Flow is not implemented'); }; this.readStateManager = new ReadStateManager(rid); this.emojiPicker = { open: unimplemented, close: unimplemented, }; this.action = { start: async (action: 'typing') => { UserAction.start(params.rid, `user-${action}`, { tmid: params.tmid }); }, performContinuously: async (action: 'recording' | 'uploading' | 'playing') => { UserAction.performContinuously(params.rid, `user-${action}`, { tmid: params.tmid }); }, stop: async (action: 'typing' | 'recording' | 'uploading' | 'playing') => { UserAction.stop(params.rid, `user-${action}`, { tmid: params.tmid }); }, }; this.flows = { uploadFiles: uploadFiles.bind(null, this), sendMessage: sendMessage.bind(this, this), processSlashCommand: processSlashCommand.bind(null, this), processTooLongMessage: processTooLongMessage.bind(null, this), processMessageEditing: processMessageEditing.bind(null, this), processMessageUploads: processMessageUploads.bind(null, this), processSetReaction: processSetReaction.bind(null, this), requestMessageDeletion: requestMessageDeletion.bind(this, this), replyBroadcast: replyBroadcast.bind(null, this), }; } public async release() { if (this.currentEditingMessage.getMID()) { if (!this.tmid) { await this.currentEditingMessage.cancel(); await this.currentEditingMessage.stop(); } this.composer?.clear(); } } }