diff --git a/.vscode/settings.json b/.vscode/settings.json index a32489e34a64..47310bec0703 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,10 +14,5 @@ } ], "typescript.tsdk": "./node_modules/typescript/lib", - "cSpell.words": [ - "livechat", - "omnichannel", - "photoswipe", - "tmid" - ] + "cSpell.words": ["katex", "livechat", "omnichannel", "photoswipe", "tmid"] } diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 47420ab2f62d..70f1066e09f4 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -8,6 +8,7 @@ import { import type { IUser } from '@rocket.chat/core-typings'; import { API } from '../api'; +import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey'; API.v1.addRoute( 'e2e.fetchMyKeys', @@ -195,3 +196,37 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'e2e.acceptSuggestedGroupKey', + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, + { + async post() { + const { rid } = this.bodyParams; + + await handleSuggestedGroupKey('accept', rid, this.userId, 'e2e.acceptSuggestedGroupKey'); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'e2e.rejectSuggestedGroupKey', + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, + { + async post() { + const { rid } = this.bodyParams; + + await handleSuggestedGroupKey('reject', rid, this.userId, 'e2e.rejectSuggestedGroupKey'); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index eb8cba74c34b..78bbabd0f37a 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { isTranslatedMessage } from '@rocket.chat/core-typings'; import { AutoTranslate } from './autotranslate'; import { settings } from '../../../settings/client'; @@ -8,6 +7,10 @@ import { hasAtLeastOnePermission } from '../../../authorization/client'; import { MessageAction } from '../../../ui-utils/client/lib/MessageAction'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { Messages } from '../../../models/client'; +import { + hasTranslationLanguageInAttachments, + hasTranslationLanguageInMessage, +} from '../../../../client/views/room/MessageList/lib/autoTranslate'; Meteor.startup(() => { AutoTranslate.init(); @@ -22,8 +25,7 @@ Meteor.startup(() => { action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); - if (!isTranslatedMessage(message) || !message.translations[language]) { - // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); Meteor.call('autoTranslate.translateMessage', message, language); @@ -31,12 +33,19 @@ Meteor.startup(() => { const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, - condition({ message, user }) { + condition({ message, subscription, user }) { if (!user) { return false; } + const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && message.autoTranslateShowInverse); + return Boolean( + (message?.u && + message.u._id !== user._id && + subscription?.autoTranslate && + (message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse) || + (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)), + ); }, order: 90, }); @@ -48,8 +57,7 @@ Meteor.startup(() => { action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); - if (!isTranslatedMessage(message) || !message.translations[language]) { - // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); Meteor.call('autoTranslate.translateMessage', message, language); @@ -57,12 +65,19 @@ Meteor.startup(() => { const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, - condition({ message, user }) { + condition({ message, subscription, user }) { + const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; if (!user) { return false; } - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && !message.autoTranslateShowInverse); + return Boolean( + message?.u && + message.u._id !== user._id && + subscription?.autoTranslate && + !(message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse && + (hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language)), + ); }, order: 90, }); diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 71ff7cc92c5a..199df2e5db71 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -9,10 +9,15 @@ import type { IUser, MessageAttachmentDefault, } from '@rocket.chat/core-typings'; +import { isTranslatedMessageAttachment } from '@rocket.chat/core-typings'; import { Subscriptions, Messages } from '../../../models/client'; import { hasPermission } from '../../../authorization/client'; import { call } from '../../../../client/lib/utils/call'; +import { + hasTranslationLanguageInAttachments, + hasTranslationLanguageInMessage, +} from '../../../../client/views/room/MessageList/lib/autoTranslate'; let userLanguage = 'en'; let username = ''; @@ -55,6 +60,9 @@ export const AutoTranslate = { language: string, autoTranslateShowInverse: boolean, ): MessageAttachmentDefault[] { + if (!isTranslatedMessageAttachment(attachments)) { + return attachments; + } for (const attachment of attachments) { if (attachment.author_name !== username) { if (attachment.text && attachment.translations && attachment.translations[language]) { @@ -134,16 +142,11 @@ export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMess message.translations = {}; } if (!!subscription?.autoTranslate !== !!message.autoTranslateShowInverse) { - const hasAttachmentsTranslate = - message.attachments?.some( - (attachment) => - 'translations' in attachment && - typeof attachment.translations === 'object' && - autoTranslateLanguage in attachment.translations, - ) ?? false; - message.translations.original = message.html; - if (message.translations[autoTranslateLanguage] && !hasAttachmentsTranslate) { + if ( + message.translations[autoTranslateLanguage] && + !hasTranslationLanguageInAttachments(message.attachments, autoTranslateLanguage) + ) { message.html = message.translations[autoTranslateLanguage]; } @@ -155,12 +158,6 @@ export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMess ); } } - } else if (message.attachments && message.attachments.length > 0) { - message.attachments = AutoTranslate.translateAttachments( - message.attachments, - autoTranslateLanguage, - !!message.autoTranslateShowInverse, - ); } return message; }; @@ -177,7 +174,8 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate subscription && subscription.autoTranslate === true && message.msg && - (!message.translations || !message.translations[language]) + (!message.translations || + (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language))) ) { // || (message.attachments && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index edb674f5a187..1d926fe5bb77 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -305,10 +305,13 @@ export abstract class AutoTranslate { Meteor.defer(() => { for (const [index, attachment] of message.attachments?.entries() ?? []) { if (attachment.description || attachment.text) { - const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); + // Removes the initial link `[ ](quoterl)` from quote message before translation + const translatedText = attachment?.text?.replace(/\[(.*?)\]\(.*?\)/g, '$1') || attachment?.text; + const attachmentMessage = { ...attachment, text: translatedText }; + const translations = this._translateAttachmentDescriptions(attachmentMessage, targetLanguages); + if (!_.isEmpty(translations)) { Messages.addAttachmentTranslations(message._id, index, translations); - Messages.addTranslations(message._id, translations, TranslationProviderRegistry[Provider]); } } } diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 91a068b880aa..135601d48efc 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -146,6 +146,7 @@ class GoogleAutoTranslate extends AutoTranslate { params: { key: this.apiKey, target: language, + format: 'text', }, query, }); @@ -190,6 +191,7 @@ class GoogleAutoTranslate extends AutoTranslate { params: { key: this.apiKey, target: language, + format: 'text', }, query, }); diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index fea71e381182..739a939b8c35 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -264,7 +264,8 @@ export class E2ERoom extends Emitter { const decryptedKey = await decryptRSA(e2e.privateKey, groupKey); this.sessionKeyExportedString = toString(decryptedKey); } catch (error) { - return this.error('Error decrypting group key: ', error); + this.error('Error decrypting group key: ', error); + return false; } this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); @@ -275,8 +276,11 @@ export class E2ERoom extends Emitter { // Key has been obtained. E2E is now in session. this.groupSessionKey = key; } catch (error) { - return this.error('Error importing group key: ', error); + this.error('Error importing group key: ', error); + return false; } + + return true; } async createGroupKey() { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index cdb088de99d1..50a5fb107310 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -136,6 +136,18 @@ class E2E extends Emitter { }); } + async acceptSuggestedKey(rid: string): Promise { + await APIClient.post('/v1/e2e.acceptSuggestedGroupKey', { + rid, + }); + } + + async rejectSuggestedKey(rid: string): Promise { + await APIClient.post('/v1/e2e.rejectSuggestedGroupKey', { + rid, + }); + } + getKeysFromLocalStorage(): KeyPair { return { public_key: Meteor._localStorage.getItem('public_key'), diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts new file mode 100644 index 000000000000..8b6d313e57e2 --- /dev/null +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import { Subscriptions } from '@rocket.chat/models'; + +export async function handleSuggestedGroupKey( + handle: 'accept' | 'reject', + rid: string, + userId: string | null, + method: string, +): Promise { + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method }); + } + + const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + if (!sub) { + throw new Meteor.Error('error-subscription-not-found', 'Subscription not found', { method }); + } + + const suggestedKey = String(sub.E2ESuggestedKey ?? '').trim(); + if (!suggestedKey) { + throw new Meteor.Error('error-no-suggested-key-available', 'No suggested key available', { method }); + } + + if (handle === 'accept') { + await Subscriptions.setGroupE2EKey(sub._id, suggestedKey); + } + + await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); +} diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.js b/apps/meteor/app/e2e/server/methods/updateGroupKey.js deleted file mode 100644 index 9aae29003ddf..000000000000 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Subscriptions } from '../../../models/server'; - -Meteor.methods({ - 'e2e.updateGroupKey'(rid, uid, key) { - const mySub = Subscriptions.findOneByRoomIdAndUserId(rid, Meteor.userId()); - if (mySub) { - // I have a subscription to this room - const userSub = Subscriptions.findOneByRoomIdAndUserId(rid, uid); - if (userSub) { - // uid also has subscription to this room - return Subscriptions.updateGroupE2EKey(userSub._id, key); - } - } - }, -}); diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts new file mode 100644 index 000000000000..fd5c0b055ada --- /dev/null +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; +import { Subscriptions } from '@rocket.chat/models'; + +Meteor.methods({ + async 'e2e.updateGroupKey'(rid, uid, key) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' }); + } + + // I have a subscription to this room + const mySub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + + if (mySub) { + // Setting the key to myself, can set directly to the final field + if (userId === uid) { + return Subscriptions.setGroupE2EKey(mySub._id, key); + } + + // uid also has subscription to this room + const userSub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid); + if (userSub) { + return Subscriptions.setGroupE2ESuggestedKey(userSub._id, key); + } + } + }, +}); diff --git a/apps/meteor/app/models/server/models/Subscriptions.js b/apps/meteor/app/models/server/models/Subscriptions.js index aa8773bb8365..42d12daca962 100644 --- a/apps/meteor/app/models/server/models/Subscriptions.js +++ b/apps/meteor/app/models/server/models/Subscriptions.js @@ -347,13 +347,6 @@ export class Subscriptions extends Base { return this.find(query, options); } - updateGroupE2EKey(_id, key) { - const query = { _id }; - const update = { $set: { E2EKey: key } }; - this.update(query, update); - return this.findOne({ _id }); - } - /** * @param {IRole['_id'][]} roles * @param {string} scope the value for the role scope (room id) diff --git a/apps/meteor/app/ui-message/client/message.html b/apps/meteor/app/ui-message/client/message.html index 6a571fa87c52..e3d9df72cb4b 100644 --- a/apps/meteor/app/ui-message/client/message.html +++ b/apps/meteor/app/ui-message/client/message.html @@ -55,7 +55,6 @@ {{#if showTranslated}} - {{ translationProvider }} {{/if}} {{#if msg.sentByEmail}} diff --git a/apps/meteor/app/ui-message/client/message.js b/apps/meteor/app/ui-message/client/message.js index c32d94b75ad6..f0bffebb9a52 100644 --- a/apps/meteor/app/ui-message/client/message.js +++ b/apps/meteor/app/ui-message/client/message.js @@ -21,6 +21,7 @@ import { renderMessageBody } from '../../../client/lib/utils/renderMessageBody'; import { settings } from '../../settings/client'; import { formatTime } from '../../../client/lib/utils/formatTime'; import { formatDate } from '../../../client/lib/utils/formatDate'; +import { hasTranslationLanguageInAttachments } from '../../../client/views/room/MessageList/lib/autoTranslate'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import './messageThread'; import './message.html'; @@ -258,7 +259,9 @@ Template.message.helpers({ const autoTranslate = subscription && subscription.autoTranslate; return ( msg.autoTranslateFetching || - (!!autoTranslate !== !!msg.autoTranslateShowInverse && msg.translations && msg.translations[settings.translateLanguage]) + (!!autoTranslate !== !!msg.autoTranslateShowInverse && msg.translations && msg.translations[settings.translateLanguage]) || + (!!autoTranslate !== !!msg.autoTranslateShowInverse && + hasTranslationLanguageInAttachments(msg.attachments, settings.translateLanguage)) ); } }, diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 9f5e79c71125..1ed1b5253281 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import type { Icon } from '@rocket.chat/fuselage'; -import type { IMessage, IUser, ISubscription, IRoom, SettingValue, Serialized } from '@rocket.chat/core-typings'; +import type { IMessage, IUser, ISubscription, IRoom, SettingValue, Serialized, ITranslatedMessage } from '@rocket.chat/core-typings'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { Messages, Rooms, Subscriptions } from '../../../models/client'; @@ -13,6 +13,7 @@ import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import type { ToolboxContextValue } from '../../../../client/views/room/contexts/ToolboxContext'; import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; import { APIClient } from '../../../utils/client'; +import type { AutoTranslateOptions } from '../../../../client/views/room/MessageList/hooks/useAutoTranslate'; const getMessage = async (msgId: string): Promise | null> => { try { @@ -71,7 +72,14 @@ export type MessageActionConfig = { tabbar, room, chat, - }: { message?: IMessage; tabbar: ToolboxContextValue; room?: IRoom; chat: ContextType }, + autoTranslateOptions, + }: { + message?: IMessage & Partial; + tabbar: ToolboxContextValue; + room?: IRoom; + chat: ContextType; + autoTranslateOptions?: AutoTranslateOptions; + }, ) => any; condition?: (props: MessageActionConditionProps) => Promise | boolean; }; diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 4ee0ced859c2..662da4ac343b 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -70,7 +70,14 @@ Meteor.startup(async function () { label: 'Quote', context: ['message', 'message-mobile', 'threads', 'federated'], action(_, props) { - const { message = messageArgs(this).msg, chat } = props; + const { message = messageArgs(this).msg, chat, autoTranslateOptions } = props; + + if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { + message.msg = + message.translations && autoTranslateOptions.autoTranslateLanguage + ? message.translations[autoTranslateOptions.autoTranslateLanguage] + : message.msg; + } chat?.composer?.quoteMessage(message); }, diff --git a/apps/meteor/client/components/message/StatusIndicators.tsx b/apps/meteor/client/components/message/StatusIndicators.tsx index d3bb297da4aa..f6eff61bef6e 100644 --- a/apps/meteor/client/components/message/StatusIndicators.tsx +++ b/apps/meteor/client/components/message/StatusIndicators.tsx @@ -1,6 +1,6 @@ import type { IMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; import { isEditedMessage, isE2EEMessage, isOTRMessage } from '@rocket.chat/core-typings'; -import { MessageStatusIndicator, MessageStatusIndicatorItem, MessageStatusIndicatorText } from '@rocket.chat/fuselage'; +import { MessageStatusIndicator, MessageStatusIndicatorItem } from '@rocket.chat/fuselage'; import { useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -10,7 +10,6 @@ import { useShowStarred, useShowTranslated, useShowFollowing, - useTranslateProvider, } from '../../views/room/MessageList/contexts/MessageListContext'; type StatusIndicatorsProps = { @@ -19,8 +18,7 @@ type StatusIndicatorsProps = { const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => { const t = useTranslation(); - const translated = useShowTranslated({ message }); - const translateProvider = useTranslateProvider({ message }); + const translated = useShowTranslated(message); const starred = useShowStarred({ message }); const following = useShowFollowing({ message }); @@ -33,11 +31,7 @@ const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => { return ( - {translated && ( - - {translateProvider} - - )} + {translated && } {following && } diff --git a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx index 8df0ddb34424..8aa541b6f1d8 100644 --- a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react'; import React from 'react'; import MarkdownText from '../../../../MarkdownText'; +import MessageContentBody from '../../../MessageContentBody'; import { useCollapse } from '../../../hooks/useCollapse'; import Attachment from '../structure/Attachment'; import AttachmentContent from '../structure/AttachmentContent'; @@ -22,13 +23,14 @@ export const AudioAttachment: FC = ({ description, title_link: link, title_link_download: hasDownload, + md, }) => { const [collapsed, collapse] = useCollapse(collapsedDefault); const getURL = useMediaUrl(); return ( - + {md ? : } {title} diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index 2f2b12d8e031..4bf59ec20603 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react'; import React from 'react'; import MarkdownText from '../../../../MarkdownText'; +import MessageContentBody from '../../../MessageContentBody'; import Attachment from '../structure/Attachment'; import AttachmentDescription from '../structure/AttachmentDescription'; import AttachmentDownload from '../structure/AttachmentDownload'; @@ -27,6 +28,7 @@ export const GenericFileAttachment: FC = ({ // format, // name, } = {}, + md, }) => { // const [collapsed, collapse] = useCollapse(collapsedDefault); const getURL = useMediaUrl(); @@ -34,7 +36,7 @@ export const GenericFileAttachment: FC = ({ {description && ( - + {md ? : } )} diff --git a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx index 90fc80487f7a..a5061c9a97dd 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react'; import React from 'react'; import MarkdownText from '../../../../MarkdownText'; +import MessageContentBody from '../../../MessageContentBody'; import { useCollapse } from '../../../hooks/useCollapse'; import Attachment from '../structure/Attachment'; import AttachmentContent from '../structure/AttachmentContent'; @@ -28,6 +29,7 @@ export const ImageAttachment: FC = ({ description, title_link: link, title_link_download: hasDownload, + md, }) => { const [loadImage, setLoadImage] = useLoadImage(); const [collapsed, collapse] = useCollapse(collapsedDefault); @@ -36,7 +38,7 @@ export const ImageAttachment: FC = ({ {description && ( - + {md ? : } )} diff --git a/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx index f8525d8d9d9d..fa1296cfe048 100644 --- a/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/VideoAttachment.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { userAgentMIMETypeFallback } from '../../../../../lib/utils/userAgentMIMETypeFallback'; import MarkdownText from '../../../../MarkdownText'; +import MessageContentBody from '../../../MessageContentBody'; import { useCollapse } from '../../../hooks/useCollapse'; import Attachment from '../structure/Attachment'; import AttachmentContent from '../structure/AttachmentContent'; @@ -32,6 +33,7 @@ export const VideoAttachment: FC = ({ description, title_link: link, title_link_download: hasDownload, + md, }) => { const [collapsed, collapse] = useCollapse(collapsedDefault); const getURL = useMediaUrl(); @@ -51,7 +53,7 @@ export const VideoAttachment: FC = ({ {description && ( - + {md ? : } )} diff --git a/apps/meteor/client/components/message/toolbox/Toolbox.tsx b/apps/meteor/client/components/message/toolbox/Toolbox.tsx index 80420d4e1709..cf6ea04b157f 100644 --- a/apps/meteor/client/components/message/toolbox/Toolbox.tsx +++ b/apps/meteor/client/components/message/toolbox/Toolbox.tsx @@ -1,4 +1,4 @@ -import type { IMessage, IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IUser, IRoom, ITranslatedMessage } from '@rocket.chat/core-typings'; import { isThreadMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { MessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; import { useUser, useUserSubscription, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; @@ -9,6 +9,7 @@ import React, { memo, useMemo } from 'react'; import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useIsSelecting } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; +import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { useChat } from '../../../views/room/contexts/ChatContext'; import { useRoom } from '../../../views/room/contexts/RoomContext'; import { useToolboxContext } from '../../../views/room/contexts/ToolboxContext'; @@ -28,7 +29,7 @@ const getMessageContext = (message: IMessage, room: IRoom): MessageActionContext }; type ToolboxProps = { - message: IMessage; + message: IMessage & Partial; }; const Toolbox = ({ message }: ToolboxProps): ReactElement | null => { @@ -61,6 +62,8 @@ const Toolbox = ({ message }: ToolboxProps): ReactElement | null => { const selecting = useIsSelecting(); + const autoTranslateOptions = useAutoTranslate(subscription); + if (selecting) { return null; } @@ -69,10 +72,10 @@ const Toolbox = ({ message }: ToolboxProps): ReactElement | null => { {actionsQueryResult.data?.message.map((action) => ( action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions })} key={action.id} icon={action.icon} title={t(action.label)} - onClick={(e): void => action.action(e, { message, tabbar: toolbox, room, chat })} data-qa-id={action.label} data-qa-type='message-action-menu' /> @@ -82,7 +85,7 @@ const Toolbox = ({ message }: ToolboxProps): ReactElement | null => { options={ actionsQueryResult.data?.menu.map((action) => ({ ...action, - action: (e): void => action.action(e, { message, tabbar: toolbox, room, chat }), + action: (e): void => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions }), })) ?? [] } data-qa-type='message-action-menu-options' diff --git a/apps/meteor/client/components/message/variants/ThreadMessage.tsx b/apps/meteor/client/components/message/variants/ThreadMessage.tsx index 7d7eb8118670..4c364098c8be 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessage.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessage.tsx @@ -1,12 +1,13 @@ -import type { ISubscription, IThreadMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IThreadMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Message, MessageLeftContainer, MessageContainer } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; -import { useUserId } from '@rocket.chat/ui-contexts'; +import { useUserId, useUserSubscription } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo, memo } from 'react'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; import { useMessageListContext } from '../../../views/room/MessageList/contexts/MessageListContext'; +import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { parseMessageTextToAstMarkdown, removePossibleNullMessageValues, @@ -21,7 +22,6 @@ import ThreadMessageContent from './thread/ThreadMessageContent'; type ThreadMessageProps = { message: IThreadMessage | IThreadMainMessage; - subscription?: ISubscription; unread: boolean; sequential: boolean; }; @@ -34,7 +34,9 @@ const ThreadMessage = ({ message, sequential, unread }: ThreadMessageProps): Rea actions: { openUserCard }, } = useMessageActions(); - const { autoTranslateLanguage, katex, showColors, useShowTranslated } = useMessageListContext(); + const { katex, showColors } = useMessageListContext(); + const subscription = useUserSubscription(message.rid); + const autoTranslateOptions = useAutoTranslate(subscription); const normalizeMessage = useMemo(() => { const parseOptions = { @@ -48,8 +50,8 @@ const ThreadMessage = ({ message, sequential, unread }: ThreadMessageProps): Rea }), }; return (message: TMessage) => - parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateLanguage, useShowTranslated); - }, [autoTranslateLanguage, katex, showColors, useShowTranslated]); + parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); + }, [katex, showColors, autoTranslateOptions]); const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index 34e9033dad4e..4e5c1539838f 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -10,12 +10,14 @@ import { ThreadMessageBody, ThreadMessageUnfollow, CheckBox, + MessageStatusIndicatorItem, } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; import { MessageTypes } from '../../../../app/ui-utils/client'; +import { useShowTranslated } from '../../../views/room/MessageList/contexts/MessageListContext'; import { useIsSelecting, useToggleSelect, @@ -24,6 +26,7 @@ import { } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; import { useMessageBody } from '../../../views/room/MessageList/hooks/useMessageBody'; import { useParentMessage } from '../../../views/room/MessageList/hooks/useParentMessage'; +import { isParsedMessage } from '../../../views/room/MessageList/lib/isParsedMessage'; import { useMessageActions } from '../../../views/room/contexts/MessageContext'; import UserAvatar from '../../avatar/UserAvatar'; import ThreadMessagePreviewBody from './threadPreview/ThreadMessagePreviewBody'; @@ -38,7 +41,8 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr actions: { openThread }, } = useMessageActions(); const parentMessage = useParentMessage(message.tmid); - const body = useMessageBody(parentMessage.data); + + const translated = useShowTranslated(message); const t = useTranslation(); const isSelecting = useIsSelecting(); @@ -47,6 +51,9 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr useCountSelected(); const messageType = parentMessage.isSuccess ? MessageTypes.getType(parentMessage.data) : null; + const messageBody = useMessageBody(parentMessage.data, message.rid); + + const previewMessage = isParsedMessage(messageBody) ? { md: messageBody } : { msg: messageBody }; return ( @@ -62,7 +69,13 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr {(parentMessage.data as { ignored?: boolean })?.ignored ? ( t('Message_Ignored') ) : ( - + + )} + {translated && ( + <> + {' '} + + )} )} @@ -80,7 +93,19 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr - {(message as { ignored?: boolean }).ignored ? t('Message_Ignored') : } + {(message as { ignored?: boolean }).ignored ? ( + t('Message_Ignored') + ) : ( + <> + + {translated && ( + <> + {' '} + + + )} + + )} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 779475a6e585..9117e4a12778 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; import type { UserPresence } from '../../../../lib/presence'; -import { useTranslateAttachments, useMessageListShowReadReceipt } from '../../../../views/room/MessageList/contexts/MessageListContext'; +import { useMessageListShowReadReceipt } from '../../../../views/room/MessageList/contexts/MessageListContext'; import type { MessageWithMdEnforced } from '../../../../views/room/MessageList/lib/parseMessageTextToAstMarkdown'; import { useMessageActions, useMessageOembedIsEnabled, useMessageRunActionLink } from '../../../../views/room/contexts/MessageContext'; import MessageContentBody from '../../MessageContentBody'; @@ -47,8 +47,6 @@ const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageConten const isEncryptedMessage = isE2EEMessage(message); - const messageAttachments = useTranslateAttachments({ message }); - return ( <> {!message.blocks?.length && !!message.md?.length && ( @@ -62,7 +60,7 @@ const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageConten {message.blocks && } - {!!messageAttachments.length && } + {!!message?.attachments?.length && } {oembedIsEnabled && !!message.urls?.length && } diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index cdd6f12674a4..fb1eb021cdb9 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; import type { UserPresence } from '../../../../lib/presence'; -import { useMessageListShowReadReceipt, useTranslateAttachments } from '../../../../views/room/MessageList/contexts/MessageListContext'; +import { useMessageListShowReadReceipt } from '../../../../views/room/MessageList/contexts/MessageListContext'; import type { MessageWithMdEnforced } from '../../../../views/room/MessageList/lib/parseMessageTextToAstMarkdown'; import { useMessageActions, useMessageOembedIsEnabled, useMessageRunActionLink } from '../../../../views/room/contexts/MessageContext'; import MessageContentBody from '../../MessageContentBody'; @@ -40,8 +40,6 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const isEncryptedMessage = isE2EEMessage(message); - const messageAttachments = useTranslateAttachments({ message }); - return ( <> {!message.blocks?.length && !!message.md?.length && ( @@ -55,7 +53,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem {message.blocks && } - {messageAttachments && } + {message.attachments && } {oembedIsEnabled && !!message.urls?.length && } diff --git a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx index 55c38b434e69..dcf4ffc5421e 100644 --- a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx +++ b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx @@ -1,18 +1,31 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import { isE2EEMessage } from '@rocket.chat/core-typings'; import { PreviewMarkup } from '@rocket.chat/gazzodown'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { parseMessageTextToAstMarkdown } from '../../../../views/room/MessageList/lib/parseMessageTextToAstMarkdown'; - type ThreadMessagePreviewBodyProps = { message: IMessage; }; -const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps): ReactElement | null => { - const parsedMessage = parseMessageTextToAstMarkdown(message, { colors: true, emoticons: true }); +const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps): ReactElement => { + const t = useTranslation(); + const isEncryptedMessage = isE2EEMessage(message); + + const getMessage = () => { + if (!isEncryptedMessage || message.e2e === 'done') { + return message.md ? : <>{message.msg}; + } + + if (isEncryptedMessage && message.e2e === 'pending') { + return <>{t('E2E_message_encrypted_placeholder')}; + } + + return <>{message.msg}; + }; - return parsedMessage.md ? : null; + return getMessage(); }; export default ThreadMessagePreviewBody; diff --git a/apps/meteor/client/lib/utils/messageArgs.ts b/apps/meteor/client/lib/utils/messageArgs.ts index 83248369c99a..0a92a6fa8adc 100644 --- a/apps/meteor/client/lib/utils/messageArgs.ts +++ b/apps/meteor/client/lib/utils/messageArgs.ts @@ -1,10 +1,10 @@ -import type { IMessage, IRoom, ISubscription, IUser, SettingValue } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription, ITranslatedMessage, IUser, SettingValue } from '@rocket.chat/core-typings'; export const messageArgs = ( context: any, ): { context?: 'threads' | 'mentions'; - msg: IMessage; + msg: ITranslatedMessage; u: IUser; room: IRoom; settings: Record; diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index 8ed28694d22d..2e2cb1d5d11f 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -53,26 +53,35 @@ Meteor.startup(() => { Notifications.onUser('e2ekeyRequest', handle); observable = Subscriptions.find().observe({ - changed: async (doc: ISubscription) => { - if (!doc.encrypted && !doc.E2EKey) { - e2e.removeInstanceByRoomId(doc.rid); + changed: async (sub: ISubscription) => { + if (!sub.encrypted && !sub.E2EKey) { + e2e.removeInstanceByRoomId(sub.rid); return; } - const e2eRoom = await e2e.getInstanceByRoomId(doc.rid); + const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); if (!e2eRoom) { return; } - doc.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + if (sub.E2ESuggestedKey) { + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + e2e.acceptSuggestedKey(sub.rid); + } else { + console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + e2e.rejectSuggestedKey(sub.rid); + } + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); // Cover private groups and direct messages - if (!e2eRoom.isSupportedRoomType(doc.t)) { + if (!e2eRoom.isSupportedRoomType(sub.t)) { e2eRoom.disable(); return; } - if (doc.E2EKey && e2eRoom.isWaitingKeys()) { + if (sub.E2EKey && e2eRoom.isWaitingKeys()) { e2eRoom.keyReceived(); return; } @@ -83,14 +92,14 @@ Meteor.startup(() => { e2eRoom.decryptSubscription(); }, - added: async (doc: ISubscription) => { - if (!doc.encrypted && !doc.E2EKey) { + added: async (sub: ISubscription) => { + if (!sub.encrypted && !sub.E2EKey) { return; } - return e2e.getInstanceByRoomId(doc.rid); + return e2e.getInstanceByRoomId(sub.rid); }, - removed: (doc: ISubscription) => { - e2e.removeInstanceByRoomId(doc.rid); + removed: (sub: ISubscription) => { + e2e.removeInstanceByRoomId(sub.rid); }, }); diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 56646a59bb4d..f02ad2c3fb1b 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -9,7 +9,7 @@ import type { soundDataType } from './lib'; import { validate, createSoundData } from './lib'; type AddCustomSoundProps = { - goToNew: (where: string) => () => void; + goToNew: (_id: string) => () => void; close: () => void; onChange: () => void; }; @@ -22,7 +22,6 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr const [sound, setSound] = useState<{ name: string }>(); const uploadCustomSound = useMethod('uploadCustomSound'); - const insertOrUpdateSound = useMethod('insertOrUpdateSound'); const handleChangeFile = useCallback((soundFile) => { @@ -74,11 +73,8 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr const handleSave = useCallback(async () => { try { const result = await saveAction(name, sound); - if (!result) { - throw new Error('error-something-went-wrong'); - } - goToNew(result); dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Saved_Successfully') }); + result && goToNew(result); onChange(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx similarity index 60% rename from apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx rename to apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx index 9eda105ec62c..7ea3ca8bb7f3 100644 --- a/apps/meteor/client/views/admin/customSounds/AdminSoundsRoute.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx @@ -1,6 +1,7 @@ -import { Button, Icon, Pagination } from '@rocket.chat/fuselage'; +import { Button, Icon, Pagination, States, StatesIcon, StatesActions, StatesAction, StatesTitle } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute, useRouteParameter, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useState, useCallback } from 'react'; @@ -14,8 +15,6 @@ import { usePagination } from '../../../components/GenericTable/hooks/usePaginat import { useSort } from '../../../components/GenericTable/hooks/useSort'; import Page from '../../../components/Page'; import VerticalBar from '../../../components/VerticalBar'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import { AsyncStatePhase } from '../../../lib/asyncState'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AddCustomSound from './AddCustomSound'; import CustomSoundRow from './CustomSoundRow'; @@ -46,7 +45,8 @@ const CustomSoundsRoute = (): ReactElement => { 500, ); - const { reload, ...result } = useEndpointData('/v1/custom-sounds.list', { params: query }); + const getCustomSoundsList = useEndpoint('GET', '/v1/custom-sounds.list'); + const { data, isSuccess, isLoading, isError, refetch } = useQuery(['custom-sounds', query], () => getCustomSoundsList(query)); const handleItemClick = useCallback( (_id) => (): void => { @@ -67,8 +67,18 @@ const CustomSoundsRoute = (): ReactElement => { }, [route]); const handleChange = useCallback(() => { - reload(); - }, [reload]); + refetch(); + }, [refetch]); + + const headers = useMemo( + () => [ + + {t('Name')} + , + , + ], + [setSort, sortBy, sortDirection, t], + ); if (!canManageCustomSounds) { return ; @@ -83,30 +93,54 @@ const CustomSoundsRoute = (): ReactElement => { - setParams(text)} /> - - - - {t('Name')} - - - - - {result.phase === AsyncStatePhase.LOADING && } - {result.phase === AsyncStatePhase.RESOLVED && - result.value.sounds.map((sound) => )} - - - {result.phase === AsyncStatePhase.RESOLVED && ( - - )} + <> + {isLoading && ( + + {headers} + + + + + )} + {isSuccess && data && data.sounds.length > 0 && ( + <> + setParams(text)} /> + + {headers} + + {data?.sounds.map((sound) => ( + + ))} + + + + + )} + {isSuccess && data?.sounds.length === 0 && ( + + + {t('No_results_found')} + + )} + + {isError && ( + + + {t('Something_went_wrong')} + + refetch()}>{t('Reload_page')} + + + )} + {context && ( diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index 38e43b9af5bf..c3e2f23091d3 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -91,29 +91,20 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl }, [saveAction, sound, onChange]); const handleDeleteButtonClick = useCallback(() => { - const handleClose = (): void => { - setModal(null); - close?.(); - onChange(); - }; - const handleDelete = async (): Promise => { try { await deleteCustomSound(_id); - setModal(() => ( - - {t('Custom_Sound_Has_Been_Deleted')} - - )); + dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Has_Been_Deleted') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); + } finally { + setModal(null); + close?.(); onChange(); } }; - const handleCancel = (): void => { - setModal(null); - }; + const handleCancel = (): void => setModal(null); setModal(() => ( diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index fa06088048fc..2e5ca8e9afcb 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -13,7 +13,7 @@ export const registerAdminRoute = createRouteGroup( registerAdminRoute('/custom-sounds/:context?/:id?', { name: 'custom-sounds', - component: lazy(() => import('./customSounds/AdminSoundsRoute')), + component: lazy(() => import('./customSounds/CustomSoundsRoute')), }); registerAdminRoute('/apps/what-is-it', { diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index 663b72229594..71bd3119adc5 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IThreadMessage } from '@rocket.chat/core-typings'; import { isThreadMessage } from '@rocket.chat/core-typings'; import { MessageDivider } from '@rocket.chat/fuselage'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; @@ -71,7 +71,7 @@ export const MessageList = ({ rid }: MessageListProps): ReactElement => { data-unread={firstUnread} data-sequential={sequential} sequential={shouldShowAsSequential} - message={message} + message={message as IThreadMessage} /> )} diff --git a/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx index bc0fe3dd776f..388493451d72 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx @@ -1,10 +1,8 @@ -import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { createContext, useContext } from 'react'; export type MessageListContextValue = { - useShowTranslated: ({ message }: { message: IMessage }) => boolean; - useTranslateProvider: ({ message }: { message: IMessage }) => string | boolean; - useTranslateAttachments: ({ message }: { message: IMessage }) => MessageAttachment[]; + useShowTranslated: (message: IMessage) => boolean; useShowStarred: ({ message }: { message: IMessage }) => boolean; useShowFollowing: ({ message }: { message: IMessage }) => boolean; useMessageDateFormatter: () => (date: Date) => string; @@ -32,8 +30,6 @@ export type MessageListContextValue = { export const MessageListContext = createContext({ useShowTranslated: () => false, - useTranslateProvider: () => false, - useTranslateAttachments: () => [], useShowStarred: () => false, useShowFollowing: () => false, useUserHasReacted: () => (): boolean => false, @@ -56,10 +52,6 @@ export const MessageListContext = createContext({ export const useShowTranslated: MessageListContextValue['useShowTranslated'] = (...args) => useContext(MessageListContext).useShowTranslated(...args); -export const useTranslateProvider: MessageListContextValue['useTranslateProvider'] = (...args) => - useContext(MessageListContext).useTranslateProvider(...args); -export const useTranslateAttachments: MessageListContextValue['useTranslateAttachments'] = (...args) => - useContext(MessageListContext).useTranslateAttachments(...args); export const useShowStarred: MessageListContextValue['useShowStarred'] = (...args) => useContext(MessageListContext).useShowStarred(...args); export const useShowFollowing: MessageListContextValue['useShowFollowing'] = (...args) => diff --git a/apps/meteor/client/views/room/MessageList/hooks/useAutoTranslate.ts b/apps/meteor/client/views/room/MessageList/hooks/useAutoTranslate.ts new file mode 100644 index 000000000000..3786f45de75a --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/hooks/useAutoTranslate.ts @@ -0,0 +1,38 @@ +import type { IMessage, ISubscription, ITranslatedMessage } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useCallback, useMemo } from 'react'; + +import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../lib/autoTranslate'; +import { isOwnUserMessage } from '../lib/isOwnUserMessage'; + +export type AutoTranslateOptions = { + autoTranslateEnabled: boolean; + autoTranslateLanguage?: string; + showAutoTranslate: (message: IMessage & Partial) => boolean; +}; + +export const useAutoTranslate = (subscription?: ISubscription): AutoTranslateOptions => { + const autoTranslateSettingEnabled = Boolean(useSetting('AutoTranslate_Enabled')); + const autoTranslateEnabled = Boolean(autoTranslateSettingEnabled && subscription?.autoTranslateLanguage && subscription?.autoTranslate); + const autoTranslateLanguage = autoTranslateEnabled ? subscription?.autoTranslateLanguage : undefined; + + const showAutoTranslate = useCallback( + (message: IMessage): boolean => { + if (!autoTranslateEnabled || !autoTranslateLanguage) { + return false; + } + + return ( + !isOwnUserMessage(message, subscription) && + !(message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse && + (hasTranslationLanguageInMessage(message, autoTranslateLanguage) || + hasTranslationLanguageInAttachments(message.attachments, autoTranslateLanguage)) + ); + }, + [subscription, autoTranslateEnabled, autoTranslateLanguage], + ); + + return useMemo(() => { + return { autoTranslateEnabled, autoTranslateLanguage, showAutoTranslate }; + }, [autoTranslateEnabled, autoTranslateLanguage, showAutoTranslate]); +}; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useAutotranslateLanguage.ts b/apps/meteor/client/views/room/MessageList/hooks/useAutotranslateLanguage.ts deleted file mode 100644 index 08a4a63d27ab..000000000000 --- a/apps/meteor/client/views/room/MessageList/hooks/useAutotranslateLanguage.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { useCallback } from 'react'; - -import { AutoTranslate } from '../../../../../app/autotranslate/client/lib/autotranslate'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; - -export const useAutotranslateLanguage = (rid: IRoom['_id']): string | undefined => - useReactiveValue(useCallback(() => AutoTranslate.getLanguage(rid), [rid])); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useKatex.ts b/apps/meteor/client/views/room/MessageList/hooks/useKatex.ts new file mode 100644 index 000000000000..b652bf283d54 --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/hooks/useKatex.ts @@ -0,0 +1,17 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; + +export const useKatex = (): { + katexEnabled: boolean; + katexDollarSyntaxEnabled: boolean; + katexParenthesisSyntaxEnabled: boolean; +} => { + const katexEnabled = Boolean(useSetting('Katex_Enabled')); + const katexDollarSyntaxEnabled = Boolean(useSetting('Katex_Dollar_Syntax')) && katexEnabled; + const katexParenthesisSyntaxEnabled = Boolean(useSetting('Katex_Parenthesis_Syntax')) && katexEnabled; + + return { + katexEnabled, + katexDollarSyntaxEnabled, + katexParenthesisSyntaxEnabled, + }; +}; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 55c98ec5352b..12d7ed5e87a1 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -1,14 +1,35 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import type { Options, Root } from '@rocket.chat/message-parser'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -export const useMessageBody = ({ attachments, msg }: Partial> = {}): string => - useMemo(() => { - if (msg) { - return msg; +import { parseMessageTextToAstMarkdown } from '../lib/parseMessageTextToAstMarkdown'; +import { useAutoTranslate } from './useAutoTranslate'; + +export const useMessageBody = (message: IMessage | undefined, rid: string): string | Root => { + const subscription = useUserSubscription(rid); + const autoTranslateOptions = useAutoTranslate(subscription); + return useMemo(() => { + if (!message) { + return ''; + } + + if (message.md) { + const parseOptions: Options = { + emoticons: true, + }; + + const messageWithMd = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + + return messageWithMd.md; + } + + if (message.msg) { + return message.msg; } - if (attachments) { - const attachment = attachments.find((attachment) => attachment.title || attachment.description); + if (message.attachments) { + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); if (attachment?.description) { return attachment.description; @@ -20,4 +41,5 @@ export const useMessageBody = ({ attachments, msg }: Partial { - const { autoTranslateLanguage, katex, showColors, useShowTranslated } = useMessageListContext(); + const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); + const subscription = useUserSubscription(rid); + + const autoTranslateOptions = useAutoTranslate(subscription); + const showColors = Boolean(useSetting('HexColorPreview_Enabled')); const hideSysMes = useSetting('Hide_System_Messages'); const hideSysMessagesStable = useStableArray(Array.isArray(hideSysMes) ? hideSysMes : []); @@ -26,16 +31,16 @@ export const useMessages = ({ rid }: { rid: IRoom['_id'] }): MessageWithMdEnforc const parseOptions = { colors: showColors, emoticons: true, - ...(Boolean(katex) && { + ...(katexEnabled && { katex: { - dollarSyntax: katex?.dollarSyntaxEnabled, - parenthesisSyntax: katex?.parenthesisSyntaxEnabled, + dollarSyntax: katexDollarSyntaxEnabled, + parenthesisSyntax: katexParenthesisSyntaxEnabled, }, }), }; return (message: IMessage): MessageWithMdEnforced => - parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateLanguage, useShowTranslated); - }, [autoTranslateLanguage, katex, showColors, useShowTranslated]); + parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); + }, [showColors, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, autoTranslateOptions]); const query: Mongo.Query = useMemo( () => ({ @@ -48,6 +53,6 @@ export const useMessages = ({ rid }: { rid: IRoom['_id'] }): MessageWithMdEnforc ); return useReactiveValue( - useCallback(() => Messages.find(query, options).fetch().map(normalizeMessage), [query, normalizeMessage]), + useCallback(() => ChatMessage.find(query, options).fetch().map(normalizeMessage), [query, normalizeMessage]), ); }; diff --git a/apps/meteor/client/views/room/MessageList/lib/autoTranslate.ts b/apps/meteor/client/views/room/MessageList/lib/autoTranslate.ts new file mode 100644 index 000000000000..a98ea330686b --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/lib/autoTranslate.ts @@ -0,0 +1,8 @@ +import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; +import { isTranslatedMessageAttachment, isTranslatedMessage } from '@rocket.chat/core-typings'; + +export const hasTranslationLanguageInMessage = (message: IMessage, language: string): boolean => + isTranslatedMessage(message) && Boolean(message.translations?.[language]); + +export const hasTranslationLanguageInAttachments = (attachments: MessageAttachment[] = [], language: string): boolean => + isTranslatedMessageAttachment(attachments) && attachments?.some((attachment) => attachment?.translations?.[language]); diff --git a/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts index 0c9c976ca423..359cf6d0e06b 100644 --- a/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts @@ -1,77 +1,99 @@ -import type { IMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isOTRMessage, isQuoteAttachment, isTranslatedMessage } from '@rocket.chat/core-typings'; +import type { IMessage, ITranslatedMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; +import { isE2EEMessage, isOTRMessage, isQuoteAttachment, isTranslatedAttachment, isTranslatedMessage } from '@rocket.chat/core-typings'; import type { Options, Root } from '@rocket.chat/message-parser'; import { parse } from '@rocket.chat/message-parser'; +import type { AutoTranslateOptions } from '../hooks/useAutoTranslate'; import { isParsedMessage } from './isParsedMessage'; type WithRequiredProperty = Omit & { [Property in Key]-?: Type[Property]; }; -export type MessageWithMdEnforced = WithRequiredProperty; -/* +export type MessageWithMdEnforced = IMessage & Partial> = + WithRequiredProperty; +/** * Removes null values for known properties values. * Adds a property `md` to the message with the parsed message if is not provided. * if has `attachments` property, but attachment is missing `md` property, it will be added. * if translation is enabled and message contains `translations` property, it will be replaced by the parsed message. * @param message The message to be parsed. * @param parseOptions The options to be used in the parser. - * @param autoTranslateLanguage The language to be used in the parser. - * @param showTranslatedMessage function that evaluates if message should be translated. + * @param autoTranslateOptions The auto translate options to be used in the parser. * @returns message normalized. */ - -export const parseMessageTextToAstMarkdown = ( +export const parseMessageTextToAstMarkdown = < + TMessage extends IMessage & Partial = IMessage & Partial, +>( message: TMessage, parseOptions: Options, - autoTranslateLanguage?: string, - showTranslated?: ({ message }: { message: IMessage }) => boolean, -): MessageWithMdEnforced => { + autoTranslateOptions: AutoTranslateOptions, +): MessageWithMdEnforced => { const msg = removePossibleNullMessageValues(message); - const translations = autoTranslateLanguage && showTranslated && isTranslatedMessage(msg) && msg.translations; - const translated = autoTranslateLanguage && showTranslated?.({ message }); + const { showAutoTranslate, autoTranslateLanguage } = autoTranslateOptions; + const translations = autoTranslateLanguage && isTranslatedMessage(msg) && msg.translations; + const translated = showAutoTranslate(message); const text = (translated && translations && translations[autoTranslateLanguage]) || msg.msg; return { ...msg, md: - isE2EEMessage(message) || isOTRMessage(message) + isE2EEMessage(message) || isOTRMessage(message) || translated ? textToMessageToken(text, parseOptions) : msg.md ?? textToMessageToken(text, parseOptions), - ...(msg.attachments && { attachments: parseMessageAttachments(msg.attachments, parseOptions) }), - } as MessageWithMdEnforced; + ...(msg.attachments && { + attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }), + }), + }; }; -const parseMessageQuoteAttachment = (quote: T, parseOptions: Options): T => { +export const parseMessageQuoteAttachment = ( + quote: T, + parseOptions: Options, + autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, +): T => { + const { translated, autoTranslateLanguage } = autoTranslateOptions; if (quote.attachments && quote.attachments?.length > 0) { - quote.attachments = quote.attachments.map((attachment) => parseMessageQuoteAttachment(attachment, parseOptions)); + quote.attachments = quote.attachments.map((attachment) => parseMessageQuoteAttachment(attachment, parseOptions, autoTranslateOptions)); } - return { ...quote, md: quote.md ?? textToMessageToken(quote.text, parseOptions) }; + const text = (isTranslatedAttachment(quote) && autoTranslateLanguage && quote?.translations?.[autoTranslateLanguage]) || quote.text || ''; + + return { + ...quote, + md: translated ? textToMessageToken(text, parseOptions) : quote.md ?? textToMessageToken(text, parseOptions), + }; }; -const parseMessageAttachments = (attachments: T[], parseOptions: Options): T[] => { - if (attachments.length === 0) { - return attachments; - } +export const parseMessageAttachments = ( + attachments: T[], + parseOptions: Options, + autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, +): T[] => + attachments.map((attachment) => { + const { translated, autoTranslateLanguage } = autoTranslateOptions; + if (!attachment.text && !attachment.description) { + return attachment; + } - return attachments.map((attachment) => { if (isQuoteAttachment(attachment) && attachment.attachments) { - attachment.attachments = attachment.attachments.map((quoteAttachment) => parseMessageQuoteAttachment(quoteAttachment, parseOptions)); + attachment.attachments = attachment.attachments.map((quoteAttachment) => + parseMessageQuoteAttachment(quoteAttachment, parseOptions, autoTranslateOptions), + ); } - if (!attachment.text) { - return attachment; - } + const text = + (isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) || + attachment.text || + attachment.description || + ''; return { ...attachment, - md: attachment.md ?? textToMessageToken(attachment.text, parseOptions), + md: translated ? textToMessageToken(text, parseOptions) : attachment.md ?? textToMessageToken(text, parseOptions), }; }); -}; const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; @@ -110,6 +132,9 @@ const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): R if (isParsedMessage(textOrRoot)) { return textOrRoot; } + const parsedMessage = parse(textOrRoot, parseOptions); + + const parsedMessageCleaned = parsedMessage[0].type !== 'LINE_BREAK' ? parsedMessage : (parsedMessage.slice(1) as Root); - return parse(textOrRoot, parseOptions); + return parsedMessageCleaned; }; diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 707348392db5..e09b237cf6a3 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -1,17 +1,17 @@ -import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; -import { isTranslatedMessage, isMessageReactionsNormalized, isThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { isMessageReactionsNormalized, isThreadMainMessage } from '@rocket.chat/core-typings'; import { useLayout, useUser, useUserPreference, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import type { VFC, ReactNode } from 'react'; import React, { useMemo, memo } from 'react'; -import { AutoTranslate } from '../../../../../app/autotranslate/client'; import { EmojiPicker } from '../../../../../app/emoji/client'; import { getRegexHighlight, getRegexHighlightUrl } from '../../../../../app/highlight-words/client/helper'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import ToolboxProvider from '../../providers/ToolboxProvider'; import type { MessageListContextValue } from '../contexts/MessageListContext'; import { MessageListContext } from '../contexts/MessageListContext'; -import { useAutotranslateLanguage } from '../hooks/useAutotranslateLanguage'; +import { useAutoTranslate } from '../hooks/useAutoTranslate'; +import { useKatex } from '../hooks/useKatex'; type MessageListProviderProps = { children: ReactNode; @@ -34,10 +34,6 @@ const MessageListProvider: VFC = ({ children }) => { const showRealName = Boolean(useSetting('UI_Use_Real_Name')); const showReadReceipt = Boolean(useSetting('Message_Read_Receipt_Enabled')); - const autoTranslateEnabled = useSetting('AutoTranslate_Enabled'); - const katexEnabled = Boolean(useSetting('Katex_Enabled')); - const katexDollarSyntaxEnabled = Boolean(useSetting('Katex_Dollar_Syntax')); - const katexParenthesisSyntaxEnabled = Boolean(useSetting('Katex_Parenthesis_Syntax')); const showColors = useSetting('HexColorPreview_Enabled') as boolean; const displayRolesGlobal = Boolean(useSetting('UI_DisplayRoles')); @@ -46,7 +42,8 @@ const MessageListProvider: VFC = ({ children }) => { const showUsername = Boolean(!useUserPreference('hideUsernames') && !isMobile); const highlights = useUserPreference('highlights'); - const autoTranslateLanguage = useAutotranslateLanguage(room._id); + const { showAutoTranslate, autoTranslateLanguage } = useAutoTranslate(subscription); + const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); const hasSubscription = Boolean(subscription); @@ -85,30 +82,7 @@ const MessageListProvider: VFC = ({ children }) => { ? ({ message }): boolean => Boolean(message.replies && message.replies.indexOf(uid) > -1 && !isThreadMainMessage(message)) : (): boolean => false, autoTranslateLanguage, - useShowTranslated: - uid && autoTranslateEnabled && hasSubscription && autoTranslateLanguage - ? ({ message }): boolean => - Boolean(message.u) && - message.u?._id !== uid && - isTranslatedMessage(message) && - Boolean(message.translations[autoTranslateLanguage]) && - !message.autoTranslateShowInverse - : (): boolean => false, - useTranslateProvider: - autoTranslateEnabled && autoTranslateLanguage - ? ({ message }): string | boolean => - isTranslatedMessage(message) && AutoTranslate.providersMetadata[message.translationProvider]?.displayName - : (): boolean => false, - useTranslateAttachments: - uid && autoTranslateEnabled && hasSubscription && autoTranslateLanguage - ? ({ message }): MessageAttachment[] => - (isTranslatedMessage(message) && - message.u?._id !== uid && - message.attachments && - AutoTranslate.translateAttachments(message.attachments, autoTranslateLanguage, !!message.autoTranslateShowInverse)) || - message.attachments || - [] - : ({ message }): MessageAttachment[] => message.attachments || [], + useShowTranslated: showAutoTranslate, useShowStarred: hasSubscription ? ({ message }): boolean => Boolean(Array.isArray(message.starred) && message.starred.find((star) => star._id === uid)) : (): boolean => false, @@ -153,7 +127,7 @@ const MessageListProvider: VFC = ({ children }) => { [ username, uid, - autoTranslateEnabled, + showAutoTranslate, hasSubscription, autoTranslateLanguage, showRoles, diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 70cd006a0654..e8a9a0edfdb9 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -330,6 +330,10 @@ const RoomBody = (): ReactElement => { useLegacyMessageEvents({ messageListRef: { get current() { + if (!useLegacyMessageTemplate) { + return null; + } + return wrapperRef.current?.querySelector('ul') ?? null; }, }, @@ -619,7 +623,6 @@ const RoomBody = (): ReactElement => { ; onResize?: () => void; onEscape?: () => void; onSend?: () => void; @@ -24,14 +24,15 @@ export type ComposerMessageProps = { onUploadFiles?: (files: readonly File[]) => void; }; -const ComposerMessage = ({ rid, chatMessagesInstance, readOnly, onSend, ...props }: ComposerMessageProps): ReactElement => { +const ComposerMessage = ({ rid, tmid, readOnly, onSend, ...props }: ComposerMessageProps): ReactElement => { + const chat = useChat(); const dispatchToastMessage = useToastMessageDispatch(); const composerProps = useMemo( () => ({ onJoin: async (): Promise => { try { - await chatMessagesInstance?.data?.joinRoom(); + await chat?.data?.joinRoom(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); throw error; @@ -40,8 +41,8 @@ const ComposerMessage = ({ rid, chatMessagesInstance, readOnly, onSend, ...props onSend: async ({ value: text, tshow }: { value: string; tshow?: boolean }): Promise => { try { - await chatMessagesInstance?.flows.action.stop('typing'); - const newMessageSent = await chatMessagesInstance?.flows.sendMessage({ + await chat?.flows.action.stop('typing'); + const newMessageSent = await chat?.flows.sendMessage({ text, tshow, }); @@ -51,26 +52,19 @@ const ComposerMessage = ({ rid, chatMessagesInstance, readOnly, onSend, ...props } }, onTyping: async (): Promise => { - if (chatMessagesInstance?.composer?.text?.trim() === '') { - await chatMessagesInstance?.flows.action.stop('typing'); + if (chat?.composer?.text?.trim() === '') { + await chat?.flows.action.stop('typing'); return; } - await chatMessagesInstance?.flows.action.start('typing'); + await chat?.flows.action.start('typing'); }, - onNavigateToPreviousMessage: () => chatMessagesInstance?.messageEditing.toPreviousMessage(), - onNavigateToNextMessage: () => chatMessagesInstance?.messageEditing.toNextMessage(), + onNavigateToPreviousMessage: () => chat?.messageEditing.toPreviousMessage(), + onNavigateToNextMessage: () => chat?.messageEditing.toNextMessage(), onUploadFiles: (files: readonly File[]) => { - return chatMessagesInstance?.flows.uploadFiles(files); + return chat?.flows.uploadFiles(files); }, }), - [ - chatMessagesInstance?.data, - chatMessagesInstance?.flows, - chatMessagesInstance?.composer?.text, - chatMessagesInstance?.messageEditing, - dispatchToastMessage, - onSend, - ], + [chat?.data, chat?.flows, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, onSend], ); const publicationReady = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(rid)?.streamActive ?? false, [rid])); @@ -83,16 +77,7 @@ const ComposerMessage = ({ rid, chatMessagesInstance, readOnly, onSend, ...props ); } - return ( - - ); + return ; }; export default memo(ComposerMessage); diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx index 1aa72e2a054a..2d741b2bcd48 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx @@ -44,8 +44,6 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; -type MessageBoxProps = MessageBoxTemplateInstance['data']; - const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -81,7 +79,9 @@ const getEmptyFalse = () => false; const a: any[] = []; const getEmptyArray = () => a; -export const MessageBox = ({ +type MessageBoxProps = Omit; + +const MessageBox = ({ rid, tmid, onSend, diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx index 0aa651d5bd22..b2d0f1920374 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -121,8 +121,8 @@ const ThreadChat: VFC = ({ mainMessage }) => { implements ISubscri }; return this.updateMany(query, update); } + + async setGroupE2EKey(_id: string, key: string): Promise { + const query = { _id }; + const update = { $set: { E2EKey: key } }; + await this.updateOne(query, update); + return this.findOneById(_id); + } + + setGroupE2ESuggestedKey(_id: string, key: string): Promise { + const query = { _id }; + const update = { $set: { E2ESuggestedKey: key } }; + return this.updateOne(query, update); + } + + unsetGroupE2ESuggestedKey(_id: string): Promise { + const query = { _id }; + return this.updateOne(query, { $unset: { E2ESuggestedKey: 1 } }); + } } diff --git a/apps/meteor/server/modules/watchers/publishFields.ts b/apps/meteor/server/modules/watchers/publishFields.ts index c3ea240507bb..42bca8ea05b4 100644 --- a/apps/meteor/server/modules/watchers/publishFields.ts +++ b/apps/meteor/server/modules/watchers/publishFields.ts @@ -36,6 +36,7 @@ export const subscriptionFields = { muteGroupMentions: 1, ignored: 1, E2EKey: 1, + E2ESuggestedKey: 1, tunread: 1, tunreadGroup: 1, tunreadUser: 1, diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useAutoTranslate.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useAutoTranslate.spec.ts new file mode 100644 index 000000000000..be5d21e653c8 --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useAutoTranslate.spec.ts @@ -0,0 +1,82 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; + +const COMPONENT_PATH = '../../../../../../../client/views/room/MessageList/hooks/useAutoTranslate'; +const defaultConfig = { + '@rocket.chat/ui-contexts': { + useSetting: () => true, + }, +}; + +describe('room/MessageList/hooks/useAutoTranslate', () => { + it('should return enabled false and undefined language if no subscription and setting disabled', () => { + const { useAutoTranslate } = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useSetting: () => false, + }, + }); + + const { result } = renderHook(() => useAutoTranslate()); + + expect(result.current.autoTranslateEnabled).to.be.equal(false); + expect(result.current.autoTranslateLanguage).to.be.undefined; + + expect(result.current.showAutoTranslate({ u: { _id: 2 } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 1 }, translations: { lang: 'translated' } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 2 }, translations: { lang: 'translated' } })).to.be.equal(false); + }); + + it('should return enabled false and undefined language if no subscription', () => { + const { useAutoTranslate } = proxyquire.load(COMPONENT_PATH, defaultConfig); + + const { result } = renderHook(() => useAutoTranslate()); + + expect(result.current.autoTranslateEnabled).to.be.equal(false); + expect(result.current.autoTranslateLanguage).to.be.undefined; + + expect(result.current.showAutoTranslate({ u: { _id: 2 } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 1 }, translations: { lang: 'translated' } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 2 }, translations: { lang: 'translated' } })).to.be.equal(false); + }); + + it('should return enabled true and the auto translate language if has subscription', () => { + const { useAutoTranslate } = proxyquire.load(COMPONENT_PATH, defaultConfig); + + const { result } = renderHook(() => useAutoTranslate({ autoTranslate: true, autoTranslateLanguage: 'lang', u: { _id: 1 } })); + + expect(result.current.autoTranslateEnabled).to.be.equal(true); + expect(result.current.autoTranslateLanguage).to.be.equal('lang'); + + expect(result.current.showAutoTranslate({ u: { _id: 2 } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 1 }, translations: { lang: 'translated' } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 2 }, translations: { lang: 'translated' } })).to.be.equal(true); + }); + + it('should return enabled false if no auto translate language', () => { + const { useAutoTranslate } = proxyquire.load(COMPONENT_PATH, defaultConfig); + + const { result } = renderHook(() => useAutoTranslate({ autoTranslate: true, autoTranslateLanguage: undefined, u: { _id: 1 } })); + + expect(result.current.autoTranslateEnabled).to.be.equal(false); + expect(result.current.autoTranslateLanguage).to.be.equal(undefined); + + expect(result.current.showAutoTranslate({ u: { _id: 2 } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 1 }, translations: { lang: 'translated' } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 2 }, translations: { lang: 'translated' } })).to.be.equal(false); + }); + + it('should return enabled false and language undefined if auto translate is false and has auto translate language', () => { + const { useAutoTranslate } = proxyquire.load(COMPONENT_PATH, defaultConfig); + + const { result } = renderHook(() => useAutoTranslate({ autoTranslate: false, autoTranslateLanguage: 'lang', u: { _id: 1 } })); + + expect(result.current.autoTranslateEnabled).to.be.equal(false); + expect(result.current.autoTranslateLanguage).to.be.equal(undefined); + + expect(result.current.showAutoTranslate({ u: { _id: 2 } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 1 }, translations: { lang: 'translated' } })).to.be.equal(false); + expect(result.current.showAutoTranslate({ u: { _id: 2 }, translations: { lang: 'translated' } })).to.be.equal(false); + }); +}); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useKatex.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useKatex.spec.ts new file mode 100644 index 000000000000..1739f070e32a --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/hooks/useKatex.spec.ts @@ -0,0 +1,67 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; + +const COMPONENT_PATH = '../../../../../../../client/views/room/MessageList/hooks/useKatex'; +const defaultConfig = { + '@rocket.chat/ui-contexts': { + useSetting: () => true, + }, +}; + +describe('room/MessageList/hooks/useKatex', () => { + it('should return enabled true dollar syntax true and parenthesis syntax true if all settings is enabled', () => { + const { useKatex } = proxyquire.load(COMPONENT_PATH, defaultConfig); + + const { result } = renderHook(() => useKatex()); + + expect(result.current.katexEnabled).to.be.equal(true); + expect(result.current.katexDollarSyntaxEnabled).to.be.equal(true); + expect(result.current.katexParenthesisSyntaxEnabled).to.be.equal(true); + }); + + it('should return enabled false dollar syntax false and parenthesis syntax false if all settings is disabled', () => { + const { useKatex } = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useSetting: () => false, + }, + }); + + const { result } = renderHook(() => useKatex()); + + expect(result.current.katexEnabled).to.be.equal(false); + expect(result.current.katexDollarSyntaxEnabled).to.be.equal(false); + expect(result.current.katexParenthesisSyntaxEnabled).to.be.equal(false); + }); + + it('should return enabled true dollar syntax false and parenthesis syntax false if Katex_Enabled settings is enable', () => { + const { useKatex } = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useSetting: (str: string) => str === 'Katex_Enabled', + }, + }); + + const { result } = renderHook(() => useKatex()); + + expect(result.current.katexEnabled).to.be.equal(true); + expect(result.current.katexDollarSyntaxEnabled).to.be.equal(false); + expect(result.current.katexParenthesisSyntaxEnabled).to.be.equal(false); + }); + + it('should return enabled false dollar syntax false and parenthesis syntax false if DollarSyntaxEnabled and ParenthesisSyntaxEnabled settings is enable', () => { + const { useKatex } = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useSetting: (str: string) => str === 'DollarSyntaxEnabled' || str === 'ParenthesisSyntaxEnabled', + }, + }); + + const { result } = renderHook(() => useKatex()); + + expect(result.current.katexEnabled).to.be.equal(false); + expect(result.current.katexDollarSyntaxEnabled).to.be.equal(false); + expect(result.current.katexParenthesisSyntaxEnabled).to.be.equal(false); + }); +}); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/autoTranslate.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/autoTranslate.spec.ts new file mode 100644 index 000000000000..bbf6f10cc7ef --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/autoTranslate.spec.ts @@ -0,0 +1,43 @@ +import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { + hasTranslationLanguageInAttachments, + hasTranslationLanguageInMessage, +} from '../../../../../../../client/views/room/MessageList/lib/autoTranslate'; + +describe('autoTranslate', () => { + describe('hasTranslationLanguageInMessage', () => { + const testCases = [ + [{}, '', false], + [{ translations: { en: 'bah' } }, '', false], + [{ translations: { en: 'bah' } }, 'pt', false], + [{ translations: { en: 'bah' } }, 'en', true], + ] as const; + + testCases.forEach(([message, language, expectedResult]) => { + it(`should return ${JSON.stringify(expectedResult)} for ${JSON.stringify(message)} with ${JSON.stringify(language)}`, () => { + const result = hasTranslationLanguageInMessage(message as unknown as IMessage, language); + expect(result).to.be.equal(expectedResult); + }); + }); + }); + + describe('hasTranslationLanguageInAttachments', () => { + const testCases = [ + [[{}], '', false], + [undefined, '', false], + [[{ translations: { en: 'bah' } }], '', false], + [[{ translations: { en: 'bah' } }], 'pt', false], + [[{ translations: { en: 'bah' } }], 'pt', false], + [[{ translations: { en: 'bah' } }], 'en', true], + ] as const; + + testCases.forEach(([attachment, language, expectedResult]) => { + it(`should return ${JSON.stringify(expectedResult)} for ${JSON.stringify(attachment)} with ${JSON.stringify(language)}`, () => { + const result = hasTranslationLanguageInAttachments(attachment as unknown as MessageAttachment[], language); + expect(result).to.be.equal(expectedResult); + }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts index 35225bcb4a9c..756765fc42bf 100644 --- a/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts @@ -1,6 +1,6 @@ /* eslint-env mocha */ import type { Options, Root } from '@rocket.chat/message-parser'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { parseMessageTextToAstMarkdown } from '../../../../../../../client/views/room/MessageList/lib/parseMessageTextToAstMarkdown'; @@ -124,13 +124,193 @@ const baseMessage: IMessage = { urls: [], }; +const autoTranslateOptions = { + autoTranslateEnabled: false, + showAutoTranslate: () => false, +}; + +const quoteMessage = { + author_name: 'authorName', + author_link: 'link', + author_icon: 'icon', + md: [], +}; + describe('parseMessage', () => { it('should return md property populated if the message is parsed', () => { - expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions).md).to.deep.equal(messageParserTokenMessage); + expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions, autoTranslateOptions).md).to.deep.equal(messageParserTokenMessage); }); it('should return correct parsed md property populated and fail in comparison with different Root element', () => { - expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions).md).to.not.deep.equal(messageParserTokenMessageWithWrongData); + expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions, autoTranslateOptions).md).to.not.deep.equal( + messageParserTokenMessageWithWrongData, + ); + }); + + describe('translated', () => { + const translatedMessage: ITranslatedMessage = { + ...baseMessage, + msg: 'message not translated', + translationProvider: 'provider', + translations: { + en: 'message translated', + }, + }; + const translatedMessageParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message translated', + }, + ], + }, + ]; + + const enabledAutoTranslatedOptions = { + autoTranslateEnabled: true, + autoTranslateLanguage: 'en', + showAutoTranslate: () => true, + }; + it('should return correct translated parsed md when translate is active', () => { + expect(parseMessageTextToAstMarkdown(translatedMessage, parseOptions, enabledAutoTranslatedOptions).md).to.deep.equal( + translatedMessageParsed, + ); + }); + + it('should return correct attachment translated parsed md when translate is active', () => { + const attachmentTranslatedMessage = { + ...translatedMessage, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + }, + ], + }; + const attachmentTranslatedMessageParsed = { + ...translatedMessage, + md: translatedMessageParsed, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'description translated', + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageTextToAstMarkdown(attachmentTranslatedMessage, parseOptions, enabledAutoTranslatedOptions)).to.deep.equal( + attachmentTranslatedMessageParsed, + ); + }); + + it('should return correct attachment quote translated parsed md when translate is active', () => { + const attachmentTranslatedMessage = { + ...translatedMessage, + attachments: [ + { + text: 'text', + translations: { + en: 'text translated', + }, + }, + ], + }; + const attachmentTranslatedMessageParsed = { + ...translatedMessage, + md: translatedMessageParsed, + attachments: [ + { + text: 'text', + translations: { + en: 'text translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'text translated', + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageTextToAstMarkdown(attachmentTranslatedMessage, parseOptions, enabledAutoTranslatedOptions)).to.deep.equal( + attachmentTranslatedMessageParsed, + ); + }); + + it('should return correct multiple attachment quote translated parsed md when translate is active', () => { + const attachmentTranslatedMessage = { + ...translatedMessage, + attachments: [ + { + text: 'text', + translations: { + en: 'text translated', + }, + attachments: [{ ...quoteMessage, text: 'text level 2', translations: { en: 'text level 2 translated' } }], + }, + ], + }; + const attachmentTranslatedMessageParsed = { + ...translatedMessage, + md: translatedMessageParsed, + attachments: [ + { + text: 'text', + translations: { + en: 'text translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'text translated', + }, + ], + }, + ], + attachments: [ + { + ...quoteMessage, + text: 'text level 2', + translations: { + en: 'text level 2 translated', + }, + }, + ], + }, + ], + }; + + expect(parseMessageTextToAstMarkdown(attachmentTranslatedMessage, parseOptions, enabledAutoTranslatedOptions)).to.deep.equal( + attachmentTranslatedMessageParsed, + ); + }); }); // TODO: Add more tests for each type of message and for each type of token diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageAttachments.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageAttachments.spec.ts new file mode 100644 index 000000000000..b1910210b80f --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageAttachments.spec.ts @@ -0,0 +1,236 @@ +/* eslint-env mocha */ +import type { Options, Root } from '@rocket.chat/message-parser'; +import { expect } from 'chai'; + +import { parseMessageAttachments } from '../../../../../../../client/views/room/MessageList/lib/parseMessageTextToAstMarkdown'; + +const parseOptions: Options = { + colors: true, + emoticons: true, + katex: { + dollarSyntax: true, + parenthesisSyntax: true, + }, +}; + +const messageParserTokenMessage: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message ', + }, + { + type: 'BOLD', + value: [ + { + type: 'PLAIN_TEXT', + value: 'bold', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' ', + }, + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'italic', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'STRIKE', + value: [ + { + type: 'PLAIN_TEXT', + value: 'strike', + }, + ], + }, + ], + }, +]; + +const autoTranslateOptions = { + autoTranslateEnabled: false, + translated: false, +}; + +const attachmentMessage = [ + { + description: 'message **bold** _italic_ and ~strike~', + md: messageParserTokenMessage, + }, +]; + +describe('parseMessageAttachments', () => { + it('should return md property populated if the message is parsed', () => { + expect(parseMessageAttachments(attachmentMessage, parseOptions, autoTranslateOptions)[0].md).to.deep.equal(messageParserTokenMessage); + }); + + it('should return md property populated if the attachment is not parsed', () => { + expect(parseMessageAttachments([{ ...attachmentMessage[0], md: undefined }], parseOptions, autoTranslateOptions)[0].md).to.deep.equal( + messageParserTokenMessage, + ); + }); + + describe('translated', () => { + const enabledAutoTranslatedOptions = { + translated: true, + autoTranslateLanguage: 'en', + }; + + it('should return correct attachment description translated parsed md when translate is active', () => { + const descriptionAttachment = [ + { + ...attachmentMessage[0], + description: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const descriptionAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(descriptionAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).to.deep.equal( + descriptionAttachmentParsed, + ); + }); + + it('should return correct attachment description parsed md when translate is active and auto translate language is undefined', () => { + const descriptionAttachment = [ + { + ...attachmentMessage[0], + description: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const descriptionAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment not translated', + }, + ], + }, + ]; + + expect( + parseMessageAttachments(descriptionAttachment, parseOptions, { + ...enabledAutoTranslatedOptions, + autoTranslateLanguage: undefined, + })[0].md, + ).to.deep.equal(descriptionAttachmentParsed); + }); + + it('should return correct attachment text translated parsed md when translate is active', () => { + const textAttachment = [ + { + ...attachmentMessage[0], + text: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const textAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(textAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).to.deep.equal(textAttachmentParsed); + }); + + it('should return correct attachment text translated parsed md when translate is active and has multiple texts', () => { + const quote = { + author_name: 'authorName', + author_link: 'link', + author_icon: 'icon', + message_link: 'messageLink', + md: [], + text: 'text level 2', + translations: { en: 'text level 2 translated' }, + }; + const textAttachment = [ + { + ...quote, + text: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + attachments: [quote], + }, + ]; + const textAttachmentParsed = { + ...textAttachment[0], + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ], + attachments: [ + { + ...quote, + text: 'text level 2', + translations: { + en: 'text level 2 translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'text level 2 translated', + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageAttachments(textAttachment, parseOptions, enabledAutoTranslatedOptions)[0]).to.deep.equal(textAttachmentParsed); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageQuoteAttachment.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageQuoteAttachment.spec.ts new file mode 100644 index 000000000000..69f7f35b2230 --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessageQuoteAttachment.spec.ts @@ -0,0 +1,196 @@ +/* eslint-env mocha */ +import type { Options, Root } from '@rocket.chat/message-parser'; +import type { MessageQuoteAttachment } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { parseMessageQuoteAttachment } from '../../../../../../../client/views/room/MessageList/lib/parseMessageTextToAstMarkdown'; + +const parseOptions: Options = { + colors: true, + emoticons: true, + katex: { + dollarSyntax: true, + parenthesisSyntax: true, + }, +}; + +const messageParserTokenMessage: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message ', + }, + { + type: 'BOLD', + value: [ + { + type: 'PLAIN_TEXT', + value: 'bold', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' ', + }, + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'italic', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'STRIKE', + value: [ + { + type: 'PLAIN_TEXT', + value: 'strike', + }, + ], + }, + ], + }, +]; + +const autoTranslateOptions = { + autoTranslateEnabled: false, + translated: false, +}; + +const quoteMessage = { + author_name: 'authorName', + author_link: 'link', + author_icon: 'icon', + text: 'message **bold** _italic_ and ~strike~', + md: messageParserTokenMessage, +}; + +describe('parseMessageQuoteAttachment', () => { + it('should return md property populated if the quote is parsed', () => { + expect(parseMessageQuoteAttachment(quoteMessage, parseOptions, autoTranslateOptions).md).to.deep.equal(messageParserTokenMessage); + }); + + it('should return md property populated if the quote is not parsed', () => { + expect( + parseMessageQuoteAttachment( + { ...quoteMessage, md: undefined } as unknown as MessageQuoteAttachment, + parseOptions, + autoTranslateOptions, + ).md, + ).to.deep.equal(messageParserTokenMessage); + }); + + describe('translated', () => { + const translatedQuote = { + ...quoteMessage, + text: 'quote not translated', + translationProvider: 'provider', + translations: { + en: 'quote translated', + }, + }; + const translatedMessageParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'quote translated', + }, + ], + }, + ]; + + const enabledAutoTranslatedOptions = { + translated: true, + autoTranslateLanguage: 'en', + }; + it('should return correct quote translated parsed md when translate is active', () => { + expect(parseMessageQuoteAttachment(translatedQuote, parseOptions, enabledAutoTranslatedOptions).md).to.deep.equal( + translatedMessageParsed, + ); + }); + + it('should return text parsed md when translate is active and autoTranslateLanguage is undefined', () => { + expect( + parseMessageQuoteAttachment(translatedQuote, parseOptions, { ...enabledAutoTranslatedOptions, autoTranslateLanguage: undefined }) + .md, + ).to.deep.equal([ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'quote not translated', + }, + ], + }, + ]); + }); + + it('should return correct multiple attachment quote translated parsed md when translate is active', () => { + const quote = { ...quoteMessage, text: 'text level 2', translations: { en: 'text level 2 translated' } }; + + const multipleQuotes = { + ...translatedQuote, + attachments: [ + { + ...translatedQuote, + text: 'text', + translations: { + en: 'text translated', + }, + attachments: [quote], + }, + ], + }; + const multipleQuotesParsed = { + ...translatedQuote, + md: translatedMessageParsed, + attachments: [ + { + ...multipleQuotes.attachments[0], + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'text translated', + }, + ], + }, + ], + attachments: [ + { + ...quote, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'text level 2 translated', + }, + ], + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageQuoteAttachment(multipleQuotes, parseOptions, enabledAutoTranslatedOptions)).to.deep.equal(multipleQuotesParsed); + }); + }); +}); diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 725cb672054f..efcd7725102d 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts @@ -27,8 +27,4 @@ export type MessageAttachmentDefault = { thumb_url?: string; color?: string; - - translations?: { - [language: string]: string; - }; } & MessageAttachmentBase; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/TranslatedMessageAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/TranslatedMessageAttachment.ts new file mode 100644 index 000000000000..711f56598546 --- /dev/null +++ b/packages/core-typings/src/IMessage/MessageAttachment/TranslatedMessageAttachment.ts @@ -0,0 +1,12 @@ +import type { MessageAttachment } from './MessageAttachment'; +import type { MessageAttachmentDefault } from './MessageAttachmentDefault'; + +export interface ITranslatedMessageAttachment extends MessageAttachmentDefault { + translations: { [language: string]: string }; +} + +export const isTranslatedAttachment = (attachment: MessageAttachment): attachment is ITranslatedMessageAttachment => + 'translations' in attachment; + +export const isTranslatedMessageAttachment = (attachments: MessageAttachment[]): attachments is ITranslatedMessageAttachment[] => + attachments?.some(isTranslatedAttachment); diff --git a/packages/core-typings/src/IMessage/MessageAttachment/index.ts b/packages/core-typings/src/IMessage/MessageAttachment/index.ts index 2bbfa99f0a36..82f8613b5e29 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/index.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/index.ts @@ -5,3 +5,4 @@ export * from './MessageAttachmentAction'; export * from './MessageAttachmentBase'; export * from './MessageAttachmentDefault'; export * from './MessageQuoteAttachment'; +export * from './TranslatedMessageAttachment'; diff --git a/packages/core-typings/src/ISubscription.ts b/packages/core-typings/src/ISubscription.ts index cbe9e84f8342..d94dca42d48e 100644 --- a/packages/core-typings/src/ISubscription.ts +++ b/packages/core-typings/src/ISubscription.ts @@ -40,6 +40,7 @@ export interface ISubscription extends IRocketChatRecord { onHold?: boolean; encrypted?: boolean; E2EKey?: string; + E2ESuggestedKey?: string; unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing'; fname?: string; diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index b6f8f9f7eefc..f0b80986dfec 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -77,4 +77,10 @@ export interface ISubscriptionsModel extends IBaseModel { setAlertForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise; setOpenForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise; + + setGroupE2EKey(_id: string, key: string): Promise; + + setGroupE2ESuggestedKey(_id: string, key: string): Promise; + + unsetGroupE2ESuggestedKey(_id: string): Promise; } diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index ac86c66316f3..d14cc642fc88 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -99,6 +99,12 @@ export type E2eEndpoints = { '/v1/e2e.updateGroupKey': { POST: (params: E2eUpdateGroupKeyProps) => void; }; + '/v1/e2e.acceptSuggestedGroupKey': { + POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void; + }; + '/v1/e2e.rejectSuggestedGroupKey': { + POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void; + }; '/v1/e2e.setRoomKeyID': { POST: (params: E2eSetRoomKeyIdProps) => void; };