From 02b69c59a09c72c10e335c0879ca85225845d729 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 28 Feb 2023 16:02:16 -0300 Subject: [PATCH 1/3] Regression: Fix mentioning rooms with special chars (#28206) --- .../client/views/room/providers/ComposerPopupProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index dd0e61ebe7b8..0c9aea8d6bcb 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -145,7 +145,7 @@ const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room: const { rooms = [] } = await userSpotlight(filter, [], { rooms: true, mentions: true }, rid); return rooms as unknown as ComposerBoxPopupRoomProps[]; }, - getValue: (item) => `${item.fname || item.name}`, + getValue: (item) => `${item.name || item.fname}`, renderItem: ({ item }) => , }) as any, createMessageBoxPopupConfig({ From fe1a36e8892574dc89d61e5d265e5aaa821716a7 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 1 Mar 2023 01:04:41 +0530 Subject: [PATCH 2/3] Regression: Denied Microphone permission disables composer (#28133) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../client/messageBox/createComposerAPI.ts | 26 ++++- apps/meteor/client/lib/chats/ChatAPI.ts | 3 + .../AudioMessageRecorder.tsx | 68 ++----------- .../body/composer/messageBox/MessageBox.tsx | 8 +- .../MessageBoxActionsToolbar.tsx | 12 ++- .../actions/AudioMessageAction.tsx | 97 ++++++++++++++++++- .../rocketchat-i18n/i18n/en.i18n.json | 3 + 7 files changed, 150 insertions(+), 67 deletions(-) diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index f648566203ec..d92bbc125a9a 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -21,7 +21,14 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) input.dispatchEvent(event); }; - const emitter = new Emitter<{ quotedMessagesUpdate: void; editing: void; recording: void; recordingVideo: void; formatting: void }>(); + const emitter = new Emitter<{ + quotedMessagesUpdate: void; + editing: void; + recording: void; + recordingVideo: void; + formatting: void; + mircophoneDenied: void; + }>(); let _quotedMessages: IMessage[] = []; @@ -167,6 +174,21 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) ]; })(); + const [isMicrophoneDenied, setIsMicrophoneDenied] = (() => { + let isMicrophoneDenied = false; + + return [ + { + get: () => isMicrophoneDenied, + subscribe: (callback: () => void) => emitter.on('mircophoneDenied', callback), + }, + (value: boolean) => { + isMicrophoneDenied = value; + emitter.emit('mircophoneDenied'); + }, + ]; + })(); + const setEditingMode = (editing: boolean): void => { setEditing(editing); }; @@ -317,5 +339,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) dismissAllQuotedMessages, quotedMessages, formatters, + isMicrophoneDenied, + setIsMicrophoneDenied, }; }; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index a96b81c44ffc..a7891cc3dcdb 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -53,6 +53,9 @@ export type ComposerAPI = { setRecordingVideo(recording: boolean): void; readonly recordingVideo: Subscribable; + setIsMicrophoneDenied(isMicrophoneDenied: boolean): void; + readonly isMicrophoneDenied: Subscribable; + readonly formatters: Subscribable; }; diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx index f64482202967..bfe2b0a8f2ff 100644 --- a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -1,9 +1,9 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement, AllHTMLAttributes } from 'react'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useState } from 'react'; import { AudioRecorder } from '../../../../app/ui/client'; @@ -14,16 +14,15 @@ const audioRecorder = new AudioRecorder(); type AudioMessageRecorderProps = { rid: IRoom['_id']; - tmid?: IMessage['_id']; chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; + isMicrophoneDenied?: boolean; +}; -const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): ReactElement | null => { +const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => { const t = useTranslation(); const [state, setState] = useState<'loading' | 'recording'>('recording'); const [time, setTime] = useState('00:00'); - const [isMicrophoneDenied, setIsMicrophoneDenied] = useState(false); const [recordingInterval, setRecordingInterval] = useState | null>(null); const [recordingRoomId, setRecordingRoomId] = useState(null); @@ -36,42 +35,13 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): setTime('00:00'); - const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); - chat?.action.stop('recording'); chat?.composer?.setRecordingMode(false); - return blob; - }); - - const handleMount = useMutableCallback(async (): Promise => { - if (navigator.permissions) { - try { - const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); - setIsMicrophoneDenied(permissionStatus.state === 'denied'); - permissionStatus.onchange = (): void => { - setIsMicrophoneDenied(permissionStatus.state === 'denied'); - }; - return; - } catch (error) { - console.warn(error); - } - } - - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - setIsMicrophoneDenied(true); - return; - } + const blob = await new Promise((resolve) => audioRecorder.stop(resolve)); - try { - if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { - setIsMicrophoneDenied(true); - return; - } - } catch (error) { - console.warn(error); - } + return blob; }); const handleUnmount = useMutableCallback(async () => { @@ -101,7 +71,6 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): setRecordingRoomId(rid); } catch (error) { console.log(error); - setIsMicrophoneDenied(true); chat?.composer?.setRecordingMode(false); } }); @@ -124,29 +93,12 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): }); useEffect(() => { - handleMount(); handleRecord(); return () => { handleUnmount(); }; - }, [handleMount, handleUnmount, handleRecord]); - - const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; - const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; - const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; - const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; - - const isAllowed = useMemo( - () => - audioRecorder.isSupported() && - !isMicrophoneDenied && - isFileUploadEnabled && - isAudioRecorderEnabled && - (!fileUploadMediaTypeBlackList || !fileUploadMediaTypeBlackList.match(/audio\/mp3|audio\/\*/i)) && - (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)), - [fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied], - ); + }, [handleUnmount, handleRecord]); const stateClass = useMemo(() => { if (recordingRoomId && recordingRoomId !== rid) { @@ -156,7 +108,7 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): return state && `rc-message-box__audio-message--${state}`; }, [recordingRoomId, rid, state]); - if (!isAllowed) { + if (isMicrophoneDenied) { return null; } 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 8bd92d8dd9f4..b7c2f238e6c1 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 @@ -261,6 +261,11 @@ const MessageBox = ({ subscribe: chat.composer?.recording.subscribe ?? emptySubscribe, }); + const isMicrophoneDenied = useSubscription({ + getCurrentValue: chat.composer?.isMicrophoneDenied.get ?? getEmptyFalse, + subscribe: chat.composer?.isMicrophoneDenied.subscribe ?? emptySubscribe, + }); + const isRecordingVideo = useSubscription({ getCurrentValue: chat.composer?.recordingVideo.get ?? getEmptyFalse, subscribe: chat.composer?.recordingVideo.subscribe ?? emptySubscribe, @@ -381,7 +386,7 @@ const MessageBox = ({ {isRecordingVideo && } - {isRecordingAudio && } + {isRecordingAudio && } } aria-label={t('Message')} @@ -421,6 +426,7 @@ const MessageBox = ({ canJoin={canJoin} rid={rid} tmid={tmid} + isMicrophoneDenied={isMicrophoneDenied} /> diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index c4a0888b33f9..9ea65fd8c440 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage } from '@rocket.chat/core-typings'; import React, { memo } from 'react'; import ActionsToolbarDropdown from './ActionsToolbarDropdown'; @@ -13,7 +13,8 @@ type MessageBoxActionsToolbarProps = { canSend: boolean; canJoin: boolean; rid: IRoom['_id']; - tmid?: string; + tmid?: IMessage['_id']; + isMicrophoneDenied?: boolean; }; const MessageBoxActionsToolbar = ({ @@ -24,10 +25,15 @@ const MessageBoxActionsToolbar = ({ rid, tmid, canJoin, + isMicrophoneDenied, }: MessageBoxActionsToolbarProps) => { const actions = [ , - , + , , ]; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx index ec97e93b0134..a7e68d432cca 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx @@ -1,25 +1,114 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { AudioRecorder } from '../../../../../../../../../app/ui/client'; import type { ChatAPI } from '../../../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../../../contexts/ChatContext'; +const audioRecorder = new AudioRecorder(); + type AudioMessageActionProps = { chatContext?: ChatAPI; + isMicrophoneDenied?: boolean; } & Omit, 'is'>; -const AudioMessageAction = ({ chatContext, ...props }: AudioMessageActionProps) => { +const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...props }: AudioMessageActionProps) => { const t = useTranslation(); const chat = useChat() ?? chatContext; + const stopRecording = useMutableCallback(() => { + chat?.action.stop('recording'); + + chat?.composer?.setRecordingMode(false); + }); + + const setMicrophoneDenied = useMutableCallback((isDenied) => { + if (isDenied) { + stopRecording(); + } + + chat?.composer?.setIsMicrophoneDenied(isDenied); + }); + const handleRecordButtonClick = () => chat?.composer?.setRecordingMode(true); + const handleMount = useMutableCallback(async (): Promise => { + if (navigator.permissions) { + try { + const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); + setMicrophoneDenied(permissionStatus.state === 'denied'); + permissionStatus.onchange = (): void => { + setMicrophoneDenied(permissionStatus.state === 'denied'); + }; + return; + } catch (error) { + console.warn(error); + } + } + + if (!navigator.mediaDevices?.enumerateDevices) { + setMicrophoneDenied(true); + return; + } + + try { + if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { + setMicrophoneDenied(true); + return; + } + } catch (error) { + console.warn(error); + } + }); + + useEffect(() => { + handleMount(); + }, [handleMount]); + + const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; + const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; + const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; + const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; + + const isAllowed = useMemo( + () => + audioRecorder.isSupported() && + !isMicrophoneDenied && + isFileUploadEnabled && + isAudioRecorderEnabled && + !fileUploadMediaTypeBlackList?.match(/audio\/mp3|audio\/\*/i) && + (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)), + [fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied], + ); + + const getTranslationKey = useMemo(() => { + if (isMicrophoneDenied) { + return t('Microphone_access_not_allowed'); + } + + if (!isFileUploadEnabled) { + return t('File_Upload_Disabled'); + } + + if (!isAudioRecorderEnabled) { + return t('Message_Audio_Recording_Disabled'); + } + + if (!isAllowed) { + return t('error-not-allowed'); + } + + return t('Audio_message'); + }, [isMicrophoneDenied, isFileUploadEnabled, isAudioRecorderEnabled, isAllowed, t]); + return ( Date: Tue, 28 Feb 2023 14:17:15 -0600 Subject: [PATCH 3/3] Regression: Avoid rendering unsupported media on PDFs & update quote styling (#28048) Co-authored-by: Filipe Marins <9275105+filipemarins@users.noreply.github.com> Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Co-authored-by: Aleksander Nicacio da Silva <6494543+aleksandernsilva@users.noreply.github.com> --- .../theme/client/imports/general/base_old.css | 2 +- .../client/sidebar/RoomList/RoomList.tsx | 2 +- apps/meteor/client/sidebar/Sidebar.tsx | 2 +- .../VideoMessageRecorder.tsx | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 +- .../rocketchat-i18n/i18n/es.i18n.json | 1 + .../src/OmnichannelTranscript.ts | 85 ++++++-- ee/packages/pdf-worker/.storybook/preview.js | 22 +- .../src/strategies/ChatTranscript.ts | 10 + .../ChatTranscript/ChatTranscript.fixtures.ts | 200 +++++++++++++++++- .../ChatTranscript/components/Files.tsx | 1 + .../ChatTranscript/components/Header.tsx | 3 +- .../components/MessageHeader.tsx | 5 +- .../ChatTranscript/components/MessageList.tsx | 3 +- .../ChatTranscript/components/Quotes.tsx | 51 +++++ .../src/templates/ChatTranscript/index.tsx | 7 +- .../markup/blocks/CodeBlock.tsx | 2 +- .../markup/elements/CodeSpan.tsx | 18 +- .../markup/elements/InlineElements.tsx | 12 +- .../markup/elements/LinkSpan.tsx | 2 +- 20 files changed, 377 insertions(+), 55 deletions(-) create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/Quotes.tsx diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 4d7d722abce0..f0dce0ec5192 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -1821,7 +1821,7 @@ } } -@media (width <= 780px) { +@media (max-width: 767px) { .rc-old.main-content { transition: right 0.25s cubic-bezier(0.5, 0, 0.1, 1), transform 0.1s linear; will-change: transform; diff --git a/apps/meteor/client/sidebar/RoomList/RoomList.tsx b/apps/meteor/client/sidebar/RoomList/RoomList.tsx index 4c3ad5454e46..ab0f9c729aea 100644 --- a/apps/meteor/client/sidebar/RoomList/RoomList.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomList.tsx @@ -104,7 +104,7 @@ const RoomList = (): ReactElement => { padding-block-start: 12px; } - @media (width <= 400px) { + @media (max-width: 400px) { padding: 0 calc(var(--sidebar-small-default-padding) - 4px); &__type, diff --git a/apps/meteor/client/sidebar/Sidebar.tsx b/apps/meteor/client/sidebar/Sidebar.tsx index 444ee98be3ce..af3bf7077cf8 100644 --- a/apps/meteor/client/sidebar/Sidebar.tsx +++ b/apps/meteor/client/sidebar/Sidebar.tsx @@ -37,7 +37,7 @@ const Sidebar = () => { transform: translate3d(0px, 0px, 0px); } - @media (width < 768px) { + @media (max-width: 767px) { position: absolute; user-select: none; transform: translate3d(-100%, 0, 0); diff --git a/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx b/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx index 9de98e868353..e85b20f5a308 100644 --- a/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx @@ -20,7 +20,7 @@ const videoContainerClass = css` transform: scaleX(-1); filter: FlipH; - @media (width <= 500px) { + @media (max-width: 500px) { & > video { width: 100%; height: 100%; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 46a190d3acea..322d87ab9cf8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5684,7 +5684,7 @@ "Customer": "Customer", "Time": "Time", "Omnichannel_Agent": "Omnichannel Agent", - "This_attachment_is_not_supported": "This attachment is not supported", + "This_attachment_is_not_supported": "Attachment format not supported", "Send_transcript": "Send transcript", "Undo_request": "Undo request", "No_permission": "No permission", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index af5c65eb48a6..f4359eef798b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -4235,6 +4235,7 @@ "This_month": "Este mes", "This_room_has_been_archived_by__username_": "Esta sala ha sido archivada por __username__", "This_room_has_been_unarchived_by__username_": "Esta sala ha sido desarchivada por __username__", + "This_attachment_is_not_supported": "El formato de archivo no es soportado", "This_week": "Esta semana", "thread": "hilo", "Thread_message": "Comentado en el mensaje de *__username__'s*: _ __msg__ _", diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index e11939502b96..81c9ef11b72f 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -1,8 +1,10 @@ import { LivechatRooms, Messages, Uploads, Users, LivechatVisitors } from '@rocket.chat/models'; import { PdfWorker } from '@rocket.chat/pdf-worker'; import type { Templates } from '@rocket.chat/pdf-worker'; +import { parse } from '@rocket.chat/message-parser'; +import type { Root } from '@rocket.chat/message-parser'; import type { IMessage, IUser, IRoom, IUpload, ILivechatVisitor, ILivechatAgent } from '@rocket.chat/core-typings'; -import { isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; +import { isQuoteAttachment, isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; import { ServiceClass, Upload as uploadService, @@ -29,8 +31,11 @@ type WorkDetailsWithSource = WorkDetails & { from: string; }; -type MessageWithFiles = Pick & { +type Quote = { name: string; ts?: Date; md: Root }; + +type MessageData = Pick & { files: ({ name?: string; buffer: Buffer | null; extension?: string } | undefined)[]; + quotes: (Quote | undefined)[]; }; type WorkerData = { @@ -38,7 +43,7 @@ type WorkerData = { visitor: ILivechatVisitor | null; agent: ILivechatAgent | undefined; closedAt?: Date; - messages: MessageWithFiles[]; + messages: MessageData[]; timezone: string; dateFormat: string; timeAndDateFormat: string; @@ -140,24 +145,70 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT }); } - private async getFiles(userId: string, messages: IMessage[]): Promise { - const messagesWithFiles: MessageWithFiles[] = []; + private getQuotesFromMessage(message: IMessage): Quote[] { + const quotes: Quote[] = []; + + if (!message.attachments) { + return quotes; + } + + for (const attachment of message.attachments) { + if (isQuoteAttachment(attachment)) { + const { text, author_name: name, md, ts } = attachment; + + if (text) { + quotes.push({ + name, + md: md ?? parse(text), + ts, + }); + } + + quotes.push(...this.getQuotesFromMessage({ attachments: attachment.attachments } as IMessage)); + } + } + + return quotes; + } + + private async getMessagesData(userId: string, messages: IMessage[]): Promise { + const messagesData: MessageData[] = []; for await (const message of messages) { if (!message.attachments || !message.attachments.length) { // If there's no attachment and no message, what was sent? lol - messagesWithFiles.push({ _id: message._id, files: [], ts: message.ts, u: message.u, msg: message.msg, md: message.md }); + messagesData.push({ + _id: message._id, + files: [], + quotes: [], + ts: message.ts, + u: message.u, + msg: message.msg, + md: message.md, + }); continue; } - const files = []; + const quotes = []; for await (const attachment of message.attachments) { - if (isFileAttachment(attachment) && attachment.type !== 'file') { + if (isQuoteAttachment(attachment)) { + quotes.push(...this.getQuotesFromMessage(message)); + continue; + } + + if (!isFileAttachment(attachment)) { + this.log.error(`Invalid attachment type ${(attachment as any).type} for file ${attachment.title} in room ${message.rid}!`); + // ignore other types of attachments + continue; + } + if (!isFileImageAttachment(attachment)) { this.log.error(`Invalid attachment type ${attachment.type} for file ${attachment.title} in room ${message.rid}!`); // ignore other types of attachments + files.push({ name: attachment.title, buffer: null }); continue; } - if (isFileAttachment(attachment) && isFileImageAttachment(attachment) && !this.worker.isMimeTypeValid(attachment.image_type)) { + + if (!this.worker.isMimeTypeValid(attachment.image_type)) { this.log.error(`Invalid mime type ${attachment.image_type} for file ${attachment.title} in room ${message.rid}!`); // ignore invalid mime types files.push({ name: attachment.title, buffer: null }); @@ -201,10 +252,18 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT // So, we'll fetch the the msg, if empty, go for the first description on an attachment, if empty, empty string const msg = message.msg || message.attachments.find((attachment) => attachment.description)?.description || ''; // Remove nulls from final array - messagesWithFiles.push({ _id: message._id, msg, u: message.u, files: files.filter(Boolean), ts: message.ts }); + messagesData.push({ + _id: message._id, + msg, + u: message.u, + files: files.filter(Boolean), + quotes, + ts: message.ts, + md: message.md, + }); } - return messagesWithFiles; + return messagesData; } private async getTranslations(): Promise> { @@ -243,7 +302,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT const agent = room.servedBy && (await Users.findOneAgentById(room.servedBy._id, { projection: { _id: 1, name: 1, username: 1, utcOffset: 1 } })); - const messagesFiles = await this.getFiles(details.userId, messages); + const messagesData = await this.getMessagesData(details.userId, messages); const [siteName, dateFormat, timeAndDateFormat, timezone, translations] = await Promise.all([ settingsService.get('Site_Name'), @@ -257,7 +316,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT agent, closedAt: room.closedAt, siteName, - messages: messagesFiles, + messages: messagesData, dateFormat, timeAndDateFormat, timezone, diff --git a/ee/packages/pdf-worker/.storybook/preview.js b/ee/packages/pdf-worker/.storybook/preview.js index abd704f79510..1992b608a61b 100644 --- a/ee/packages/pdf-worker/.storybook/preview.js +++ b/ee/packages/pdf-worker/.storybook/preview.js @@ -1,19 +1,19 @@ -import '../../../apps/meteor/app/theme/client/main.css'; +import '../../../../apps/meteor/app/theme/client/main.css'; import 'highlight.js/styles/github.css'; export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -} + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; export const decorators = [ (Story) => ( -
+
- ) + ), ]; diff --git a/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts b/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts index 3aea23e60c02..95490bc48843 100644 --- a/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts +++ b/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts @@ -16,18 +16,28 @@ export class ChatTranscript implements IStrategy { const { ts, ...rest } = message; const formattedTs = moment(ts).tz(timezone).format(timeAndDateFormat); const isDivider = this.isNewDay(message, previousMessage, timezone); + const formattedQuotes = message.quotes?.length + ? message.quotes.map((quote) => { + return { + ...quote, + ts: moment(quote.ts).tz(timezone).format(timeAndDateFormat), + }; + }) + : undefined; if (isDivider) { return { ...rest, ts: formattedTs, divider: moment(ts).tz(timezone).format(dateFormat), + quotes: formattedQuotes, }; } return { ...rest, ts: formattedTs, + quotes: formattedQuotes, }; }); } diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts index 1ffa48c47c87..bf4726892457 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts @@ -91,7 +91,7 @@ export const exampleData = { { key: 'Date', value: 'Date' }, { key: 'Customer', value: 'Customer' }, { key: 'Time', value: 'Time' }, - { key: 'This_attachment_is_not_supported', value: 'This attachment is not supported' }, + { key: 'This_attachment_is_not_supported', value: 'Attachment format not supported' }, ], messages: [ { @@ -163,6 +163,23 @@ export const exampleData = { name: 'Juanito De Ponce', username: 'juanito.ponce', }, + quotes: [ + { + name: 'Christian Castro', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.', + }, + ], + }, + ], + }, + ], md: [ { type: 'PARAGRAPH', @@ -191,9 +208,56 @@ export const exampleData = { }, ], }, + { + msg: 'Consectetur adipiscing eli.', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + md: [ + { + type: 'PARAGRAPH', + value: [{ type: 'PLAIN_TEXT', value: 'I am having trouble with my password. ' }], + }, + ], + quotes: [ + { + name: 'Juanito De Ponce', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.', + }, + ], + }, + ], + }, + { + name: 'Juanito De Ponce', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Adipiscing elit, sed do eiusmod tempor incididunt ut labore.', + }, + ], + }, + ], + }, + ], + }, { msg: 'You are welcome. Have a great day!', - ts: '2022-11-22T16:00:00.000Z', + ts: '2022-11-21T16:00:00.000Z', u: { _id: '123', name: 'Juanito De Ponce', @@ -230,13 +294,143 @@ export const exampleData = { }, }, { - msg: 'No, I am good. Thanks!', ts: '2022-11-22T16:00:00.000Z', u: { _id: '321', name: 'Christian Castro', username: 'cristiano.castro', }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Ac aliquet odio mattis. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat ', + }, + { + type: 'BOLD', + value: [ + { + type: 'PLAIN_TEXT', + value: 'bold ', + }, + ], + }, + { + type: 'STRIKE', + value: [ + { + type: 'PLAIN_TEXT', + value: 'strike ', + }, + ], + }, + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'italic ', + }, + ], + }, + { + type: 'BOLD', + value: [ + { + type: 'STRIKE', + value: [ + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'all together', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'LINK', + value: { + label: [{ type: 'PLAIN_TEXT', value: ' ' }], + src: { type: 'PLAIN_TEXT', value: 'linktoquote' }, + }, + }, + { type: 'PLAIN_TEXT', value: 'Consectetur adipiscing elit. Nunc vulputate libero et velit interdum, ac aliquet odio mattis.' }, + ], + }, + ], + quotes: [ + { + name: 'Juanito De Ponce', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + }, + ], + }, + ], + }, + { + name: 'Christian Castro', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + }, + ], + }, + ], + }, + { + name: 'Juanito De Ponce', + time: '2022-11-21T16:00:00.000Z', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + }, + ], + }, + ], + }, + ], }, ], }; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx index 6c46437b3a96..baa7f0a27940 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx @@ -22,6 +22,7 @@ const styles = StyleSheet.create({ textAlign: 'center', borderColor: colors.n250, borderWidth: 1, + borderRadius: 4, paddingVertical: 8, marginTop: 4, }, diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx index f9f33d677b4b..63f7f8acb185 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx @@ -7,7 +7,6 @@ const styles = StyleSheet.create({ padding: 32, borderRadius: 4, backgroundColor: colors.n100, - color: colors.n900, marginBottom: 16, }, headerText: { @@ -18,6 +17,7 @@ const styles = StyleSheet.create({ borderBottomWidth: 2, marginBottom: 16, paddingBottom: 16, + color: colors.n900, }, pagination: { fontSize: fontScales.c1.fontSize, @@ -30,6 +30,7 @@ const styles = StyleSheet.create({ subtitle: { fontSize: fontScales.p2m.fontSize, fontWeight: fontScales.p2m.fontWeight, + color: colors.n900, }, container: { fontSize: fontScales.c1.fontSize, diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx index 9914dabc1f93..478a55cbbe68 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx @@ -14,14 +14,13 @@ const styles = StyleSheet.create({ }, time: { fontSize: fontScales.c1.fontSize, - color: colors.n700, marginLeft: 4, }, }); -export const MessageHeader = ({ name, time }: { name: string; time: string }) => ( +export const MessageHeader = ({ name, time, light }: { name: string; time: string; light?: boolean }) => ( {name} - {time} + {time} ); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx index 8c42d2245c0d..2c0cfa2e99c9 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx @@ -6,6 +6,7 @@ import { MessageHeader } from './MessageHeader'; import { Files } from './Files'; import type { ChatTranscriptData } from '..'; import { Markup } from '../markup'; +import { Quotes } from './Quotes'; const styles = StyleSheet.create({ wrapper: { @@ -15,7 +16,6 @@ const styles = StyleSheet.create({ message: { marginTop: 1, fontSize: fontScales.p2.fontSize, - textAlign: 'justify', }, }); @@ -26,6 +26,7 @@ export const MessageList = ({ messages, invalidFileMessage }: { messages: ChatTr {message.divider && } {message.md ? : {message.msg}} + {message.quotes && } {message.files && } ))} diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Quotes.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Quotes.tsx new file mode 100644 index 000000000000..fbc5871d4e3b --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Quotes.tsx @@ -0,0 +1,51 @@ +import { View, StyleSheet } from '@react-pdf/renderer'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +import type { Quote as QuoteType } from '..'; +import { MessageHeader } from './MessageHeader'; +import { Markup } from '../markup'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.n100, + borderWidth: 1, + borderColor: colors.n250, + borderLeftColor: colors.n600, + padding: 16, + borderTopWidth: 1, + borderBottomWidth: 1, + }, + quoteMessage: { + marginTop: 6, + fontSize: fontScales.p2.fontSize, + }, +}); + +const Quote = ({ quote, children, index }: { quote: QuoteType; children: JSX.Element | null; index: number }) => ( + + + + + + + + + {children} + +); + +export const Quotes = ({ quotes }: { quotes: QuoteType[] }) => + quotes.reduceRight( + (lastQuote, quote, index) => ( + + {lastQuote} + + ), + null, + ); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx index 6e666a6a840a..bd61eb76aa44 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx @@ -2,6 +2,8 @@ import * as path from 'path'; import ReactPDF, { Font, Document, Page, StyleSheet } from '@react-pdf/renderer'; import type { ILivechatAgent, ILivechatVisitor, IMessage, Serialized } from '@rocket.chat/core-typings'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import type { Root } from '@rocket.chat/message-parser'; import { Header } from './components/Header'; import { MessageList } from './components/MessageList'; @@ -10,9 +12,11 @@ const FONT_PATH = path.resolve(__dirname, '../../public'); export type PDFFile = { name?: string; buffer: Buffer | null; extension?: 'png' | 'jpg' }; +export type Quote = { md: Root; name: string; ts: string }; + export type PDFMessage = Serialized, 'files'>> & { files?: PDFFile[]; -} & { divider?: string }; +} & { divider?: string } & { quotes?: Quote[] }; export type ChatTranscriptData = { header: { @@ -30,6 +34,7 @@ const styles = StyleSheet.create({ page: { fontFamily: 'Inter', lineHeight: 1.25, + color: colors.n800, }, wrapper: { paddingHorizontal: 32, diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx index 2d11b3c89a79..4badb13eea3d 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx @@ -9,7 +9,7 @@ type CodeBlockProps = { }; const CodeBlock = ({ lines }: CodeBlockProps): ReactElement => ( - + {lines.map((line, index) => ( {line.value?.value || ' '} diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx index c7f19a3e5184..6e95c6d428b8 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx @@ -1,18 +1,22 @@ -import { StyleSheet, View, Text } from '@react-pdf/renderer'; +import { StyleSheet, Text } from '@react-pdf/renderer'; import colors from '@rocket.chat/fuselage-tokens/colors.json'; export const codeStyles = StyleSheet.create({ wrapper: { - backgroundColor: colors.n250, - borderColor: colors.n100, + backgroundColor: colors.n100, + borderColor: colors.n250, borderWidth: 1, borderRadius: 4, paddingHorizontal: 3, paddingVertical: 1, }, + space: { + backgroundColor: colors.n100, + }, code: { - fontSize: 13, + backgroundColor: colors.n100, color: colors.n800, + fontSize: 13, fontWeight: 700, fontFamily: 'FiraCode', }, @@ -23,9 +27,11 @@ type CodeSpanProps = { }; const CodeSpan = ({ code }: CodeSpanProps) => ( - + + {code} - + + ); export default CodeSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx index cb847d992fce..cca11ebd5f6d 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx @@ -1,4 +1,4 @@ -import { StyleSheet, Text, View } from '@react-pdf/renderer'; +import { Text } from '@react-pdf/renderer'; import type * as MessageParser from '@rocket.chat/message-parser'; import BoldSpan from './BoldSpan'; @@ -12,14 +12,8 @@ type InlineElementsProps = { children: MessageParser.Inlines[]; }; -const styles = StyleSheet.create({ - inline: { - flexDirection: 'row', - }, -}); - const InlineElements = ({ children }: InlineElementsProps) => ( - + {children.map((child, index) => { switch (child.type) { case 'BOLD': @@ -51,7 +45,7 @@ const InlineElements = ({ children }: InlineElementsProps) => ( return null; } })} - + ); export default InlineElements; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx index 396345c1b083..3d9f15cb07e6 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx @@ -18,7 +18,7 @@ const LinkSpan = ({ label }: LinkSpanProps): ReactElement => { const labelElements = labelArray.map((child, index) => { switch (child.type) { case 'PLAIN_TEXT': - return {child.value}; + return {child.value.trim()}; case 'STRIKE': return ;