diff --git a/apps/meteor/app/emoji/client/lib/EmojiPicker.js b/apps/meteor/app/emoji/client/lib/EmojiPicker.js index 8c5f883dfacf..c318f0e3c8de 100644 --- a/apps/meteor/app/emoji/client/lib/EmojiPicker.js +++ b/apps/meteor/app/emoji/client/lib/EmojiPicker.js @@ -122,10 +122,9 @@ export const EmojiPicker = { this.source.focus(); }, pickEmoji(emoji) { - this.pickCallback(emoji); - this.close(); this.addRecent(emoji); + this.pickCallback(emoji); }, addRecent(_emoji) { const pos = this.recent.indexOf(_emoji); diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index ab5638d32037..d2d6e1285c78 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -84,7 +84,7 @@ &__container { display: flex; - padding: 0.75rem 0; + padding: 0.5rem 0.75rem; cursor: text; @@ -305,6 +305,7 @@ border-width: 0; border-top-width: 1px; flex-wrap: wrap; + justify-content: space-between; } & [data-desktop] { @@ -318,7 +319,7 @@ &__textarea { flex: 1 0 100%; - margin-bottom: 10px; + margin-block-end: 8px; order: 1; } @@ -357,6 +358,10 @@ font-size: 20px; order: 5; } + + [role='toolbar'] { + order: 6; + } } .js-message-action { diff --git a/apps/meteor/app/threads/client/flextab/thread.ts b/apps/meteor/app/threads/client/flextab/thread.ts index c420fe494501..50f86148c00e 100644 --- a/apps/meteor/app/threads/client/flextab/thread.ts +++ b/apps/meteor/app/threads/client/flextab/thread.ts @@ -140,16 +140,7 @@ Template.thread.helpers({ subscription, rid, tmid, - onSend: async ( - _event: Event, - { - value: text, - tshow, - }: { - value: string; - tshow?: boolean; - }, - ) => { + onSend: async ({ value: text, tshow }: { value: string; tshow?: boolean }) => { instance.sendToBottom(); if (alsoSendPreferenceState === 'default') { instance.state.set('sendToChannel', false); diff --git a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts index b093f670122a..eb47ecfcdf9b 100644 --- a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts +++ b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts @@ -1,5 +1,6 @@ import { Session } from 'meteor/session'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { Rooms } from '../../../models/client'; import { messageBox } from '../../../ui-utils/client'; @@ -14,7 +15,7 @@ const APP_GROUP = 'Create_new'; export const onAdded = (button: IUIActionButton): void => // eslint-disable-next-line no-void - void messageBox.actions.add(APP_GROUP, t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)), { + void messageBox.actions.add(APP_GROUP, t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)) as TranslationKey, { id: getIdForActionButton(button), // icon: button.icon || '', condition() { diff --git a/apps/meteor/app/ui-message/client/index.ts b/apps/meteor/app/ui-message/client/index.ts index 11a1de06ca63..ea932b690880 100644 --- a/apps/meteor/app/ui-message/client/index.ts +++ b/apps/meteor/app/ui-message/client/index.ts @@ -1,5 +1,5 @@ import './message'; -import './messageBox/messageBox.ts'; +// import './messageBox/messageBox.ts'; import './popup/customMessagePopups'; import './popup/messagePopup'; import './popup/messagePopupChannel'; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts new file mode 100644 index 000000000000..ce30c5156b7e --- /dev/null +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -0,0 +1,194 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import $ from 'jquery'; + +import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; +import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; +import './messageBoxActions'; +import './messageBoxReplyPreview.ts'; +import './userActionIndicator.ts'; + +export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string): ComposerAPI => { + const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => { + $(input).trigger(evt); + + const event = new Event(evt, { bubbles: true }); + // TODO: Remove this hack for react to trigger onChange + const tracker = (input as any)._valueTracker; + if (tracker) { + tracker.setValue(new Date().toString()); + } + input.dispatchEvent(event); + }; + + const emitter = new Emitter<{ quotedMessagesUpdate: void; editing: void; recording: void }>(); + + let _quotedMessages: IMessage[] = []; + + const persist = withDebouncing({ wait: 1000 })(() => { + if (input.value) { + Meteor._localStorage.setItem(storageID, input.value); + return; + } + + Meteor._localStorage.removeItem(storageID); + }); + + const notifyQuotedMessagesUpdate = (): void => { + emitter.emit('quotedMessagesUpdate'); + }; + + input.addEventListener('input', persist); + + const release = (): void => { + input.removeEventListener('input', persist); + }; + + const setText = ( + text: string, + { + selection, + }: { + selection?: + | { readonly start?: number; readonly end?: number } + | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); + } = {}, + ): void => { + focus(); + + const { selectionStart, selectionEnd } = input; + const textAreaTxt = input.value; + + if (typeof selection === 'function') { + selection = selection({ start: selectionStart, end: selectionEnd }); + } + + if (selection) { + if (!document.execCommand || !document.execCommand('insertText', false, text)) { + input.value = textAreaTxt.substring(0, selectionStart) + text + textAreaTxt.substring(selectionStart); + focus(); + } + input.setSelectionRange(selection.start ?? 0, selection.end ?? text.length); + } + + if (!selection) { + input.value = text; + } + + persist(); + + triggerEvent(input, 'input'); + triggerEvent(input, 'change'); + + focus(); + }; + + const insertText = (text: string): void => { + setText(text, { + selection: ({ start, end }) => ({ + start: start + text.length, + end: end + text.length, + }), + }); + }; + + const clear = (): void => { + setText(''); + }; + + const focus = (): void => { + input.focus(); + }; + + const replyWith = async (text: string): Promise => { + if (input) { + input.value = text; + input.focus(); + } + }; + + const quoteMessage = async (message: IMessage): Promise => { + _quotedMessages = [..._quotedMessages.filter((_message) => _message._id !== message._id), message]; + notifyQuotedMessagesUpdate(); + input.focus(); + }; + + const dismissQuotedMessage = async (mid: IMessage['_id']): Promise => { + _quotedMessages = _quotedMessages.filter((message) => message._id !== mid); + notifyQuotedMessagesUpdate(); + }; + + const dismissAllQuotedMessages = async (): Promise => { + _quotedMessages = []; + notifyQuotedMessagesUpdate(); + }; + + const quotedMessages = { + get: () => _quotedMessages, + subscribe: (callback: () => void) => emitter.on('quotedMessagesUpdate', callback), + }; + + const [editing, setEditing] = (() => { + let editing = false; + + return [ + { + get: () => editing, + subscribe: (callback: () => void) => emitter.on('editing', callback), + }, + (value: boolean) => { + editing = value; + emitter.emit('editing'); + }, + ]; + })(); + + const [recording, setRecordingMode] = (() => { + let recording = false; + + return [ + { + get: () => recording, + subscribe: (callback: () => void) => emitter.on('recording', callback), + }, + (value: boolean) => { + recording = value; + emitter.emit('recording'); + }, + ]; + })(); + + const setEditingMode = (editing: boolean): void => { + setEditing(editing); + }; + + setText(Meteor._localStorage.getItem(storageID) ?? ''); + + return { + release, + get text(): string { + return input.value; + }, + get selection(): { start: number; end: number } { + return { + start: input.selectionStart, + end: input.selectionEnd, + }; + }, + + editing, + setEditingMode, + recording, + setRecordingMode, + insertText, + setText, + clear, + focus, + replyWith, + quoteMessage, + dismissQuotedMessage, + dismissAllQuotedMessages, + quotedMessages, + }; +}; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 1fc8a3f7575b..3e96eea4c670 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -1,164 +1,33 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { ReactiveDict } from 'meteor/reactive-dict'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; -import { Tracker } from 'meteor/tracker'; -import moment from 'moment'; +// import { Meteor } from 'meteor/meteor'; +// import { ReactiveVar } from 'meteor/reactive-var'; +// import { ReactiveDict } from 'meteor/reactive-dict'; +// import { Session } from 'meteor/session'; +// import { Template } from 'meteor/templating'; +// import { Tracker } from 'meteor/tracker'; import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; +// import { isRoomFederated } from '@rocket.chat/core-typings'; import type { Blaze } from 'meteor/blaze'; import type { ContextType } from 'react'; -import { Emitter } from '@rocket.chat/emitter'; -import $ from 'jquery'; - -import { setupAutogrow } from './messageBoxAutogrow'; -import { formattingButtons, applyFormatting } from './messageBoxFormatting'; -import { EmojiPicker } from '../../../emoji/client'; -import { Users, ChatRoom } from '../../../models/client'; -import { settings } from '../../../settings/client'; -import { UserAction, USER_ACTIVITIES, KonchatNotification } from '../../../ui/client'; -import { messageBox, popover } from '../../../ui-utils/client'; -import { t, getUserPreference } from '../../../utils/client'; -import { getImageExtensionFromMime } from '../../../../lib/getImageExtensionFromMime'; -import { keyCodes } from '../../../../client/lib/utils/keyCodes'; -import { isRTL } from '../../../../client/lib/utils/isRTL'; -import { call } from '../../../../client/lib/utils/call'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +// import $ from 'jquery'; + +// import { setupAutogrow } from './messageBoxAutogrow'; +// import { formattingButtons } from './messageBoxFormatting'; +// import { Users, ChatRoom } from '../../../models/client'; +// import { settings } from '../../../settings/client'; +// import { UserAction, USER_ACTIVITIES } from '../../../ui/client'; +// import { messageBox } from '../../../ui-utils/client'; +// import { getUserPreference } from '../../../utils/client'; +// import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; -import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; -import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import './messageBoxActions'; import './messageBoxReplyPreview.ts'; -import './userActionIndicator.ts'; -import './messageBox.html'; - -const createComposerAPI = (input: HTMLTextAreaElement, storageID: string): ComposerAPI => { - const emitter = new Emitter<{ quotedMessagesUpdate: void }>(); - - let _quotedMessages: IMessage[] = []; - - const persist = withDebouncing({ wait: 1000 })(() => { - if (input.value) { - Meteor._localStorage.setItem(storageID, input.value); - return; - } - - Meteor._localStorage.removeItem(storageID); - }); - - const notifyQuotedMessagesUpdate = (): void => { - emitter.emit('quotedMessagesUpdate'); - }; - - input.value = Meteor._localStorage.getItem(storageID) ?? ''; - input.addEventListener('input', persist); - - const release = (): void => { - input.removeEventListener('input', persist); - }; - - const setText = ( - text: string, - { - selection, - }: { - selection?: - | { readonly start?: number; readonly end?: number } - | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); - } = {}, - ): void => { - input.value = text; - - if (typeof selection === 'function') { - selection = selection({ start: input.selectionStart, end: input.selectionEnd }); - } - - if (selection) { - input.setSelectionRange(selection.start ?? 0, selection.end ?? text.length); - } - - persist(); - $(input).trigger('change').trigger('input'); - }; - - const clear = (): void => { - setText(''); - }; - - const focus = (): void => { - input.focus(); - }; - - const replyWith = async (text: string): Promise => { - if (input) { - input.value = text; - input.focus(); - } - }; - - const quoteMessage = async (message: IMessage): Promise => { - _quotedMessages = [..._quotedMessages.filter((_message) => _message._id !== message._id), message]; - notifyQuotedMessagesUpdate(); - input.focus(); - }; - - const dismissQuotedMessage = async (mid: IMessage['_id']): Promise => { - _quotedMessages = _quotedMessages.filter((message) => message._id !== mid); - notifyQuotedMessagesUpdate(); - }; - - const dismissAllQuotedMessages = async (): Promise => { - _quotedMessages = []; - notifyQuotedMessagesUpdate(); - }; - - const quotedMessages = { - get: () => _quotedMessages, - subscribe: (callback: () => void) => emitter.on('quotedMessagesUpdate', callback), - }; - - const setEditingMode = (editing: boolean): void => { - if (editing) { - input.parentElement?.classList.add('editing'); - } else { - input.parentElement?.classList.remove('editing'); - } - }; - - return { - release, - get text(): string { - return input.value; - }, - get selection(): { start: number; end: number } { - return { - start: input.selectionStart, - end: input.selectionEnd, - }; - }, - setText, - clear, - focus, - replyWith, - quoteMessage, - dismissQuotedMessage, - dismissAllQuotedMessages, - quotedMessages, - setEditingMode, - }; -}; +// import './messageBox.html'; +// import { createComposerAPI } from './createComposerAPI'; export type MessageBoxTemplateInstance = Blaze.TemplateInstance<{ rid: IRoom['_id']; tmid?: IMessage['_id']; - onSend?: ( - event: Event, - params: { - value: string; - tshow?: boolean; - }, - ) => Promise; + onSend?: (params: { value: string; tshow?: boolean }) => Promise; onResize?: () => void; onEscape?: () => void; onNavigateToPreviousMessage?: () => void; @@ -195,616 +64,572 @@ export type MessageBoxTemplateInstance = Blaze.TemplateInstance<{ sendIconDisabled: ReactiveVar; }; -let lastFocusedInput: HTMLTextAreaElement | undefined = undefined; +const lastFocusedInput: HTMLTextAreaElement | undefined = undefined; export const refocusComposer = () => { (lastFocusedInput ?? document.querySelector('.js-input-message'))?.focus(); }; -Template.messageBox.onCreated(function (this: MessageBoxTemplateInstance) { - this.state = new ReactiveDict(); - this.popupConfig = new ReactiveVar(null); - this.replyMessageData = new ReactiveVar(this.data.chatContext?.composer?.quotedMessages.get() ?? []); - this.isMicrophoneDenied = new ReactiveVar(true); - this.isSendIconVisible = new ReactiveVar(false); - - this.set = (value) => { - if (!this.input) { - return; - } - - this.input.value = value; - $(this.input).trigger('change').trigger('input'); - }; - - this.insertNewLine = () => { - const { input, autogrow } = this; - if (!input) { - return; - } - - if (input.selectionStart || input.selectionStart === 0) { - const newPosition = input.selectionStart + 1; - const before = input.value.substring(0, input.selectionStart); - const after = input.value.substring(input.selectionEnd, input.value.length); - input.value = `${before}\n${after}`; - input.selectionStart = newPosition; - input.selectionEnd = newPosition; - } else { - input.value += '\n'; - } - $(input).trigger('change').trigger('input'); - - input.blur(); - input.focus(); - autogrow?.update(); - }; - - this.send = (event) => { - const { input } = this; - - if (!input) { - return; - } - - const { - autogrow, - data: { onSend, tshow }, - } = this; - const { value } = input; - this.set(''); - - UserAction.stop(this.data.rid, USER_ACTIVITIES.USER_TYPING, { tmid: this.data.tmid }); - - onSend?.call(this.data, event, { value, tshow }).then(() => { - autogrow?.update(); - input.focus(); - }); - }; -}); - -Template.messageBox.onRendered(function (this: MessageBoxTemplateInstance) { - let inputSetup = false; - - this.autorun(() => { - const { rid, subscription } = Template.currentData() as MessageBoxTemplateInstance['data']; - const room = Session.get(`roomData${rid}`); - - if (!inputSetup) { - const $input = $(this.find('.js-input-message')); - this.source = $input[0] as HTMLTextAreaElement | undefined; - if (this.source) { - inputSetup = true; - } - } - - if (!room) { - return this.state.set({ - room: false, - isBlockedOrBlocker: false, - mustJoinWithCode: false, - }); - } - - const isBlocked = room && room.t === 'd' && subscription && subscription.blocked; - const isBlocker = room && room.t === 'd' && subscription && subscription.blocker; - const isBlockedOrBlocker = isBlocked || isBlocker; - - const mustJoinWithCode = !subscription && room.joinCodeRequired; - - return this.state.set({ - room: false, - isBlockedOrBlocker, - mustJoinWithCode, - }); - }); - - this.autorun(() => { - const { rid, tmid, onResize, chatContext } = Template.currentData() as MessageBoxTemplateInstance['data']; - - let unsubscribeToQuotedMessages: (() => void) | undefined; - - Tracker.afterFlush(() => { - const input = this.find('.js-input-message') as HTMLTextAreaElement; - - if (this.input === input) { - return; - } - - this.input = input; - - if (chatContext) { - const storageID = `messagebox_${rid}${tmid ? `-${tmid}` : ''}`; - const composer = createComposerAPI(input, storageID); - chatContext.setComposerAPI(composer); - this.set = composer.setText; - } - - setTimeout(() => { - if (window.matchMedia('screen and (min-device-width: 500px)').matches) { - input.focus(); - } - }, 200); - - unsubscribeToQuotedMessages?.(); - - unsubscribeToQuotedMessages = chatContext?.composer?.quotedMessages.subscribe(() => { - this.replyMessageData.set(chatContext?.composer?.quotedMessages.get() ?? []); - }); - - if (input && rid) { - this.popupConfig.set({ - rid, - tmid, - getInput: () => input, - }); - } else { - this.popupConfig.set(null); - } - - if (this.autogrow) { - this.autogrow.destroy(); - this.autogrow = null; - } - - if (!input) { - return; - } - - const shadow = this.find('.js-input-message-shadow'); - this.autogrow = setupAutogrow(input, shadow, onResize); - }); - }); -}); - -Template.messageBox.onDestroyed(function (this: MessageBoxTemplateInstance) { - UserAction.cancel(this.data.rid); - - if (lastFocusedInput === this.input) { - lastFocusedInput = undefined; - } - - if (!this.autogrow) { - return; - } - - this.autogrow.destroy(); -}); - -Template.messageBox.helpers({ - isAnonymousOrMustJoinWithCode() { - const instance = Template.instance() as MessageBoxTemplateInstance; - const { rid } = Template.currentData() as MessageBoxTemplateInstance['data']; - if (!rid) { - return false; - } - const isAnonymous = !Meteor.userId(); - return isAnonymous || instance.state.get('mustJoinWithCode'); - }, - isWritable() { - const { rid, subscription } = Template.currentData() as MessageBoxTemplateInstance['data']; - if (!rid) { - return true; - } - - const isBlockedOrBlocker = (Template.instance() as MessageBoxTemplateInstance).state.get('isBlockedOrBlocker'); - - if (isBlockedOrBlocker) { - return false; - } - - if (subscription?.onHold) { - return false; - } - - const isReadOnly = roomCoordinator.readOnly(rid, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })); - const isArchived = roomCoordinator.archived(rid) || (subscription && subscription.t === 'd' && subscription.archived); - - return !isReadOnly && !isArchived; - }, - popupConfig() { - return (Template.instance() as MessageBoxTemplateInstance).popupConfig.get(); - }, - input() { - return (Template.instance() as MessageBoxTemplateInstance).input; - }, - replyMessageData() { - return (Template.instance() as MessageBoxTemplateInstance).replyMessageData.get(); - }, - onDismissReply() { - const { chatContext } = (Template.instance() as MessageBoxTemplateInstance).data; - return (mid: IMessage['_id']) => chatContext?.composer?.dismissQuotedMessage(mid); - }, - isEmojiEnabled() { - return getUserPreference(Meteor.userId(), 'useEmojis'); - }, - maxMessageLength() { - return settings.get('Message_AllowConvertLongMessagesToAttachment') ? null : settings.get('Message_MaxAllowedSize'); - }, - isSendIconVisible() { - return (Template.instance() as MessageBoxTemplateInstance).isSendIconVisible.get(); - }, - canSend() { - const { rid } = Template.currentData(); - if (!rid) { - return true; - } - - return roomCoordinator.verifyCanSendMessage(rid); - }, - actions() { - const actionGroups = messageBox.actions.get(); - - return Object.values(actionGroups).reduce((actions, actionGroup) => [...actions, ...actionGroup], []); - }, - formattingButtons() { - return formattingButtons.filter(({ condition }) => !condition || condition()); - }, - isBlockedOrBlocker() { - return (Template.instance() as MessageBoxTemplateInstance).state.get('isBlockedOrBlocker'); - }, - onHold() { - const { rid, subscription } = Template.currentData(); - return rid && !!subscription?.onHold; - }, - isSubscribed() { - const { subscription } = Template.currentData(); - return !!subscription; - }, - isFederatedRoom() { - const { rid } = Template.currentData(); - - const room = ChatRoom.findOne(rid); - - return room && isRoomFederated(room); - }, -}); - -const handleFormattingShortcut = (event: KeyboardEvent, instance: MessageBoxTemplateInstance) => { - const isMacOS = navigator.platform.indexOf('Mac') !== -1; - const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey); - - if (!isCmdOrCtrlPressed) { - return false; - } - - const key = event.key.toLowerCase(); - - const { pattern } = formattingButtons.filter(({ condition }) => !condition || condition()).find(({ command }) => command === key) || {}; - - if (!pattern) { - return false; - } - - const { input } = instance; - applyFormatting(pattern, input); - return true; -}; - -let sendOnEnter; -let sendOnEnterActive: boolean | undefined; - -Tracker.autorun(() => { - sendOnEnter = getUserPreference(Meteor.userId(), 'sendOnEnter'); - sendOnEnterActive = sendOnEnter == null || sendOnEnter === 'normal' || (sendOnEnter === 'desktop' && Meteor.Device.isDesktop()); -}); - -const handleSubmit = (event: KeyboardEvent, instance: MessageBoxTemplateInstance) => { - const { which: keyCode } = event; - - const isSubmitKey = keyCode === keyCodes.CARRIAGE_RETURN || keyCode === keyCodes.NEW_LINE; - - if (!isSubmitKey) { - return false; - } - - const withModifier = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; - const isSending = (sendOnEnterActive && !withModifier) || (!sendOnEnterActive && withModifier); - - if (isSending) { - instance.send(event); - return true; - } - - instance.insertNewLine(); - return true; -}; - -Template.messageBox.events({ - async 'click .js-join'(event: JQuery.ClickEvent) { - event.stopPropagation(); - event.preventDefault(); - - const joinCodeInput = (Template.instance() as MessageBoxTemplateInstance).find('[name=joinCode]') as HTMLInputElement | undefined; - const joinCode = joinCodeInput?.value; - - await call('joinRoom', this.rid, joinCode); - }, - 'click .js-emoji-picker'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { - event.stopPropagation(); - event.preventDefault(); - - if (!getUserPreference(Meteor.userId(), 'useEmojis')) { - return; - } - - if (EmojiPicker.isOpened()) { - EmojiPicker.close(); - return; - } - - EmojiPicker.open(instance.source, (emoji: string) => { - const emojiValue = `:${emoji}: `; - - const { input } = instance; - - const caretPos = input.selectionStart; - const textAreaTxt = input.value; - - input.focus(); - if (!document.execCommand || !document.execCommand('insertText', false, emojiValue)) { - instance.set(textAreaTxt.substring(0, caretPos) + emojiValue + textAreaTxt.substring(caretPos)); - input.focus(); - } - - input.selectionStart = caretPos + emojiValue.length; - input.selectionEnd = caretPos + emojiValue.length; - }); - }, - 'focus .js-input-message'(event: JQuery.FocusEvent) { - KonchatNotification.removeRoomNotification(this.rid); - lastFocusedInput = event.currentTarget; - }, - 'keydown .js-input-message'( - this: MessageBoxTemplateInstance['data'], - event: JQuery.KeyDownEvent, - instance: MessageBoxTemplateInstance, - ) { - const { originalEvent } = event; - if (!originalEvent) { - throw new Error('Event is not an original event'); - } - - const isEventHandled = handleFormattingShortcut(originalEvent, instance) || handleSubmit(originalEvent, instance); - - if (isEventHandled) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - const { chatContext } = this; - const { currentTarget: input } = event; - - switch (event.key) { - case 'Escape': { - const currentEditing = chatContext?.currentEditing; - - if (currentEditing) { - event.preventDefault(); - event.stopPropagation(); - - currentEditing.reset().then((reset) => { - if (!reset) { - currentEditing?.cancel(); - } - }); - - return; - } - - if (!input.value.trim()) this.onEscape?.(); - return; - } - - case 'ArrowUp': { - if (event.shiftKey) { - return; - } - - if (input.selectionEnd === 0) { - event.preventDefault(); - event.stopPropagation(); - - this.onNavigateToPreviousMessage?.(); - - if (event.altKey) { - input.setSelectionRange(0, 0); - } - } - - return; - } - - case 'ArrowDown': { - if (event.shiftKey) { - return; - } - - if (input.selectionEnd === input.value.length) { - event.preventDefault(); - event.stopPropagation(); - - this.onNavigateToNextMessage?.(); - - if (event.altKey) { - input.setSelectionRange(input.value.length, input.value.length); - } - } - } - } - }, - 'keyup .js-input-message'(this: MessageBoxTemplateInstance['data'], event: JQuery.KeyUpEvent) { - const { rid, tmid } = this; - const { currentTarget: input, which: keyCode } = event; - - if (!Object.values(keyCodes).includes(keyCode)) { - if (input?.value.trim()) { - UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); - } else { - UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); - } - } - }, - 'paste .js-input-message'(event: JQuery.TriggeredEvent, instance: MessageBoxTemplateInstance) { - const originalEvent = event.originalEvent as ClipboardEvent | undefined; - if (!originalEvent) { - throw new Error('Event is not an original event'); - } - - const { autogrow } = instance; - - setTimeout(() => autogrow?.update(), 50); - - if (!originalEvent.clipboardData) { - return; - } - - const items = Array.from(originalEvent.clipboardData.items); - - if (items.some(({ kind, type }) => kind === 'string' && type === 'text/plain')) { - return; - } - - const files = items - .filter((item) => item.kind === 'file' && item.type.indexOf('image/') !== -1) - .map((item) => { - const fileItem = item.getAsFile(); - - if (!fileItem) { - return; - } - - const imageExtension = fileItem ? getImageExtensionFromMime(fileItem.type) : undefined; - - const extension = imageExtension ? `.${imageExtension}` : ''; - - Object.defineProperty(fileItem, 'name', { - writable: true, - value: `Clipboard - ${moment().format(settings.get('Message_TimeAndDateFormat'))}${extension}`, - }); - return fileItem; - }) - .filter((file): file is File => !!file); - - if (files.length) { - event.preventDefault(); - instance.data.onUploadFiles?.(files); - } - }, - 'input .js-input-message'( - this: MessageBoxTemplateInstance['data'], - _event: JQuery.TriggeredEvent, - instance: MessageBoxTemplateInstance, - ) { - const { input } = instance; - if (!input) { - return; - } - - instance.isSendIconVisible.set(!!input.value); - - if (input.value.length > 0) { - input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; - } - }, - 'propertychange .js-input-message'( - this: MessageBoxTemplateInstance['data'], - event: JQuery.TriggeredEvent, - instance: MessageBoxTemplateInstance, - ) { - const originalEvent = event.originalEvent as { propertyName: string } | undefined; - if (!originalEvent) { - throw new Error('Event is not an original event'); - } - - if (originalEvent.propertyName !== 'value') { - return; - } - - const { input } = instance; - if (!input) { - return; - } - - instance.sendIconDisabled.set(!!input.value); - - if (input.value.length > 0) { - input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; - } - }, - async 'click .js-send'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { - instance.send(event as unknown as Event); - }, - 'click .js-action-menu'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { - const groups = messageBox.actions.get(); - const config = { - popoverClass: 'message-box', - columns: [ - { - groups: Object.keys(groups).map((group) => { - const items = groups[group].map((item) => { - return { - icon: item.icon, - name: t(item.label), - type: 'messagebox-action', - id: item.id, - }; - }); - return { - title: t(group), - items, - }; - }), - }, - ], - offsetVertical: 10, - direction: 'top-inverted', - currentTarget: event.currentTarget.firstElementChild.firstElementChild, - data: { - rid: this.rid, - tmid: this.tmid, - prid: this.subscription.prid, - messageBox: instance.firstNode, - chat: instance.data.chatContext, - }, - activeElement: event.currentTarget, - }; - - popover.open(config); - }, - 'click .js-message-actions .js-message-action'( - this: { rid: IRoom['_id']; tmid?: IMessage['_id']; subscription: IRoom }, - event: JQuery.ClickEvent, - instance: MessageBoxTemplateInstance, - ) { - const { id } = event.currentTarget.dataset; - const actions = messageBox.actions.getById(id); - actions - .filter(({ action }) => !!action) - .forEach(({ action }) => { - console.log(instance.data); - action.call(null, { - rid: this.rid, - tmid: this.tmid, - messageBox: instance.firstNode as HTMLElement, - prid: this.subscription.prid, - event: event as unknown as Event, - chat: instance.data.chatContext, - }); - }); - }, - 'click .js-format'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { - event.preventDefault(); - event.stopPropagation(); - - const { id } = event.currentTarget.dataset; - const { pattern } = formattingButtons.filter(({ condition }) => !condition || condition()).find(({ label }) => label === id) ?? {}; - - if (!pattern) { - return; - } - - applyFormatting(pattern, instance.input); - }, -}); +// Template.messageBox.onCreated(function (this: MessageBoxTemplateInstance) { +// this.state = new ReactiveDict(); +// this.popupConfig = new ReactiveVar(null); +// this.replyMessageData = new ReactiveVar(this.data.chatContext?.composer?.quotedMessages.get() ?? []); +// this.isMicrophoneDenied = new ReactiveVar(true); +// this.isSendIconVisible = new ReactiveVar(false); + +// this.set = (value) => { +// const { input } = this; +// if (!input) { +// return; +// } + +// input.value = value; +// $(input).trigger('change').trigger('input'); +// }; + +// this.insertNewLine = () => { +// const { input, autogrow } = this; +// if (!input) { +// return; +// } + +// if (input.selectionStart || input.selectionStart === 0) { +// const newPosition = input.selectionStart + 1; +// const before = input.value.substring(0, input.selectionStart); +// const after = input.value.substring(input.selectionEnd, input.value.length); +// input.value = `${before}\n${after}`; +// input.selectionStart = newPosition; +// input.selectionEnd = newPosition; +// } else { +// input.value += '\n'; +// } +// $(input).trigger('change').trigger('input'); + +// input.blur(); +// input.focus(); +// autogrow?.update(); +// }; + +// this.send = (event) => { +// const { input } = this; + +// if (!input) { +// return; +// } + +// const { +// autogrow, +// data: { onSend, tshow }, +// } = this; +// const { value } = input; +// this.set(''); + +// UserAction.stop(this.data.rid, USER_ACTIVITIES.USER_TYPING, { tmid: this.data.tmid }); + +// onSend?.call(this.data, event, { value, tshow }).then(() => { +// autogrow?.update(); +// input.focus(); +// }); +// }; +// }); + +// Template.messageBox.onRendered(function (this: MessageBoxTemplateInstance) { +// let inputSetup = false; + +// this.autorun(() => { +// const { rid, subscription } = Template.currentData() as MessageBoxTemplateInstance['data']; +// const room = Session.get(`roomData${rid}`); + +// if (!inputSetup) { +// const $input = $(this.find('.js-input-message')); +// this.source = $input[0] as HTMLTextAreaElement | undefined; +// if (this.source) { +// inputSetup = true; +// } +// } + +// if (!room) { +// return this.state.set({ +// room: false, +// isBlockedOrBlocker: false, +// mustJoinWithCode: false, +// }); +// } + +// const isBlocked = room && room.t === 'd' && subscription && subscription.blocked; +// const isBlocker = room && room.t === 'd' && subscription && subscription.blocker; +// const isBlockedOrBlocker = isBlocked || isBlocker; + +// const mustJoinWithCode = !subscription && room.joinCodeRequired; + +// return this.state.set({ +// room: false, +// isBlockedOrBlocker, +// mustJoinWithCode, +// }); +// }); + +// this.autorun(() => { +// const { rid, tmid, onResize, chatContext } = Template.currentData() as MessageBoxTemplateInstance['data']; + +// let unsubscribeToQuotedMessages: (() => void) | undefined; + +// Tracker.afterFlush(() => { +// const input = this.find('.js-input-message') as HTMLTextAreaElement; + +// if (this.input === input) { +// return; +// } + +// this.input = input; + +// if (chatContext) { +// const storageID = `${rid}${tmid ? `-${tmid}` : ''}`; +// chatContext.setComposerAPI(createComposerAPI(input, storageID)); +// } + +// setTimeout(() => { +// if (window.matchMedia('screen and (min-device-width: 500px)').matches) { +// input.focus(); +// } +// }, 200); + +// unsubscribeToQuotedMessages?.(); + +// unsubscribeToQuotedMessages = chatContext?.composer?.quotedMessages.subscribe(() => { +// this.replyMessageData.set(chatContext?.composer?.quotedMessages.get() ?? []); +// }); + +// if (input && rid) { +// this.popupConfig.set({ +// rid, +// tmid, +// getInput: () => input, +// }); +// } else { +// this.popupConfig.set(null); +// } + +// if (this.autogrow) { +// this.autogrow.destroy(); +// this.autogrow = null; +// } + +// if (!input) { +// return; +// } + +// const shadow = this.find('.js-input-message-shadow'); +// this.autogrow = onResize ? setupAutogrow(input, shadow, onResize) : null; +// }); +// }); +// }); + +// Template.messageBox.onDestroyed(function (this: MessageBoxTemplateInstance) { +// UserAction.cancel(this.data.rid); + +// if (lastFocusedInput === this.input) { +// lastFocusedInput = undefined; +// } + +// if (!this.autogrow) { +// return; +// } + +// this.autogrow.destroy(); +// }); + +// Template.messageBox.helpers({ +// isAnonymousOrMustJoinWithCode() { +// const instance = Template.instance() as MessageBoxTemplateInstance; +// const { rid } = Template.currentData() as MessageBoxTemplateInstance['data']; +// if (!rid) { +// return false; +// } +// const isAnonymous = !Meteor.userId(); +// return isAnonymous || instance.state.get('mustJoinWithCode'); +// }, +// isWritable() { +// const { rid, subscription } = Template.currentData() as MessageBoxTemplateInstance['data']; +// if (!rid) { +// return true; +// } + +// const isBlockedOrBlocker = (Template.instance() as MessageBoxTemplateInstance).state.get('isBlockedOrBlocker'); + +// if (isBlockedOrBlocker) { +// return false; +// } + +// if (subscription?.onHold) { +// return false; +// } + +// const isReadOnly = roomCoordinator.readOnly(rid, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })); +// const isArchived = roomCoordinator.archived(rid) || (subscription && subscription.t === 'd' && subscription.archived); + +// return !isReadOnly && !isArchived; +// }, +// popupConfig() { +// return (Template.instance() as MessageBoxTemplateInstance).popupConfig.get(); +// }, +// input() { +// return (Template.instance() as MessageBoxTemplateInstance).input; +// }, +// replyMessageData() { +// return (Template.instance() as MessageBoxTemplateInstance).replyMessageData.get(); +// }, +// onDismissReply() { +// const { chatContext } = (Template.instance() as MessageBoxTemplateInstance).data; +// return (mid: IMessage['_id']) => chatContext?.composer?.dismissQuotedMessage(mid); +// }, +// isEmojiEnabled() { +// return getUserPreference(Meteor.userId(), 'useEmojis'); +// }, +// maxMessageLength() { +// return settings.get('Message_AllowConvertLongMessagesToAttachment') ? null : settings.get('Message_MaxAllowedSize'); +// }, +// isSendIconVisible() { +// return (Template.instance() as MessageBoxTemplateInstance).isSendIconVisible.get(); +// }, +// canSend() { +// const { rid } = Template.currentData(); +// if (!rid) { +// return true; +// } + +// return roomCoordinator.verifyCanSendMessage(rid); +// }, +// actions() { +// const actionGroups = messageBox.actions.get(); + +// return Object.values(actionGroups).reduce((actions, actionGroup) => [...actions, ...actionGroup], []); +// }, +// formattingButtons() { +// return formattingButtons.filter(({ condition }) => !condition || condition()); +// }, +// isBlockedOrBlocker() { +// return (Template.instance() as MessageBoxTemplateInstance).state.get('isBlockedOrBlocker'); +// }, +// onHold() { +// const { rid, subscription } = Template.currentData(); +// return rid && !!subscription?.onHold; +// }, +// isSubscribed() { +// const { subscription } = Template.currentData(); +// return !!subscription; +// }, +// isFederatedRoom() { +// const { rid } = Template.currentData(); + +// const room = ChatRoom.findOne(rid); + +// return room && isRoomFederated(room); +// }, +// }); + +// const handleFormattingShortcut = (event: KeyboardEvent, instance: MessageBoxTemplateInstance) => { +// const isMacOS = navigator.platform.indexOf('Mac') !== -1; +// const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey); + +// if (!isCmdOrCtrlPressed) { +// return false; +// } + +// const key = event.key.toLowerCase(); + +// const { pattern } = formattingButtons.filter(({ condition }) => !condition || condition()).find(({ command }) => command === key) || {}; + +// if (!pattern) { +// return false; +// } + +// const { input } = instance; +// applyFormatting(pattern, input); +// return true; +// }; + +// Template.messageBox.events({ +// async 'click .js-join'(event: JQuery.ClickEvent) { +// event.stopPropagation(); +// event.preventDefault(); + +// const joinCodeInput = (Template.instance() as MessageBoxTemplateInstance).find('[name=joinCode]') as HTMLInputElement | undefined; +// const joinCode = joinCodeInput?.value; + +// await call('joinRoom', this.rid, joinCode); +// }, + +// const caretPos = input.selectionStart; +// const textAreaTxt = input.value; + +// input.focus(); +// if (!document.execCommand || !document.execCommand('insertText', false, emojiValue)) { +// instance.set(textAreaTxt.substring(0, caretPos) + emojiValue + textAreaTxt.substring(caretPos)); +// input.focus(); +// } + +// input.selectionStart = caretPos + emojiValue.length; +// input.selectionEnd = caretPos + emojiValue.length; +// }); +// }, +// 'focus .js-input-message'(event: JQuery.FocusEvent) { +// KonchatNotification.removeRoomNotification(this.rid); +// lastFocusedInput = event.currentTarget; +// }, +// 'keydown .js-input-message'( +// this: MessageBoxTemplateInstance['data'], +// event: JQuery.KeyDownEvent, +// instance: MessageBoxTemplateInstance, +// ) { +// const { originalEvent } = event; +// if (!originalEvent) { +// throw new Error('Event is not an original event'); +// } + +// const isEventHandled = handleFormattingShortcut(originalEvent, instance) || handleSubmit(originalEvent, instance); + +// if (!Object.values(keyCodes).includes(keyCode)) { +// if (input?.value.trim()) { +// UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); +// } else { +// UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); +// } +// } + +// const { chatContext } = this; +// const { currentTarget: input } = event; + +// switch (event.key) { +// case 'Escape': { +// const currentEditing = chatContext?.currentEditing; + +// if (currentEditing) { +// event.preventDefault(); +// event.stopPropagation(); + +// currentEditing.reset().then((reset) => { +// if (!reset) { +// currentEditing?.cancel(); +// } +// }); + +// return; +// } + +// if (!input.value.trim()) this.onEscape?.(); +// return; +// } + +// case 'ArrowUp': { +// if (event.shiftKey) { +// return; +// } + +// if (input.selectionEnd === 0) { +// event.preventDefault(); +// event.stopPropagation(); + +// this.onNavigateToPreviousMessage?.(); + +// if (event.altKey) { +// input.setSelectionRange(0, 0); +// } +// } + +// return; +// } + +// case 'ArrowDown': { +// if (event.shiftKey) { +// return; +// } + +// if (input.selectionEnd === input.value.length) { +// event.preventDefault(); +// event.stopPropagation(); + +// this.onNavigateToNextMessage?.(); + +// if (event.altKey) { +// input.setSelectionRange(input.value.length, input.value.length); +// } +// } +// } +// } +// }, +// 'keyup .js-input-message'(this: MessageBoxTemplateInstance['data'], event: JQuery.KeyUpEvent) { +// const { rid, tmid } = this; +// const { currentTarget: input, which: keyCode } = event; + +// if (!Object.values(keyCodes).includes(keyCode)) { +// if (input?.value.trim()) { +// UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); +// } else { +// UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid }); +// } +// } +// }, +// 'paste .js-input-message'(event: JQuery.TriggeredEvent, instance: MessageBoxTemplateInstance) { +// const originalEvent = event.originalEvent as ClipboardEvent | undefined; +// if (!originalEvent) { +// throw new Error('Event is not an original event'); +// } + +// const { autogrow } = instance; + +// setTimeout(() => autogrow?.update(), 50); + +// if (!originalEvent.clipboardData) { +// return; +// } + +// const items = Array.from(originalEvent.clipboardData.items); + +// if (items.some(({ kind, type }) => kind === 'string' && type === 'text/plain')) { +// return; +// } + +// const files = items +// .filter((item) => item.kind === 'file' && item.type.indexOf('image/') !== -1) +// .map((item) => { +// const fileItem = item.getAsFile(); + +// if (!fileItem) { +// return; +// } + +// const imageExtension = fileItem ? getImageExtensionFromMime(fileItem.type) : undefined; + +// const extension = imageExtension ? `.${imageExtension}` : ''; + +// Object.defineProperty(fileItem, 'name', { +// writable: true, +// value: `Clipboard - ${moment().format(settings.get('Message_TimeAndDateFormat'))}${extension}`, +// }); +// return fileItem; +// }) +// .filter((file): file is File => !!file); + +// if (files.length) { +// event.preventDefault(); +// instance.data.onUploadFiles?.(files); +// } +// }, +// 'input .js-input-message'( +// this: MessageBoxTemplateInstance['data'], +// _event: JQuery.TriggeredEvent, +// instance: MessageBoxTemplateInstance, +// ) { +// const { input } = instance; +// if (!input) { +// return; +// } + +// instance.isSendIconVisible.set(!!input.value); + +// if (input.value.length > 0) { +// input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; +// } +// }, +// 'propertychange .js-input-message'( +// this: MessageBoxTemplateInstance['data'], +// event: JQuery.TriggeredEvent, +// instance: MessageBoxTemplateInstance, +// ) { +// const originalEvent = event.originalEvent as { propertyName: string } | undefined; +// if (!originalEvent) { +// throw new Error('Event is not an original event'); +// } + +// if (originalEvent.propertyName !== 'value') { +// return; +// } + +// const { input } = instance; +// if (!input) { +// return; +// } + +// instance.sendIconDisabled.set(!!input.value); + +// if (input.value.length > 0) { +// input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; +// } +// }, +// async 'click .js-send'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { +// instance.send(event as unknown as Event); +// }, +// 'click .js-action-menu'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { +// const groups = messageBox.actions.get(); +// const config = { +// popoverClass: 'message-box', +// columns: [ +// { +// groups: Object.keys(groups).map((group) => { +// const items = groups[group].map((item) => { +// return { +// icon: item.icon, +// name: t(item.label), +// type: 'messagebox-action', +// id: item.id, +// }; +// }); +// return { +// title: t(group), +// items, +// }; +// }), +// }, +// ], +// offsetVertical: 10, +// direction: 'top-inverted', +// currentTarget: event.currentTarget.firstElementChild.firstElementChild, +// data: { +// rid: this.rid, +// tmid: this.tmid, +// prid: this.subscription.prid, +// messageBox: instance.firstNode, +// chat: instance.data.chatContext, +// }, +// activeElement: event.currentTarget, +// }; + +// popover.open(config); +// }, +// 'click .js-action-menu'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) {}, +// 'click .js-message-actions .js-message-action'( +// this: { rid: IRoom['_id']; tmid?: IMessage['_id']; subscription: IRoom }, +// event: JQuery.ClickEvent, +// instance: MessageBoxTemplateInstance, +// ) { +// const { id } = event.currentTarget.dataset; +// const actions = messageBox.actions.getById(id); +// actions +// .filter(({ action }) => !!action) +// .forEach(({ action }) => { +// console.log(instance.data); +// action.call(null, { +// rid: this.rid, +// tmid: this.tmid, +// messageBox: instance.firstNode as HTMLElement, +// prid: this.subscription.prid, +// event: event as unknown as Event, +// chat: instance.data.chatContext, +// }); +// }); +// }, +// 'click .js-format'(event: JQuery.ClickEvent, instance: MessageBoxTemplateInstance) { +// event.preventDefault(); +// event.stopPropagation(); + +// const { id } = event.currentTarget.dataset; +// const { pattern } = formattingButtons.filter(({ condition }) => !condition || condition()).find(({ label }) => label === id) ?? {}; + +// if (!pattern) { +// return; +// } + +// applyFormatting(pattern, instance.input); +// }, +// }); diff --git a/apps/meteor/app/ui-message/client/messageBox/userActionIndicator.ts b/apps/meteor/app/ui-message/client/messageBox/userActionIndicator.ts index ddec463ad785..fd9460f25e46 100644 --- a/apps/meteor/app/ui-message/client/messageBox/userActionIndicator.ts +++ b/apps/meteor/app/ui-message/client/messageBox/userActionIndicator.ts @@ -11,8 +11,8 @@ const maxUsernames = parseInt(getConfig('max-usernames-typing') || '2'); Template.userActionIndicator.helpers({ data() { - const roomAction = UserAction.get(this.tmid || this.rid) || {}; - if (!Object.keys(roomAction).length) { + const roomAction = UserAction.get(this.tmid || this.rid); + if (!roomAction || !Object.keys(roomAction).length) { return []; } diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index bbed317e8dea..8648d3a2771e 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -1,10 +1,11 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import type { ContextType } from 'react'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; type MessageBoxAction = { - label: string; + label: TranslationKey; id: string; icon?: string; action: (params: { @@ -18,51 +19,51 @@ type MessageBoxAction = { }; export class MessageBoxActions { - actions: Record = {}; + actions: Map = new Map(); - add(group: string, label: string, config: Omit) { + add(group: TranslationKey, label: TranslationKey, config: Omit) { if (!group && !label && !config) { return; } - if (!this.actions[group]) { - this.actions[group] = []; + if (!this.actions.has(group)) { + this.actions.set(group, []); } - const actionExists = this.actions[group].find((action) => action.label === label); + const actionExists = this.actions.get(group)?.find((action) => action.label === label); if (actionExists) { return; } - this.actions[group].push({ ...config, label }); + this.actions.get(group)?.push({ ...config, label }); } - remove(group: string, expression: RegExp) { - if (!group || !this.actions[group]) { + remove(group: TranslationKey, expression: RegExp) { + if (!group || !this.actions.get(group)) { return false; } - this.actions[group] = this.actions[group].filter((action) => !expression.test(action.id)); - return this.actions[group]; + this.actions.set(group, this.actions.get(group)?.filter((action) => !expression.test(action.id)) || []); + return this.actions.get(group); } - get(): Record; + get(): Record; - get(group: string): MessageBoxAction[]; + get(group: TranslationKey): MessageBoxAction[]; - get(group?: string) { + get(group?: TranslationKey) { if (!group) { - return Object.entries(this.actions).reduce>((ret, [group, actions]) => { + return [...this.actions.entries()].reduce>((ret, [group, actions]) => { const filteredActions = actions.filter((action) => !action.condition || action.condition()); if (filteredActions.length) { ret[group] = filteredActions; } return ret; - }, {}); + }, {} as Record); } - return this.actions[group].filter((action) => !action.condition || action.condition()); + return this.actions.get(group)?.filter((action) => !action.condition || action.condition()); } getById(id: MessageBoxAction['id']) { diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 4c2e1053a4fc..747a0009530c 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -72,10 +72,10 @@ export class ChatMessages implements ChatAPI { this.currentEditingMID = message._id; setHighlightMessage(message._id); - this.composer?.setEditingMode(true); + this.composer.setEditingMode(true); this.composer.setText(text, { selection: { start: cursorPosition, end: cursorPosition } }); - this.composer?.focus(); + this.composer.focus(); }, }; diff --git a/apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx b/apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx index 63a985a2bf95..90aea18563ab 100644 --- a/apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx +++ b/apps/meteor/client/components/message/Attachments/FieldsAttachment/Field.tsx @@ -8,6 +8,7 @@ type FieldProps = { value: ReactNode; } & Omit, 'title' | 'value'>; +// TODO: description missing color token const Field: FC = ({ title, value, ...props }) => ( diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 970e2a58d2d2..7e1729a18ecc 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -14,6 +14,7 @@ export type ComposerAPI = { | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); }, ): void; + insertText(text: string): void; clear(): void; focus(): void; replyWith(text: string): Promise; @@ -24,7 +25,18 @@ export type ComposerAPI = { get(): IMessage[]; subscribe(callback: () => void): () => void; }; + setEditingMode(editing: boolean): void; + readonly editing: { + get(): boolean; + subscribe(callback: () => void): () => void; + }; + + setRecordingMode(recording: boolean): void; + readonly recording: { + get(): boolean; + subscribe(callback: () => void): () => void; + }; }; export type DataAPI = { diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 278ff547a720..d51c2f3193f7 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -79,3 +79,5 @@ createTemplateForComponent('loggedOutBanner', () => import('../ee/client/compone createTemplateForComponent('AudioMessageRecorder', () => import('./views/composer/AudioMessageRecorder'), { renderContainerView: () => HTML.DIV({ class: 'rc-message-box__audio-message-container' }), }); + +createTemplateForComponent('messageBox', () => import('./views/room/components/body/composer/LegacyComposer/MessageBoxBlazeWrapper')); diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx index 745c00980d0f..8e17616e9fa5 100644 --- a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -1,6 +1,7 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import { Icon, Throbber } from '@rocket.chat/fuselage'; +import { Box, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { MessageComposerAction } from '@rocket.chat/ui-composer'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useState } from 'react'; @@ -13,7 +14,7 @@ const audioRecorder = new AudioRecorder(); type AudioMessageRecorderProps = { rid: IRoom['_id']; - tmid: IMessage['_id']; + tmid?: IMessage['_id']; chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React }; @@ -38,6 +39,8 @@ const AudioMessageRecorder = ({ rid, tmid, chatContext }: AudioMessageRecorderPr const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); + chat?.composer?.setRecordingMode(false); + setState('idle'); return blob; @@ -114,7 +117,7 @@ const AudioMessageRecorder = ({ rid, tmid, chatContext }: AudioMessageRecorderPr if (recordingRoomId && recordingRoomId !== rid) { return; } - + chat?.composer?.setRecordingMode(true); setState('recording'); try { @@ -134,6 +137,7 @@ const AudioMessageRecorder = ({ rid, tmid, chatContext }: AudioMessageRecorderPr } catch (error) { console.log(error); setIsMicrophoneDenied(true); + chat?.composer?.setRecordingMode(false); setState('idle'); } }); @@ -159,30 +163,40 @@ const AudioMessageRecorder = ({ rid, tmid, chatContext }: AudioMessageRecorderPr return null; } + if (state === 'idle') { + return ( + + ); + } + return (
{state === 'recording' && ( <> -
- -
-
+ + {time} -
-
- -
+ + )} - {state === 'idle' && ( -
- -
- )} {state === 'loading' && (
- + a
)}
diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx index 2e4c0fd48bdd..ee12743b144b 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx @@ -1,17 +1,13 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { Blaze } from 'meteor/blaze'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; import type { ContextType, ReactElement } from 'react'; -import React, { memo, useCallback, useEffect, useRef } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; -import type { MessageBoxTemplateInstance } from '../../../../../../app/ui-message/client/messageBox/messageBox'; import { RoomManager } from '../../../../../../app/ui-utils/client'; -import { useEmbeddedLayout } from '../../../../../hooks/useEmbeddedLayout'; import { useReactiveValue } from '../../../../../hooks/useReactiveValue'; import ComposerSkeleton from '../../../Room/ComposerSkeleton'; import type { ChatContext } from '../../../contexts/ChatContext'; +import MessageBox from './LegacyComposer/MessageBox'; export type ComposerMessageProps = { rid: IRoom['_id']; @@ -24,100 +20,26 @@ export type ComposerMessageProps = { onUploadFiles?: (files: readonly File[]) => void; }; -const ComposerMessage = ({ - rid, - subscription, - chatMessagesInstance, - onResize, - onEscape, - onNavigateToNextMessage, - onNavigateToPreviousMessage, - onUploadFiles, -}: ComposerMessageProps): ReactElement => { - const isLayoutEmbedded = useEmbeddedLayout(); +const ComposerMessage = ({ rid, chatMessagesInstance, ...props }: ComposerMessageProps): ReactElement => { const showFormattingTips = useSetting('Message_ShowFormattingTips') as boolean; - const messageBoxViewRef = useRef(); - const messageBoxViewDataRef = useRef( - new ReactiveVar({ - rid, - subscription, - isEmbedded: isLayoutEmbedded, - showFormattingTips: showFormattingTips && !isLayoutEmbedded, - onResize, - onEscape, - onNavigateToNextMessage, - onNavigateToPreviousMessage, - onUploadFiles, - chatContext: chatMessagesInstance, - }), - ); - - useEffect(() => { - messageBoxViewDataRef.current.set({ - rid, - subscription, - isEmbedded: isLayoutEmbedded, - showFormattingTips: showFormattingTips && !isLayoutEmbedded, - onResize, - onEscape, - onNavigateToNextMessage, - onNavigateToPreviousMessage, - onUploadFiles, - chatContext: chatMessagesInstance, - }); - }, [ - isLayoutEmbedded, - onResize, - rid, - showFormattingTips, - subscription, - chatMessagesInstance, - onEscape, - onNavigateToNextMessage, - onNavigateToPreviousMessage, - onUploadFiles, - ]); - const dispatchToastMessage = useToastMessageDispatch(); - const footerRef = useCallback( - (footer: HTMLElement | null) => { - if (footer) { - messageBoxViewRef.current = Blaze.renderWithData( - Template.messageBox, - (): MessageBoxTemplateInstance['data'] => ({ - ...messageBoxViewDataRef.current.get(), - onSend: async ( - _event: Event, - { - value: text, - tshow, - }: { - value: string; - tshow?: boolean; - }, - ): Promise => { - try { - await chatMessagesInstance?.flows.sendMessage({ - text, - tshow, - }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }, - }), - footer, - ); - return; - } - - if (messageBoxViewRef.current) { - Blaze.remove(messageBoxViewRef.current); - messageBoxViewRef.current = undefined; - } - }, + const composerProp = useMemo( + () => ({ + onSend: async ({ value: text, tshow }: { value: string; tshow?: boolean }): Promise => { + try { + await chatMessagesInstance?.flows.sendMessage({ + text, + tshow, + }); + + await chatMessagesInstance?.composer?.setText(''); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + }), [chatMessagesInstance, dispatchToastMessage], ); @@ -131,7 +53,7 @@ const ComposerMessage = ({ ); } - return