From 4ee6d2fafcaf440657debb9ada0e1a691ed414fb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Apr 2024 18:16:10 -0300 Subject: [PATCH 001/215] POC --- apps/meteor/app/e2e/client/helper.js | 8 ++ .../app/e2e/client/rocketchat.e2e.room.js | 42 +++++----- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 +- .../server/methods/sendFileMessage.ts | 79 +++++++++++-------- .../message/content/Attachments.tsx | 4 +- .../content/attachments/AttachmentsItem.tsx | 4 +- .../attachments/file/ImageAttachment.tsx | 2 + .../attachments/structure/AttachmentImage.tsx | 53 ++++++++++++- .../variants/room/RoomMessageContent.tsx | 7 +- .../client/lib/chats/flows/uploadFiles.ts | 13 ++- 10 files changed, 151 insertions(+), 65 deletions(-) diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.js index 2e0843ee33801..49b157c5ccf45 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.js @@ -53,6 +53,10 @@ export async function encryptAES(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } +export async function encryptAESCTR(vector, key, data) { + return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); +} + export async function decryptRSA(key, data) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } @@ -65,6 +69,10 @@ export async function generateAESKey() { return crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); } +export async function generateAESCTRKey() { + return crypto.subtle.generateKey({ name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']); +} + export async function generateRSAKey() { return crypto.subtle.generateKey( { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 9e2f72d381158..aafb2c8b99c72 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -23,6 +23,8 @@ import { importAESKey, importRSAKey, readFileAsArrayBuffer, + encryptAESCTR, + generateAESCTRKey, } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; @@ -335,42 +337,40 @@ export class E2ERoom extends Emitter { // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return; - } + // if (!this.isSupportedRoomType(this.typeOfRoom)) { + // return; + // } const fileArrayBuffer = await readFileAsArrayBuffer(file); const vector = crypto.getRandomValues(new Uint8Array(16)); + const key = await generateAESCTRKey(); let result; try { - result = await encryptAES(vector, this.groupSessionKey, fileArrayBuffer); + result = await encryptAESCTR(vector, key, fileArrayBuffer); } catch (error) { + console.log(error); return this.error('Error encrypting group key: ', error); } - const output = joinVectorAndEcryptedData(vector, result); + const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const encryptedFile = new File([toArrayBuffer(EJSON.stringify(output))], file.name); + const encryptedFile = new File([toArrayBuffer(result)], file.name); - return encryptedFile; + return { + file: encryptedFile, + key: exportedKey, + iv: Base64.encode(vector), + type: file.type, + }; } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(message) { - if (message[0] !== '{') { - return; - } + async decryptFile(file, key, iv) { + const ivArray = Base64.decode(iv); + const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(message)); - - try { - return await decryptAES(vector, this.groupSessionKey, cipherText); - } catch (error) { - this.error('Error decrypting file: ', error); - - return false; - } + return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); } // Encrypts messages @@ -440,7 +440,7 @@ export class E2ERoom extends Emitter { return { ...message, msg: data.text, - e2e: 'done', + // e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 472e719599337..57944ddd0297c 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,7 @@ class E2E extends Emitter { ...message, ...(data && { msg: data.text, - e2e: 'done', + // e2e: 'done', }), }; @@ -474,7 +474,7 @@ class E2E extends Emitter { return { ...message, attachments: decryptedAttachments, - e2e: 'done', + // e2e: 'done', }; } diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 08e3225ad9a1c..6234090371f6d 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -33,6 +33,7 @@ export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, user: IUser, + msgData?: Record, ): Promise => { validateFileRequiredFields(file); @@ -42,6 +43,16 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; + let e2e; + console.log(msgData?.e2e); + if (msgData?.e2e) { + e2e = JSON.parse(msgData.e2e); + } + + if (e2e.type) { + file.type = e2e.type; + } + const files = [ { _id: file._id, @@ -68,39 +79,39 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); - } - } catch (e) { - SystemLogger.error(e); - } + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // } + // } catch (e) { + // SystemLogger.error(e); + // } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { @@ -191,7 +202,7 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user, msgData); const msg = await executeSendMessage(userId, { rid: roomId, diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index dea78a3f65135..02de21e120aa1 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -11,12 +11,12 @@ type AttachmentsProps = { isMessageEncrypted?: boolean; }; -const Attachments = ({ attachments, id, isMessageEncrypted = false }: AttachmentsProps): ReactElement => { +const Attachments = ({ attachments, id, isMessageEncrypted = false, message }: AttachmentsProps): ReactElement => { return ( <> {attachments?.map((attachment, index) => ( - + ))} diff --git a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx index dd4f9bac9d282..9adf6b29f87f7 100644 --- a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx +++ b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx @@ -12,9 +12,9 @@ type AttachmentsItemProps = { id: string | undefined; }; -const AttachmentsItem = ({ attachment, id }: AttachmentsItemProps): ReactElement => { +const AttachmentsItem = ({ attachment, id, message }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment)) { - return ; + return ; } if (isQuoteAttachment(attachment)) { 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 819d6581b3148..81d06f4d9bd2f 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -22,6 +22,7 @@ const ImageAttachment = ({ title_link: link, title_link_download: hasDownload, collapsed, + message, }: ImageAttachmentProps) => { const [loadImage, setLoadImage] = useLoadImage(); const getURL = useMediaUrl(); @@ -38,6 +39,7 @@ const ImageAttachment = ({ src={getURL(url)} previewUrl={`data:image/png;base64,${imagePreview}`} id={id} + message={message} /> diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 5b251ad6143f1..0c4c6c9db6572 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -1,9 +1,11 @@ import type { Dimensions } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useAttachmentDimensions } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { FC } from 'react'; import React, { memo, useState, useMemo } from 'react'; +import { e2e } from '../../../../../../app/e2e/client'; import ImageBox from './image/ImageBox'; import Load from './image/Load'; import Retry from './image/Retry'; @@ -37,7 +39,7 @@ const getDimensions = ( return { width, height, ratio: (height / width) * 100 }; }; -const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { +const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, message, ...size }) => { const limits = useAttachmentDimensions(); const [error, setError] = useState(false); @@ -51,6 +53,53 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo [], ); + async function imageUrlToBase64(blob: Blob) { + return new Promise((onSuccess, onError) => { + try { + const reader = new FileReader(); + reader.onload = function () { + onSuccess(this.result); + }; + reader.readAsDataURL(blob); + } catch (e) { + onError(e); + } + }); + } + + async function imageUrlArrayBuffer(blob: Blob) { + return new Promise((onSuccess, onError) => { + try { + const reader = new FileReader(); + reader.onload = function () { + onSuccess(this.result); + }; + reader.readAsArrayBuffer(blob); + } catch (e) { + onError(e); + } + }); + } + + const base64 = useQuery( + [src], + async () => { + const file = await fetch(src); + const blob = await file.blob(); + if (message.e2e) { + const { key, iv } = JSON.parse(message.e2e); + const e2eRoom = await e2e.getInstanceByRoomId(message.rid); + + const file = await e2eRoom.decryptFile(await imageUrlArrayBuffer(blob), key, iv); + return imageUrlToBase64(new Blob([file])); + } + return imageUrlToBase64(blob); + }, + { + suspense: true, + }, + ); + const dimensions = getDimensions(width, height, limits); const background = previewUrl && `url(${previewUrl}) center center / cover no-repeat fixed`; @@ -82,7 +131,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo data-id={id} className='gallery-item' data-src={dataSrc || src} - src={src} + src={base64.data} alt='' width={dimensions.width} height={dimensions.height} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4bd548393d022..146c9ac72bb24 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -68,7 +68,12 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {!!normalizedMessage?.attachments?.length && ( - + )} {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 801ac2aa883cf..fe4c4832e6686 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -70,7 +70,18 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedDescription = await e2eRoom.encryptAttachmentDescription(description, Random.id()); - uploadFile(file, encryptedDescription, { t: 'e2e', e2e: 'pending' }); + const encryptedFile = await e2eRoom.encryptFile(file); + + if (encryptedFile) { + uploadFile(encryptedFile.file, encryptedDescription, { + t: 'e2e', + e2e: JSON.stringify({ + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }), + }); + } }, invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), }, From a1c05694ecee8e5b7d41dde49885e84093022563 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 25 Apr 2024 21:25:19 -0300 Subject: [PATCH 002/215] Initial service worker implementation --- .../message/content/Attachments.tsx | 4 +- .../content/attachments/AttachmentsItem.tsx | 4 +- .../attachments/file/ImageAttachment.tsx | 2 - .../attachments/structure/AttachmentImage.tsx | 53 +----------------- .../variants/room/RoomMessageContent.tsx | 28 ++++++++-- .../client/lib/chats/flows/uploadFiles.ts | 13 +++++ apps/meteor/public/enc.js | 55 +++++++++++++++++++ 7 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 apps/meteor/public/enc.js diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index 02de21e120aa1..dea78a3f65135 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -11,12 +11,12 @@ type AttachmentsProps = { isMessageEncrypted?: boolean; }; -const Attachments = ({ attachments, id, isMessageEncrypted = false, message }: AttachmentsProps): ReactElement => { +const Attachments = ({ attachments, id, isMessageEncrypted = false }: AttachmentsProps): ReactElement => { return ( <> {attachments?.map((attachment, index) => ( - + ))} diff --git a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx index 9adf6b29f87f7..dd4f9bac9d282 100644 --- a/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx +++ b/apps/meteor/client/components/message/content/attachments/AttachmentsItem.tsx @@ -12,9 +12,9 @@ type AttachmentsItemProps = { id: string | undefined; }; -const AttachmentsItem = ({ attachment, id, message }: AttachmentsItemProps): ReactElement => { +const AttachmentsItem = ({ attachment, id }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment)) { - return ; + return ; } if (isQuoteAttachment(attachment)) { 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 81d06f4d9bd2f..819d6581b3148 100644 --- a/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/ImageAttachment.tsx @@ -22,7 +22,6 @@ const ImageAttachment = ({ title_link: link, title_link_download: hasDownload, collapsed, - message, }: ImageAttachmentProps) => { const [loadImage, setLoadImage] = useLoadImage(); const getURL = useMediaUrl(); @@ -39,7 +38,6 @@ const ImageAttachment = ({ src={getURL(url)} previewUrl={`data:image/png;base64,${imagePreview}`} id={id} - message={message} /> diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 0c4c6c9db6572..5b251ad6143f1 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -1,11 +1,9 @@ import type { Dimensions } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useAttachmentDimensions } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; import type { FC } from 'react'; import React, { memo, useState, useMemo } from 'react'; -import { e2e } from '../../../../../../app/e2e/client'; import ImageBox from './image/ImageBox'; import Load from './image/Load'; import Retry from './image/Retry'; @@ -39,7 +37,7 @@ const getDimensions = ( return { width, height, ratio: (height / width) * 100 }; }; -const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, message, ...size }) => { +const AttachmentImage: FC = ({ id, previewUrl, dataSrc, loadImage = true, setLoadImage, src, ...size }) => { const limits = useAttachmentDimensions(); const [error, setError] = useState(false); @@ -53,53 +51,6 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo [], ); - async function imageUrlToBase64(blob: Blob) { - return new Promise((onSuccess, onError) => { - try { - const reader = new FileReader(); - reader.onload = function () { - onSuccess(this.result); - }; - reader.readAsDataURL(blob); - } catch (e) { - onError(e); - } - }); - } - - async function imageUrlArrayBuffer(blob: Blob) { - return new Promise((onSuccess, onError) => { - try { - const reader = new FileReader(); - reader.onload = function () { - onSuccess(this.result); - }; - reader.readAsArrayBuffer(blob); - } catch (e) { - onError(e); - } - }); - } - - const base64 = useQuery( - [src], - async () => { - const file = await fetch(src); - const blob = await file.blob(); - if (message.e2e) { - const { key, iv } = JSON.parse(message.e2e); - const e2eRoom = await e2e.getInstanceByRoomId(message.rid); - - const file = await e2eRoom.decryptFile(await imageUrlArrayBuffer(blob), key, iv); - return imageUrlToBase64(new Blob([file])); - } - return imageUrlToBase64(blob); - }, - { - suspense: true, - }, - ); - const dimensions = getDimensions(width, height, limits); const background = previewUrl && `url(${previewUrl}) center center / cover no-repeat fixed`; @@ -131,7 +82,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo data-id={id} className='gallery-item' data-src={dataSrc || src} - src={base64.data} + src={src} alt='' width={dimensions.width} height={dimensions.height} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 146c9ac72bb24..5bc16ddfdf43e 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,3 +1,4 @@ +import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; @@ -46,6 +47,26 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + if (normalizedMessage?.attachments?.length) { + normalizedMessage.attachments.forEach((attachment) => { + if (!normalizedMessage.e2e) { + return; + } + console.log(attachment); + + const key = Base64.encode(normalizedMessage.e2e); + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (attachment.image_url && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (attachment.audio_url && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + }); + } + return ( <> {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( @@ -68,12 +89,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {!!normalizedMessage?.attachments?.length && ( - + )} {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fe4c4832e6686..7ee613eb23f1e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -9,6 +9,19 @@ import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; +if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/enc.js', { + scope: '/', + }) + .then((reg) => { + if (reg.active) console.log('service worker installed'); + }) + .catch((err) => { + console.log(`registration failed: ${err}`); + }); +} + export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js new file mode 100644 index 0000000000000..25b74110b11ae --- /dev/null +++ b/apps/meteor/public/enc.js @@ -0,0 +1,55 @@ +function base64Decode (string) { + string = atob(string); + const + length = string.length, + buf = new ArrayBuffer(length), + bufView = new Uint8Array(buf); + for (var i = 0; i < string.length; i++) { bufView[i] = string.charCodeAt(i) } + return buf +} + +function base64DecodeString (string) { + return atob(string); +} + +self.addEventListener('fetch', (event) => { + if (!event.request.url.includes('/file-decrypt/')) { + return; + } + + const url = new URL(event.request.url); + const k = base64DecodeString(url.searchParams.get('key')); + + console.log(url); + const { + key, + iv + } = JSON.parse(k); + + const newUrl = url.href.replace('/file-decrypt/', '/'); + + const requestToFetch = new Request(newUrl, event.request); + + event.respondWith( + caches.match(requestToFetch).then((response) => { + if (response) { + console.log('cached'); + return response; + } + + return fetch(requestToFetch) + .then(async (response) => { + const file = await response.arrayBuffer(); + const ivArray = base64Decode(iv); + const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); + const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); + return new Response(result); + }) + .catch((error) => { + console.error("Fetching failed:", error); + + throw error; + }); + }), + ); +}); From 640ed9ed45c6426869ccc7f60d63362c24e9784a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 27 Apr 2024 17:15:00 -0300 Subject: [PATCH 003/215] Start changing the upload to two calls --- apps/meteor/app/api/server/v1/rooms.ts | 84 +++++++++++++++++++ .../server/methods/sendFileMessage.ts | 2 +- apps/meteor/client/lib/chats/uploads.ts | 20 +++-- .../server/models/raw/BaseUploadModel.ts | 2 + packages/rest-typings/src/v1/rooms.ts | 19 +++++ 5 files changed, 120 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 9576a79f6678e..c4150419f76a5 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -6,6 +6,7 @@ import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps } import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; +import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/methods/eraseRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; @@ -139,6 +140,7 @@ API.v1.addRoute( }, ); +// TODO: deprecate API API.v1.addRoute( 'rooms.upload/:rid', { authRequired: true }, @@ -194,6 +196,88 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.media/:rid', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await getUploadFormData( + { + request: this.request, + }, + { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, + ); + + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; + + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + }; + + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } + + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + + const fileUrl = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + return API.v1.success({ + file: { + _id: uploadedFile._id, + url: fileUrl, + }, + }); + }, + }, +); + +API.v1.addRoute( + 'rooms.mediaConfirm/:rid/:fileId', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { + throw new Meteor.Error('invalid-file'); + } + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + + await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + + return API.v1.success({ + message, + }); + }, + }, +); + API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 6234090371f6d..4f7a613ae2097 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -49,7 +49,7 @@ export const parseFileIntoMessageAttachments = async ( e2e = JSON.parse(msgData.e2e); } - if (e2e.type) { + if (e2e?.type) { file.type = e2e.type; } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index a3425aa29b206..14340e535677b 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -61,14 +61,9 @@ const send = async ( try { await new Promise((resolve, reject) => { const xhr = sdk.rest.upload( - `/v1/rooms.upload/${rid}`, + `/v1/rooms.media/${rid}`, { - msg, - tmid, file, - description, - t, - e2e, }, { load: (event) => { @@ -115,6 +110,19 @@ const send = async ( }, ); + xhr.onload = async () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { + msg, + tmid, + description, + t, + e2e, + }); + } + }; + if (uploads.length) { UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); } diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index 98c12965b9ffb..eac646e443882 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -34,6 +34,8 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.insertOne(fileData); } + // TODO: upload as temporary, create a cron to delete non used ones and a way to mark as used + updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { if (!fileId) { return; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index b6a3f7be6b4b8..733baa710c511 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -600,6 +600,25 @@ export type RoomsEndpoints = { }) => { message: IMessage | null }; }; + '/v1/rooms.media/:rid': { + POST: (params: { file: File }) => { file: { url: string } }; + }; + + '/v1/rooms.mediaConfirm/:rid/:fileId': { + POST: (params: { + description?: string; + avatar?: string; + emoji?: string; + alias?: string; + groupable?: boolean; + msg?: string; + tmid?: string; + customFields?: string; + t?: IMessage['t']; + e2e?: IMessage['e2e']; + }) => { message: IMessage | null }; + }; + '/v1/rooms.saveNotification': { POST: (params: { roomId: string; notifications: Notifications }) => void; }; From 59d99a6686ef35c187e0d069d4ec68f092bab7bb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 27 Apr 2024 21:40:46 -0300 Subject: [PATCH 004/215] Start using content property in place of e2e --- .../file-upload/server/methods/sendFileMessage.ts | 13 ++++++------- .../message/variants/room/RoomMessageContent.tsx | 6 ++++-- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 14 +++++++++----- apps/meteor/client/lib/chats/uploads.ts | 13 +++++++++++-- packages/core-typings/src/IMessage/IMessage.ts | 2 ++ packages/rest-typings/src/v1/rooms.ts | 4 ++++ 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 4f7a613ae2097..f14bc07804dae 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -43,14 +43,12 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; - let e2e; - console.log(msgData?.e2e); - if (msgData?.e2e) { - e2e = JSON.parse(msgData.e2e); - } + if (msgData?.content) { + const content = JSON.parse(msgData.content); - if (e2e?.type) { - file.type = e2e.type; + if (content?.file.type) { + file.type = content?.file.type; + } } const files = [ @@ -199,6 +197,7 @@ export const sendFileMessage = async ( customFields: Match.Optional(String), t: Match.Optional(String), e2e: Match.Optional(String), + content: Match.Optional(String), }), ); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 5bc16ddfdf43e..ad9ac8aa73f20 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -49,12 +49,14 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.e2e) { + if (!normalizedMessage.content) { return; } console.log(attachment); - const key = Base64.encode(normalizedMessage.e2e); + const content = JSON.parse(normalizedMessage.content); + + const key = Base64.encode(JSON.stringify(content.file)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 7ee613eb23f1e..6da707cb8c155 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -31,7 +31,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - const uploadFile = (file: File, description?: string, extraData?: Pick) => { + const uploadFile = (file: File, description?: string, extraData?: Pick) => { chat.uploads.send(file, { description, msg, @@ -88,10 +88,14 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi if (encryptedFile) { uploadFile(encryptedFile.file, encryptedDescription, { t: 'e2e', - e2e: JSON.stringify({ - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, + // TODO: Encrypt content + content: JSON.stringify({ + file: { + // url + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }, }), }); } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 14340e535677b..2ccd110d81b39 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -38,6 +38,7 @@ const send = async ( tmid, t, e2e, + content, }: { description?: string; msg?: string; @@ -45,6 +46,7 @@ const send = async ( tmid?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; + content?: string; }, ): Promise => { const id = Random.id(); @@ -119,6 +121,7 @@ const send = async ( description, t, e2e, + content, }); } }; @@ -162,6 +165,12 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e }), + { + description, + msg, + t, + e2e, + content, + }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; content?: string }, + ): Promise => send(file, { description, msg, rid, tmid, t, e2e, content }), }); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index fc73ede6ece43..a4470a3c19241 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,6 +223,8 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; + + content?: string; } export type MessageSystem = { diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 733baa710c511..176ebe40217b2 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -616,6 +616,10 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; + content?: string; + encryption?: { + version: string; + }; }) => { message: IMessage | null }; }; From d84ce58afbc31c37d59897cf4be9bbf03f11d313 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 10:15:09 -0300 Subject: [PATCH 005/215] Readd e2e property --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 2 +- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index aafb2c8b99c72..9ba79ea674c19 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -440,7 +440,7 @@ export class E2ERoom extends Emitter { return { ...message, msg: data.text, - // e2e: 'done', + e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 57944ddd0297c..472e719599337 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,7 @@ class E2E extends Emitter { ...message, ...(data && { msg: data.text, - // e2e: 'done', + e2e: 'done', }), }; @@ -474,7 +474,7 @@ class E2E extends Emitter { return { ...message, attachments: decryptedAttachments, - // e2e: 'done', + e2e: 'done', }; } From e6a4330582839f89aa4c8cf4986db8675da413ec Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 11:01:41 -0300 Subject: [PATCH 006/215] Client side attachment generation --- .../server/methods/sendFileMessage.ts | 74 +++++++--------- .../variants/room/RoomMessageContent.tsx | 13 ++- .../client/lib/chats/flows/uploadFiles.ts | 85 ++++++++++++++++--- apps/meteor/client/lib/chats/uploads.ts | 19 ++--- 4 files changed, 124 insertions(+), 67 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index f14bc07804dae..9f74b099fa95b 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -43,14 +43,6 @@ export const parseFileIntoMessageAttachments = async ( const attachments: MessageAttachment[] = []; - if (msgData?.content) { - const content = JSON.parse(msgData.content); - - if (content?.file.type) { - file.type = content?.file.type; - } - } - const files = [ { _id: file._id, @@ -77,39 +69,39 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // } - // } catch (e) { - // SystemLogger.error(e); - // } + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + } + } catch (e) { + SystemLogger.error(e); + } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index ad9ac8aa73f20..4d932a2580a5f 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -47,16 +47,21 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + if (normalizedMessage.content) { + // TODO: decrypt + const content = JSON.parse(normalizedMessage.content); + normalizedMessage.attachments = content.attachments; + normalizedMessage.fileInfo = content.file; + } + if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content) { + if (!normalizedMessage.fileInfo) { return; } console.log(attachment); - const content = JSON.parse(normalizedMessage.content); - - const key = Base64.encode(JSON.stringify(content.file)); + const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 6da707cb8c155..1b451e465bf7c 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; @@ -31,12 +31,21 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - const uploadFile = (file: File, description?: string, extraData?: Pick) => { - chat.uploads.send(file, { - description, - msg, - ...extraData, - }); + const uploadFile = ( + file: File, + description?: string, + extraData?: Pick, + getContent?: (fileId: string, fileUrl: string) => Promise, + ) => { + chat.uploads.send( + file, + { + description, + msg, + ...extraData, + }, + getContent, + ); chat.composer?.clear(); imperativeModal.close(); uploadNextFile(); @@ -86,18 +95,70 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - uploadFile(encryptedFile.file, encryptedDescription, { - t: 'e2e', + const getContent = async (_id: string, fileUrl: string) => { + const attachments = []; + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + }; + + if (/^image\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + }); + + // if (file.identify?.size) { + // attachment.image_dimensions = file.identify.size; + // } + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + // format: getFileExtension(file.name), + }); + } + // TODO: Encrypt content - content: JSON.stringify({ + return JSON.stringify({ file: { // url key: encryptedFile.key, iv: encryptedFile.iv, type: file.type, }, - }), - }); + attachments, + }); + }; + + uploadFile( + encryptedFile.file, + encryptedDescription, + { + t: 'e2e', + }, + getContent, + ); } }, invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 2ccd110d81b39..9e621d853aec3 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -38,7 +38,6 @@ const send = async ( tmid, t, e2e, - content, }: { description?: string; msg?: string; @@ -46,8 +45,8 @@ const send = async ( tmid?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; - content?: string; }, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { const id = Random.id(); @@ -115,6 +114,11 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); + let content; + if (getContent) { + content = await getContent(result.file._id, result.file.url); + } + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { msg, tmid, @@ -165,12 +169,7 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { - description, - msg, - t, - e2e, - content, - }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e']; content?: string }, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e, content }), + { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, + ): Promise => send(file, { description, msg, rid, tmid, t, e2e }, getContent), }); From 677f0ef34b4daf52ebeaf5da33aee9a0f2795ccd Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 11:03:02 -0300 Subject: [PATCH 007/215] Cleanup --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9f74b099fa95b..16d4f93c6b46b 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -33,7 +33,6 @@ export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, user: IUser, - msgData?: Record, ): Promise => { validateFileRequiredFields(file); @@ -193,7 +192,7 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user, msgData); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); const msg = await executeSendMessage(userId, { rid: roomId, From 7c7eb29ae28ddd974bf965b714179b6152e54bf7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 12:44:45 -0300 Subject: [PATCH 008/215] Encrypt content --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 7 ++++++ .../variants/room/RoomMessageContent.tsx | 8 ------- .../client/lib/chats/flows/uploadFiles.ts | 24 +++++++++++-------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 472e719599337..53cdf16c61368 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -422,6 +422,13 @@ class E2E extends Emitter { const data = await e2eRoom.decrypt(message.msg); + if (message.content) { + const content = await e2eRoom.decrypt(message.content); + message.content = content; + message.attachments = content.attachments; + message.fileInfo = content.file; + } + const decryptedMessage: IE2EEMessage = { ...message, ...(data && { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4d932a2580a5f..417fbd303e1c2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -47,19 +47,11 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (normalizedMessage.content) { - // TODO: decrypt - const content = JSON.parse(normalizedMessage.content); - normalizedMessage.attachments = content.attachments; - normalizedMessage.fileInfo = content.file; - } - if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { if (!normalizedMessage.fileInfo) { return; } - console.log(attachment); const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 1b451e465bf7c..305a1c24fbe15 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,7 @@ import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; +import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; @@ -139,16 +140,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - // TODO: Encrypt content - return JSON.stringify({ - file: { - // url - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, - }, - attachments, - }); + const data = new TextEncoder('UTF-8').encode( + EJSON.stringify({ + file: { + // url + key: encryptedFile.key, + iv: encryptedFile.iv, + type: file.type, + }, + attachments, + }), + ); + + return e2eRoom.encryptText(data); }; uploadFile( From a9add79e4aa02315e7a2d589ba17e4d85c139dc2 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 12:52:50 -0300 Subject: [PATCH 009/215] Fix file description --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 45 +------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 53cdf16c61368..bee9549f210fe 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -439,50 +439,7 @@ class E2E extends Emitter { const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); - const decryptedMessageWithAttachments = await this.decryptMessageAttachments(decryptedMessageWithQuote); - - return decryptedMessageWithAttachments; - } - - async decryptMessageAttachments(message: IMessage): Promise { - const { attachments } = message; - - if (!attachments || !attachments.length) { - return message; - } - - const e2eRoom = await this.getInstanceByRoomId(message.rid); - - if (!e2eRoom) { - return message; - } - - const decryptedAttachments = await Promise.all( - attachments.map(async (attachment) => { - if (!isFileAttachment(attachment)) { - return attachment; - } - - if (!attachment.description) { - return attachment; - } - - const data = await e2eRoom.decrypt(attachment.description); - - if (!data) { - return attachment; - } - - attachment.description = data.text; - return attachment; - }), - ); - - return { - ...message, - attachments: decryptedAttachments, - e2e: 'done', - }; + return decryptedMessageWithQuote; } async decryptPendingMessages(): Promise { From 69f815e3b671a1af2f4e18d2913de2147a96f1e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:23:51 -0300 Subject: [PATCH 010/215] Small improvements --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 1 - .../message/variants/room/RoomMessageContent.tsx | 4 ++-- apps/meteor/client/lib/chats/ChatAPI.ts | 1 + apps/meteor/client/lib/chats/flows/uploadFiles.ts | 11 +++-------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index bee9549f210fe..dfe7e0821f11d 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -426,7 +426,6 @@ class E2E extends Emitter { const content = await e2eRoom.decrypt(message.content); message.content = content; message.attachments = content.attachments; - message.fileInfo = content.file; } const decryptedMessage: IE2EEMessage = { diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 417fbd303e1c2..b77d632a5383a 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -49,11 +49,11 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.fileInfo) { + if (!normalizedMessage.content) { return; } - const key = Base64.encode(JSON.stringify(normalizedMessage.fileInfo)); + const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 1b466e396c212..bcc59432f376c 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -103,6 +103,7 @@ export type UploadsAPI = { send( file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 305a1c24fbe15..a7ecc70579d72 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,14 +34,12 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - description?: string, - extraData?: Pick, + extraData?: Pick & { description?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, ) => { chat.uploads.send( file, { - description, msg, ...extraData, }, @@ -80,19 +78,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, description); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, description); + uploadFile(file, { description }); return; } - const encryptedDescription = await e2eRoom.encryptAttachmentDescription(description, Random.id()); - const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { @@ -157,7 +153,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi uploadFile( encryptedFile.file, - encryptedDescription, { t: 'e2e', }, From ebbe64f700e75c8492923f399f2d59a870a70348 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:29:17 -0300 Subject: [PATCH 011/215] Cleanup --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 14 -------------- .../file-upload/server/methods/sendFileMessage.ts | 1 - apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/client/lib/chats/uploads.ts | 7 ++----- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 9ba79ea674c19..37f4c3e675a63 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -410,20 +410,6 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - encryptAttachmentDescription(description, _id) { - const ts = new Date(); - - const data = new TextEncoder('UTF-8').encode( - EJSON.stringify({ - userId: this.userId, - text: description, - _id, - ts, - }), - ); - return this.encryptText(data); - } - // Decrypt messages async decryptMessage(message) { diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 16d4f93c6b46b..ac36953fccf72 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,7 +187,6 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - e2e: Match.Optional(String), content: Match.Optional(String), }), ); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index a7ecc70579d72..87d74a49e04b3 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,5 @@ import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 9e621d853aec3..b4f632a2ff40e 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -37,14 +37,12 @@ const send = async ( rid, tmid, t, - e2e, }: { description?: string; msg?: string; rid: string; tmid?: string; t?: IMessage['t']; - e2e?: IMessage['e2e']; }, getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { @@ -124,7 +122,6 @@ const send = async ( tmid, description, t, - e2e, content, }); } @@ -169,7 +166,7 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string, fileUrl: string) => Promise, - ): Promise => send(file, { description, msg, rid, tmid, t, e2e }, getContent), + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), }); From b43f57ba029b4b3d0365c16fae7756e552529231 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:31:30 -0300 Subject: [PATCH 012/215] Cleanup --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index dfe7e0821f11d..0e2c44b36ee54 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -2,7 +2,7 @@ import QueryString from 'querystring'; import URL from 'url'; import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isFileAttachment } from '@rocket.chat/core-typings'; +import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; From ec06eb3a422aef67ac0bfde8ad3cf0f1592d19a9 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sun, 28 Apr 2024 16:43:24 -0300 Subject: [PATCH 013/215] Improve TS --- .../variants/room/RoomMessageContent.tsx | 34 +++++++++++++------ .../core-typings/src/IMessage/IMessage.ts | 10 +++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index b77d632a5383a..d215b38dbb15e 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,6 +1,14 @@ import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; -import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; +import { + isDiscussionMessage, + isThreadMainMessage, + isE2EEMessage, + isFileImageAttachment, + isFileAttachment, + isFileAudioAttachment, + isFileVideoAttachment, +} from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; @@ -49,19 +57,25 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content) { + if (!normalizedMessage.content || typeof normalizedMessage.content === 'string') { return; } const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); - if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { - attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; - } - if (attachment.image_url && !attachment.image_url.startsWith('/file-decrypt/')) { - attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; - } - if (attachment.audio_url && !attachment.audio_url.startsWith('/file-decrypt/')) { - attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + + if (isFileAttachment(attachment)) { + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { + attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; + } } }); } diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index a4470a3c19241..ee66354901399 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -224,7 +224,15 @@ export interface IMessage extends IRocketChatRecord { customFields?: IMessageCustomFields; - content?: string; + content?: + | string + | { + attachments?: MessageAttachment[]; + file: { + iv: string; + key: JsonWebKey; + }; + }; } export type MessageSystem = { From f70d2e1fe006da9146746e1a2017df5215e7dc25 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 10:58:03 -0300 Subject: [PATCH 014/215] Change content data structure --- .vscode/settings.json | 1 + .../app/e2e/client/rocketchat.e2e.room.js | 22 ++++++++---------- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 9 ++++---- .../server/methods/sendFileMessage.ts | 5 +++- .../variants/room/RoomMessageContent.tsx | 9 ++++++-- apps/meteor/client/lib/chats/ChatAPI.ts | 4 ++-- .../client/lib/chats/flows/uploadFiles.ts | 23 +++++++++---------- apps/meteor/client/lib/chats/uploads.ts | 6 ++--- .../core-typings/src/IMessage/IMessage.ts | 14 ++++------- .../MessageAttachmentBase.ts | 2 ++ 10 files changed, 47 insertions(+), 48 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4eaf1836d1fde..2dcd055310d14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.words": [ "autotranslate", + "ciphertext", "Contextualbar", "fname", "Gazzodown", diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 37f4c3e675a63..922af9b065212 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -375,27 +375,23 @@ export class E2ERoom extends Emitter { // Encrypts messages async encryptText(data) { - if (!(typeof data === 'function' || (typeof data === 'object' && !!data))) { - data = new TextEncoder('UTF-8').encode(EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) })); - } - - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return data; - } - const vector = crypto.getRandomValues(new Uint8Array(16)); - let result; + try { - result = await encryptAES(vector, this.groupSessionKey, data); + const result = await encryptAES(vector, this.groupSessionKey, data); + return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } catch (error) { - return this.error('Error encrypting message: ', error); + this.error('Error encrypting message: ', error); + throw error; } - - return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } // Helper function for encryption of messages encrypt(message) { + if (!this.isSupportedRoomType(this.typeOfRoom)) { + return; + } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 0e2c44b36ee54..146dbf7176791 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -422,17 +422,16 @@ class E2E extends Emitter { const data = await e2eRoom.decrypt(message.msg); - if (message.content) { - const content = await e2eRoom.decrypt(message.content); - message.content = content; - message.attachments = content.attachments; + if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { + const content = await e2eRoom.decrypt(message.content.ciphertext); + Object.assign(message, content); } const decryptedMessage: IE2EEMessage = { ...message, + e2e: 'done', ...(data && { msg: data.text, - e2e: 'done', }), }; diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index ac36953fccf72..9f6aedbf7e3ae 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,7 +187,10 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - content: Match.Optional(String), + content: Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), }), ); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index d215b38dbb15e..a56105384e445 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -57,11 +57,16 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM if (normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!normalizedMessage.content || typeof normalizedMessage.content === 'string') { + if (!encrypted || message.e2e !== 'done') { return; } - const key = Base64.encode(JSON.stringify(normalizedMessage.content.file)); + const key = Base64.encode( + JSON.stringify({ + key: attachment.key, + iv: attachment.iv, + }), + ); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index bcc59432f376c..b903528f1d341 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IE2EEMessage } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; @@ -103,7 +103,7 @@ export type UploadsAPI = { send( file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 87d74a49e04b3..45503ba5d994c 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,4 +1,4 @@ -import type { IMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { EJSON } from 'meteor/ejson'; @@ -33,8 +33,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { description?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string, fileUrl: string) => Promise, ) => { chat.uploads.send( file, @@ -91,7 +91,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string) => { + const getContent = async (_id: string, fileUrl: string): Promise => { const attachments = []; const attachment: FileAttachmentProps = { @@ -100,6 +100,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi description, title_link: fileUrl, title_link_download: true, + key: encryptedFile.key, + iv: encryptedFile.iv, }; if (/^image\/.+/.test(file.type)) { @@ -135,19 +137,16 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - const data = new TextEncoder('UTF-8').encode( + const data = new TextEncoder().encode( EJSON.stringify({ - file: { - // url - key: encryptedFile.key, - iv: encryptedFile.iv, - type: file.type, - }, attachments, }), ); - return e2eRoom.encryptText(data); + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await e2eRoom.encryptText(data), + }; }; uploadFile( diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index b4f632a2ff40e..14c695efd3c09 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; @@ -44,7 +44,7 @@ const send = async ( tmid?: string; t?: IMessage['t']; }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => { const id = Random.id(); @@ -167,6 +167,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes send: ( file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string, fileUrl: string) => Promise, ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), }); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index ee66354901399..87293f830c42c 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,16 +223,6 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; - - content?: - | string - | { - attachments?: MessageAttachment[]; - file: { - iv: string; - key: JsonWebKey; - }; - }; } export type MessageSystem = { @@ -367,6 +357,10 @@ export const isVoipMessage = (message: IMessage): message is IVoipMessage => 'vo export type IE2EEMessage = IMessage & { t: 'e2e'; e2e: 'pending' | 'done'; + content?: { + algorithm: 'rc.v1.aes-sha2'; + ciphertext: string; // Encrypted subset JSON of IMessage + }; }; export interface IOTRMessage extends IMessage { diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 167975ae014f5..1f6fff1444c99 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -13,4 +13,6 @@ export type MessageAttachmentBase = { format?: string; title_link?: string; title_link_download?: boolean; + iv?: string; + key?: JsonWebKey; }; From 445c5283d5950e3d698eada3a4999e63802c6847 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 11:37:02 -0300 Subject: [PATCH 015/215] Move attachment key info to inside encryption property --- .../message/variants/room/RoomMessageContent.tsx | 11 +++-------- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 6 ++++-- .../MessageAttachment/MessageAttachmentBase.ts | 6 ++++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index a56105384e445..4f90b0b8f889e 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -55,18 +55,13 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (normalizedMessage?.attachments?.length) { + if (encrypted && normalizedMessage?.attachments?.length) { normalizedMessage.attachments.forEach((attachment) => { - if (!encrypted || message.e2e !== 'done') { + if (!attachment.encryption) { return; } - const key = Base64.encode( - JSON.stringify({ - key: attachment.key, - iv: attachment.iv, - }), - ); + const key = Base64.encode(JSON.stringify(attachment.encryption)); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 45503ba5d994c..bb29fa62d0408 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -100,8 +100,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi description, title_link: fileUrl, title_link_download: true, - key: encryptedFile.key, - iv: encryptedFile.iv, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, }; if (/^image\/.+/.test(file.type)) { diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 1f6fff1444c99..23a9998441450 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -13,6 +13,8 @@ export type MessageAttachmentBase = { format?: string; title_link?: string; title_link_download?: boolean; - iv?: string; - key?: JsonWebKey; + encryption?: { + iv: string; + key: JsonWebKey; + }; }; From 6daab1a96bfc05d0a5a812cd1a792ea76af3ebca Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 14:21:09 -0300 Subject: [PATCH 016/215] Set encrypted file name as a hash sha-256 of the name --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 922af9b065212..862e28cb3fe85 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -335,6 +335,13 @@ export class E2ERoom extends Emitter { } } + async sha256Hash(text) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data))); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } + // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { // if (!this.isSupportedRoomType(this.typeOfRoom)) { @@ -355,7 +362,9 @@ export class E2ERoom extends Emitter { const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const encryptedFile = new File([toArrayBuffer(result)], file.name); + const fileName = await this.sha256Hash(file.name); + + const encryptedFile = new File([toArrayBuffer(result)], fileName); return { file: encryptedFile, From fd5fcd29cee66d7a9994a07039bdeb3a4a5185fa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 14:31:16 -0300 Subject: [PATCH 017/215] Fix lint and TS --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 5 ----- apps/meteor/client/startup/e2e.ts | 4 +++- packages/rest-typings/src/v1/rooms.ts | 8 ++------ 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 862e28cb3fe85..215227781e377 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -1,6 +1,5 @@ import { Base64 } from '@rocket.chat/base64'; import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { RoomManager } from '../../../client/lib/RoomManager'; @@ -436,10 +435,6 @@ export class E2ERoom extends Emitter { } async decrypt(message) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return message; - } - const keyID = message.slice(0, 12); if (keyID !== this.keyID) { diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index f9cf156f8d8b3..bda1d5e9a0b67 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -142,7 +142,9 @@ Meteor.startup(() => { // Should encrypt this message. const msg = await e2eRoom.encrypt(message); - message.msg = msg; + if (msg) { + message.msg = msg; + } message.t = 'e2e'; message.e2e = 'pending'; return message; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 176ebe40217b2..36d8aa87a921a 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, IUser, RoomAdminFieldsType, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser, RoomAdminFieldsType, IUpload, IE2EEMessage } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; @@ -615,11 +615,7 @@ export type RoomsEndpoints = { tmid?: string; customFields?: string; t?: IMessage['t']; - e2e?: IMessage['e2e']; - content?: string; - encryption?: { - version: string; - }; + content?: IE2EEMessage['content']; }) => { message: IMessage | null }; }; From 2e1f78a7baa1778f0b89070fee5665f6422eac89 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Apr 2024 15:50:53 -0300 Subject: [PATCH 018/215] Fix regression --- .../app/file-upload/server/methods/sendFileMessage.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9f6aedbf7e3ae..19579d7db9c6e 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -187,10 +187,12 @@ export const sendFileMessage = async ( tmid: Match.Optional(String), customFields: Match.Optional(String), t: Match.Optional(String), - content: Match.ObjectIncluding({ - algorithm: String, - ciphertext: String, - }), + content: Match.Optional( + Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), + ), }), ); From 1e25638ef9cac74cb5e023e6b724b1497ba0ed9b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 9 May 2024 10:04:45 -0300 Subject: [PATCH 019/215] Mark uploads as temporary and confirm on send message --- apps/meteor/app/api/server/v1/rooms.ts | 6 +++++ .../server/models/raw/BaseUploadModel.ts | 22 +++++++++++++++++-- .../src/models/IBaseUploadsModel.ts | 2 ++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index c4150419f76a5..6f9eed8b02f9f 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -218,12 +218,16 @@ API.v1.addRoute( let { fileBuffer } = file; + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + const details = { name: file.filename, size: fileBuffer.length, type: file.mimetype, rid: this.urlParams.rid, userId: this.userId, + expiresAt, }; const stripExif = settings.get('Message_Attachments_Strip_Exif'); @@ -267,6 +271,8 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index eac646e443882..91468e14d6e9c 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -11,6 +11,7 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return [ { key: { name: 1 }, sparse: true }, { key: { rid: 1 }, sparse: true }, + { key: { expiresAt: 1 }, sparse: true }, ]; } @@ -34,8 +35,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.insertOne(fileData); } - // TODO: upload as temporary, create a cron to delete non used ones and a way to mark as used - updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { if (!fileId) { return; @@ -63,6 +62,25 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } + confirmTemporaryFile(fileId: string, userId: string): Promise | undefined { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId, + }; + + const update: Filter = { + $unset: { + expiresAt: 1, + }, + }; + + return this.updateOne(filter, update); + } + async findOneByName(name: string): Promise { return this.findOne({ name }); } diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 8995b78eda08d..940a5cf657a43 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -8,6 +8,8 @@ export interface IBaseUploadsModel extends IBaseModel { updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined; + confirmTemporaryFile(fileId: string, userId: string): Promise | undefined; + findOneByName(name: string): Promise; findOneByRoomId(rid: string): Promise; From 574169f58ca8d0f7cc18634ac0830e66e75ea58e Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 9 May 2024 10:21:01 -0300 Subject: [PATCH 020/215] Fix API test --- apps/meteor/tests/end-to-end/api/09-rooms.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 4cd3806c9e86b..d5a24474db748 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -305,7 +305,6 @@ describe('[Rooms]', function () { .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .field('t', 'e2e') - .field('e2e', 'pending') .field('description', 'some_file_description') .attach('file', imgURL) .expect('Content-Type', 'application/json') @@ -323,7 +322,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); expect(res.body.message).to.have.property('t', 'e2e'); - expect(res.body.message).to.have.property('e2e', 'pending'); }); }); }); From 7154756f2d4eaee25f7aac819ac32312673c503a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 13:52:06 -0300 Subject: [PATCH 021/215] Implement cronjob --- apps/meteor/app/api/server/v1/rooms.ts | 4 ++-- .../server/cron/temporaryUploadsCleanup.ts | 16 +++++++++++++ .../server/models/raw/BaseUploadModel.ts | 24 ++++++++++++++++++- apps/meteor/server/startup/cron.ts | 2 ++ .../src/models/IBaseUploadsModel.ts | 4 +++- 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/server/cron/temporaryUploadsCleanup.ts diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 6f9eed8b02f9f..a7b07c7534705 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -271,10 +271,10 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); - await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); return API.v1.success({ diff --git a/apps/meteor/server/cron/temporaryUploadsCleanup.ts b/apps/meteor/server/cron/temporaryUploadsCleanup.ts new file mode 100644 index 0000000000000..b227dd31947fb --- /dev/null +++ b/apps/meteor/server/cron/temporaryUploadsCleanup.ts @@ -0,0 +1,16 @@ +import { cronJobs } from '@rocket.chat/cron'; +import { Uploads } from '@rocket.chat/models'; + +import { FileUpload } from '../../app/file-upload/server'; + +async function temporaryUploadCleanup(): Promise { + const files = await Uploads.findExpiredTemporaryFiles({ projection: { _id: 1 } }).toArray(); + + for await (const file of files) { + await FileUpload.getStore('Uploads').deleteById(file._id); + } +} + +export async function temporaryUploadCleanupCron(): Promise { + await cronJobs.add('temporaryUploadCleanup', '31 * * * *', async () => temporaryUploadCleanup()); +} diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index 91468e14d6e9c..d0e187a91b23c 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -1,6 +1,16 @@ import type { IUpload } from '@rocket.chat/core-typings'; import type { IBaseUploadsModel } from '@rocket.chat/model-typings'; -import type { DeleteResult, IndexDescription, UpdateResult, Document, InsertOneResult, WithId, Filter } from 'mongodb'; +import type { + DeleteResult, + IndexDescription, + UpdateResult, + Document, + InsertOneResult, + WithId, + Filter, + FindOptions, + FindCursor, +} from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -78,6 +88,7 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo }, }; + console.log('confirm', filter, update); return this.updateOne(filter, update); } @@ -89,6 +100,17 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.findOne({ rid }); } + findExpiredTemporaryFiles(options?: FindOptions): FindCursor { + return this.find( + { + expiresAt: { + $lte: new Date(), + }, + }, + options, + ); + } + async updateFileNameById(fileId: string, name: string): Promise { const filter = { _id: fileId }; const update = { diff --git a/apps/meteor/server/startup/cron.ts b/apps/meteor/server/startup/cron.ts index 3c0482069e79c..6057186a1e4e1 100644 --- a/apps/meteor/server/startup/cron.ts +++ b/apps/meteor/server/startup/cron.ts @@ -6,6 +6,7 @@ import { npsCron } from '../cron/nps'; import { oembedCron } from '../cron/oembed'; import { startCron } from '../cron/start'; import { statsCron } from '../cron/statistics'; +import { temporaryUploadCleanupCron } from '../cron/temporaryUploadsCleanup'; import { userDataDownloadsCron } from '../cron/userDataDownloads'; import { videoConferencesCron } from '../cron/videoConferences'; @@ -17,6 +18,7 @@ Meteor.defer(async () => { await oembedCron(); await statsCron(logger); await npsCron(); + await temporaryUploadCleanupCron(); await federationCron(); await videoConferencesCron(); await userDataDownloadsCron(); diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 940a5cf657a43..12db0ee25d6b6 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -1,5 +1,5 @@ import type { IUpload } from '@rocket.chat/core-typings'; -import type { DeleteResult, UpdateResult, Document, InsertOneResult, WithId } from 'mongodb'; +import type { DeleteResult, UpdateResult, Document, InsertOneResult, WithId, FindCursor, FindOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -14,6 +14,8 @@ export interface IBaseUploadsModel extends IBaseModel { findOneByRoomId(rid: string): Promise; + findExpiredTemporaryFiles(options?: FindOptions): FindCursor; + updateFileNameById(fileId: string, name: string): Promise; deleteFile(fileId: string): Promise; From aff80f33581e177bbf9fc21ae1596ce771d2c2ea Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 16:34:36 -0300 Subject: [PATCH 022/215] Skip UI attachment test for now --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index b63efa2d092ff..26ad3b9721e66 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -311,7 +311,8 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); - test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { + // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted + test.skip('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); From de5f0f23127f54230a610d798ecc1d4d90207e91 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 10 May 2024 17:14:10 -0300 Subject: [PATCH 023/215] Do not generate attachment on backend for e2ee messages --- .../server/methods/sendFileMessage.ts | 18 +++++++++++------- .../variants/room/RoomMessageContent.tsx | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 19579d7db9c6e..9393d14fc120e 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -196,19 +196,23 @@ export const sendFileMessage = async ( }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - - const msg = await executeSendMessage(userId, { + const data = { rid: roomId, ts: new Date(), - file: files[0], - files, - attachments, ...(msgData as Partial), ...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }), msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, - }); + }; + + if (msgData?.t !== 'e2e') { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + data.file = files[0]; + data.files = files; + data.attachments = attachments; + } + + const msg = await executeSendMessage(userId, data); callbacks.runAsync('afterFileUpload', { user, room, message: msg }); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 4f90b0b8f889e..5a78ce5a7cbc8 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -82,6 +82,8 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( @@ -93,7 +95,6 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM searchText={searchText} /> )} - {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} )} From 939940d90687274d3e32d7c6917c8a21df4ce2d2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 14 May 2024 11:08:41 -0300 Subject: [PATCH 024/215] Download through serviceworker --- .../structure/AttachmentDownload.tsx | 25 ++-- .../structure/AttachmentDownloadBase.tsx | 26 ++++ .../structure/AttachmentEncryptedDownload.tsx | 15 ++ .../attachments/structure/AttachmentImage.tsx | 1 + .../message/hooks/useNormalizedMessage.ts | 30 +++- .../variants/room/RoomMessageContent.tsx | 36 +---- .../hooks/useDownloadFromServiceWorker.ts | 49 +++++++ apps/meteor/public/enc.js | 131 ++++++++++++------ apps/meteor/server/routes/fileDecrypt.ts | 7 + apps/meteor/server/routes/index.ts | 1 + 10 files changed, 227 insertions(+), 94 deletions(-) create mode 100644 apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx create mode 100644 apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx create mode 100644 apps/meteor/client/hooks/useDownloadFromServiceWorker.ts create mode 100644 apps/meteor/server/routes/fileDecrypt.ts diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx index de790f9e39363..b76cb268bb2f8 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx @@ -1,25 +1,20 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, FC } from 'react'; import React from 'react'; -import Action from '../../Action'; +import type Action from '../../Action'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; +import AttachmentEncryptedDownload from './AttachmentEncryptedDownload'; type AttachmentDownloadProps = Omit, 'icon'> & { title?: string | undefined; href: string }; const AttachmentDownload: FC = ({ title, href, ...props }) => { - const t = useTranslation(); - return ( - - ); + const isEncrypted = href.includes('/file-decrypt/'); + + if (isEncrypted) { + return ; + } + + return ; }; export default AttachmentDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx new file mode 100644 index 0000000000000..48c078b9146c8 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import Action from '../../Action'; + +type AttachmentDownloadBaseProps = Omit, 'icon'> & { title?: string | undefined; href: string }; + +const AttachmentDownloadBase: FC = ({ title, href, ...props }) => { + const t = useTranslation(); + + return ( + + ); +}; + +export default AttachmentDownloadBase; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx new file mode 100644 index 0000000000000..1dc6752abdd0a --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx @@ -0,0 +1,15 @@ +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; + +type AttachmentDownloadProps = ComponentProps; + +const AttachmentEncryptedDownload: FC = ({ title, href, ...props }) => { + const encryptedAnchorProps = useDownloadFromServiceWorker(href, title); + + return ; +}; + +export default AttachmentEncryptedDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 5b251ad6143f1..8195fdee5973c 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -86,6 +86,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo alt='' width={dimensions.width} height={dimensions.height} + loading='lazy' /> diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 2ade3deb49267..6898c55ebad2e 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -1,3 +1,5 @@ +import { Base64 } from '@rocket.chat/base64'; +import { isFileImageAttachment, isFileAttachment, isFileAudioAttachment, isFileVideoAttachment } from '@rocket.chat/core-typings'; import type { IMessage } from '@rocket.chat/core-typings'; import type { Options } from '@rocket.chat/message-parser'; import { useSetting } from '@rocket.chat/ui-contexts'; @@ -30,6 +32,32 @@ export const useNormalizedMessage = (message: TMessag }), }; - return parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + + normalizedMessage.attachments = normalizedMessage.attachments?.map((attachment) => { + if (!attachment.encryption) { + return attachment; + } + + const key = Base64.encode(JSON.stringify(attachment.encryption)); + + if (isFileAttachment(attachment)) { + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { + attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; + } + } + return attachment; + }); + + return normalizedMessage; }, [showColors, customDomains, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, message, autoTranslateOptions]); }; diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 5a78ce5a7cbc8..804002e2ffaf2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,14 +1,5 @@ -import { Base64 } from '@rocket.chat/base64'; import type { IMessage } from '@rocket.chat/core-typings'; -import { - isDiscussionMessage, - isThreadMainMessage, - isE2EEMessage, - isFileImageAttachment, - isFileAttachment, - isFileAudioAttachment, - isFileVideoAttachment, -} from '@rocket.chat/core-typings'; +import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; @@ -55,31 +46,6 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; - if (encrypted && normalizedMessage?.attachments?.length) { - normalizedMessage.attachments.forEach((attachment) => { - if (!attachment.encryption) { - return; - } - - const key = Base64.encode(JSON.stringify(attachment.encryption)); - - if (isFileAttachment(attachment)) { - if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { - attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; - } - if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { - attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; - } - if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { - attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; - } - if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { - attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; - } - } - }); - } - return ( <> {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} diff --git a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts new file mode 100644 index 0000000000000..9a44d8d5fd23b --- /dev/null +++ b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts @@ -0,0 +1,49 @@ +import { Emitter } from '@rocket.chat/emitter'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { downloadAs } from '../lib/download'; + +const ee = new Emitter>(); + +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; + + ee.emit(event.data.id, { result, id: event.data.id }); + } +}); + +export const useDownloadFromServiceWorker = (href: string, title?: string) => { + const { controller } = navigator.serviceWorker; + + const uid = useUniqueId(); + + const { t } = useTranslation(); + + useEffect( + () => + ee.once(uid, ({ result }) => { + downloadAs({ data: [new Blob([result])] }, title ?? t('Download')); + }), + [title, t, uid], + ); + + return { + disabled: !controller, + onContextMenu: useCallback((e) => e.preventDefault(), []), + onClick: useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + + controller?.postMessage({ + type: 'attachment-download', + url: href, + id: uid, + }); + }, + [href, uid, controller], + ), + }; +}; diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 25b74110b11ae..ea296f031e8ac 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -1,55 +1,100 @@ -function base64Decode (string) { +function base64Decode(string) { string = atob(string); - const - length = string.length, - buf = new ArrayBuffer(length), - bufView = new Uint8Array(buf); - for (var i = 0; i < string.length; i++) { bufView[i] = string.charCodeAt(i) } - return buf + const length = string.length, + buf = new ArrayBuffer(length), + bufView = new Uint8Array(buf); + for (var i = 0; i < string.length; i++) { + bufView[i] = string.charCodeAt(i); + } + return buf; } -function base64DecodeString (string) { +function base64DecodeString(string) { return atob(string); } +const decrypt = async (key, iv, file) => { + const ivArray = base64Decode(iv); + const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); + const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); + + return result; +}; +const getUrlParams = (url) => { + const urlObj = new URL(url); + + const k = base64DecodeString(urlObj.searchParams.get('key')); + + const { key, iv } = JSON.parse(k); + + const newUrl = urlObj.href.replace('/file-decrypt/', '/'); + + return { key, iv, url: newUrl }; +}; + self.addEventListener('fetch', (event) => { if (!event.request.url.includes('/file-decrypt/')) { return; } - const url = new URL(event.request.url); - const k = base64DecodeString(url.searchParams.get('key')); - - console.log(url); - const { - key, - iv - } = JSON.parse(k); - - const newUrl = url.href.replace('/file-decrypt/', '/'); - - const requestToFetch = new Request(newUrl, event.request); - - event.respondWith( - caches.match(requestToFetch).then((response) => { - if (response) { - console.log('cached'); - return response; - } - - return fetch(requestToFetch) - .then(async (response) => { - const file = await response.arrayBuffer(); - const ivArray = base64Decode(iv); - const cryptoKey = await crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - const result = await crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); - return new Response(result); - }) - .catch((error) => { - console.error("Fetching failed:", error); - - throw error; - }); - }), - ); + try { + const { url, key, iv } = getUrlParams(event.request.url); + + const requestToFetch = new Request(url, event.request); + + event.respondWith( + caches.match(requestToFetch).then((response) => { + if (response) { + return response; + } + + return fetch(requestToFetch) + .then(async (res) => { + const file = await res.arrayBuffer(); + const result = await decrypt(key, iv, file); + const response = new Response(result); + await caches.open('v1').then((cache) => { + cache.put(requestToFetch, response.clone()); + }); + + return response; + }) + .catch((error) => { + console.error('Fetching failed:', error); + + throw error; + }); + }), + ); + } catch (error) { + console.error(error); + throw error; + } +}); + +self.addEventListener('message', async (event) => { + if (event.data.type !== 'attachment-download') { + return; + } + const requestToFetch = new Request(url); + + const { url, key, iv } = getUrlParams(event.data.url); + const res = (await caches.match(requestToFetch)) ?? (await fetch(url)); + + const file = await res.arrayBuffer(); + const result = await decrypt(key, iv, file); + event.source + .postMessage({ + id: event.data.id, + type: 'attachment-download-result', + result, + }) + .catch((error) => { + console.error('Posting message failed:', error); + event.source.postMessage({ + id: event.data.id, + type: 'attachment-download-result', + error, + }); + }); }); diff --git a/apps/meteor/server/routes/fileDecrypt.ts b/apps/meteor/server/routes/fileDecrypt.ts new file mode 100644 index 0000000000000..005c0202658f4 --- /dev/null +++ b/apps/meteor/server/routes/fileDecrypt.ts @@ -0,0 +1,7 @@ +import { WebApp } from 'meteor/webapp'; + +WebApp.connectHandlers.use('/file-decrypt/', (_, res) => { + // returns 404 + res.writeHead(404); + res.end('Not found'); +}); diff --git a/apps/meteor/server/routes/index.ts b/apps/meteor/server/routes/index.ts index e60f0ceb3f243..4abc6c6f044a8 100644 --- a/apps/meteor/server/routes/index.ts +++ b/apps/meteor/server/routes/index.ts @@ -2,4 +2,5 @@ import './avatar'; import './health'; import './i18n'; import './timesync'; +import './fileDecrypt'; import './userDataDownload'; From 26e2eb84df605c3b9077dcc9284cc522203d78b7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 15 May 2024 10:27:51 -0300 Subject: [PATCH 025/215] =?UTF-8?q?=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index d393f7182036e..bb29fa62d0408 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -2,6 +2,7 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/c import { isRoomFederated } from '@rocket.chat/core-typings'; import { EJSON } from 'meteor/ejson'; +import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; import { imperativeModal } from '../../imperativeModal'; @@ -66,7 +67,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi imperativeModal.close(); uploadNextFile(); }, - onSubmit: (fileName: string, description?: string): void => { + onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, From aacf63dbea54d6582636e928f14911d755e99d8b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 12 Jun 2024 11:00:06 +0530 Subject: [PATCH 026/215] testing with e2e --- ee/packages/api-client/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index 7a664b9fcc8a8..c1648e87c6152 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -221,6 +221,7 @@ export class RestClient implements RestClientInterface { } send(endpoint: string, method: string, { headers, ...options }: Omit = {}): Promise { + console.log(`hello ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); return fetch(`${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, { ...options, headers: { ...this.getCredentialsAsHeaders(), ...this.headers, ...headers }, @@ -281,7 +282,7 @@ export class RestClient implements RestClientInterface { } value && data.append(key, value as any); }); - + console.log(`hello from file ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => { xhr.setRequestHeader(key, value); From ee3fe8afd6dd1720bef976fc85924deedd59867d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 12 Jun 2024 17:11:40 -0300 Subject: [PATCH 027/215] Move encryption of msg to inside content --- .../app/e2e/client/rocketchat.e2e.room.js | 39 ++++++++++++++++--- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 15 +------ apps/meteor/client/lib/chats/data.ts | 5 +-- .../client/lib/chats/flows/sendMessage.ts | 3 ++ .../client/lib/chats/flows/uploadFiles.ts | 13 ++----- apps/meteor/client/startup/e2e.ts | 13 ++----- 6 files changed, 46 insertions(+), 42 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index e1d18ddadc355..16e431ed7a977 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -403,6 +403,30 @@ export class E2ERoom extends Emitter { } } + // Helper function for encryption of content + async encryptMessageContent(contentToBeEncrypted) { + const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); + + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await this.encryptText(data), + }; + } + + // Helper function for encryption of content + async encryptMessage(message) { + const { msg, attachments, ...rest } = message; + + const content = await this.encryptMessageContent({ msg, attachments }); + + return { + ...rest, + content, + t: 'e2e', + e2e: 'pending', + }; + } + // Helper function for encryption of messages encrypt(message) { if (!this.isSupportedRoomType(this.typeOfRoom)) { @@ -424,21 +448,26 @@ export class E2ERoom extends Emitter { } // Decrypt messages - async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } - const data = await this.decrypt(message.msg); + if (message.msg) { + const data = await this.decrypt(message.msg); - if (!data?.text) { - return message; + if (data?.text) { + message.msg = data.text; + } + } + + if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(message.content.ciphertext); + Object.assign(message, content); } return { ...message, - msg: data.text, e2e: 'done', }; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 89331b762b559..ec21b47d1c5b1 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -521,20 +521,7 @@ class E2E extends Emitter { return message; } - const data = await e2eRoom.decrypt(message.msg); - - if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { - const content = await e2eRoom.decrypt(message.content.ciphertext); - Object.assign(message, content); - } - - const decryptedMessage: IE2EEMessage = { - ...message, - e2e: 'done', - ...(data && { - msg: data.text, - }), - }; + const decryptedMessage: IE2EEMessage = await e2eRoom.decryptMessage(message); const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index 12fb4097dce52..830ae940f91aa 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -7,7 +7,6 @@ import { Messages, ChatRoom, ChatSubscription } from '../../../app/models/client import { settings } from '../../../app/settings/client'; import { MessageTypes } from '../../../app/ui-utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { onClientBeforeSendMessage } from '../onClientBeforeSendMessage'; import { prependReplies } from '../utils/prependReplies'; import type { DataAPI } from './ChatAPI'; @@ -21,7 +20,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const effectiveRID = originalMessage?.rid ?? rid; const effectiveTMID = originalMessage ? originalMessage.tmid : tmid; - return (await onClientBeforeSendMessage({ + return { _id: originalMessage?._id ?? Random.id(), rid: effectiveRID, ...(effectiveTMID && { @@ -29,7 +28,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage ...(sendToChannel && { tshow: sendToChannel }), }), msg, - })) as IMessage; + }; }; const findMessageByID = async (mid: IMessage['_id']): Promise => diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 62adbd80eb3e2..0d05bce507de3 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -3,6 +3,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; +import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { processMessageEditing } from './processMessageEditing'; @@ -29,6 +30,8 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]) return; } + message = (await onClientBeforeSendMessage(message)) as IMessage; + await sdk.call('sendMessage', message, previewUrls); }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index bb29fa62d0408..45dc8bbdfd50b 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -139,16 +139,9 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } - const data = new TextEncoder().encode( - EJSON.stringify({ - attachments, - }), - ); - - return { - algorithm: 'rc.v1.aes-sha2', - ciphertext: await e2eRoom.encryptText(data), - }; + return e2eRoom.encryptMessageContent({ + attachments, + }); }; uploadFile( diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index 255fb7c3387c8..21473e1585b0d 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -1,4 +1,4 @@ -import type { AtLeast, IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -129,7 +129,7 @@ Meteor.startup(() => { }); // Encrypt messages before sending - offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message: AtLeast) => { + offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message) => { const e2eRoom = await e2e.getInstanceByRoomId(message.rid); if (!e2eRoom) { @@ -147,14 +147,7 @@ Meteor.startup(() => { } // Should encrypt this message. - const msg = await e2eRoom.encrypt(message); - - if (msg) { - message.msg = msg; - } - message.t = 'e2e'; - message.e2e = 'pending'; - return message; + return e2eRoom.encryptMessage(message); }); listenersAttached = true; From 48a97f741901fe0a764586ec125c12cbaa8ed5fa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:04:50 -0300 Subject: [PATCH 028/215] Fix placeholder message for threads --- .../message/variants/thread/ThreadMessageContent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 7098a2709d5b2..2f341dc99a23e 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -1,5 +1,6 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; +import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -37,14 +38,17 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const normalizedMessage = useNormalizedMessage(message); + const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( )} - {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} From 446b1a71a6e31b705927d57b2db6053123184737 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:06:45 -0300 Subject: [PATCH 029/215] Unskip test --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index be14273cf9b4b..9ead233affa60 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -240,7 +240,7 @@ test.describe.serial('e2e-encryption', () => { }); // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted - test.skip('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { + test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); From c258b218a3bb600d6b7da6c40e202c5176be5af8 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 12:12:49 -0300 Subject: [PATCH 030/215] Fix ts --- apps/meteor/client/lib/chats/data.ts | 2 +- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/package.json | 2 +- packages/core-typings/src/IMessage/IMessage.ts | 9 +++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index 830ae940f91aa..445f61f27226e 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -28,7 +28,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage ...(sendToChannel && { tshow: sendToChannel }), }), msg, - }; + } as IMessage; }; const findMessageByID = async (mid: IMessage['_id']): Promise => diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 45dc8bbdfd50b..47941e8f5f6e2 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,5 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { EJSON } from 'meteor/ejson'; import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index d791551678452..eae52435a2dd7 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -25,7 +25,7 @@ "debug": "meteor run --inspect", "debug-brk": "meteor run --inspect-brk", "lint": "yarn stylelint && yarn eslint", - "eslint": "NODE_OPTIONS=\"--max-old-space-size=4098\" eslint --ext .js,.jsx,.ts,.tsx . --cache", + "eslint": "NODE_OPTIONS=\"--max-old-space-size=6144\" eslint --ext .js,.jsx,.ts,.tsx . --cache", "eslint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix --cache", "obj:dev": "TEST_MODE=true yarn dev", "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 87293f830c42c..8730a6a0cea75 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -223,6 +223,11 @@ export interface IMessage extends IRocketChatRecord { }; customFields?: IMessageCustomFields; + + content?: { + algorithm: string; // 'rc.v1.aes-sha2' + ciphertext: string; // Encrypted subset JSON of IMessage + }; } export type MessageSystem = { @@ -357,10 +362,6 @@ export const isVoipMessage = (message: IMessage): message is IVoipMessage => 'vo export type IE2EEMessage = IMessage & { t: 'e2e'; e2e: 'pending' | 'done'; - content?: { - algorithm: 'rc.v1.aes-sha2'; - ciphertext: string; // Encrypted subset JSON of IMessage - }; }; export interface IOTRMessage extends IMessage { From 989c61c616e9ad14a6917911c0385dbe94d7adf6 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 13 Jun 2024 16:46:08 -0300 Subject: [PATCH 031/215] Fix tests --- apps/meteor/app/api/server/v1/rooms.ts | 6 +++++- .../app/file-upload/server/methods/sendFileMessage.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index d5ca92377eb48..c05557873ba83 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -271,7 +271,11 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; - await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }); + await sendFileMessage( + this.userId, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { parseAttachmentsForE2EE: false }, + ); await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9393d14fc120e..485528a5e62fe 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -159,6 +159,13 @@ export const sendFileMessage = async ( file: Partial; msgData?: Record; }, + { + parseAttachmentsForE2EE, + }: { + parseAttachmentsForE2EE: boolean; + } = { + parseAttachmentsForE2EE: true, + }, ): Promise => { const user = await Users.findOneById(userId); if (!user) { @@ -205,7 +212,7 @@ export const sendFileMessage = async ( groupable: msgData?.groupable ?? false, }; - if (msgData?.t !== 'e2e') { + if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; data.files = files; From 9da964021641d63ed8e4dcb418db24926248f39d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Fri, 14 Jun 2024 16:08:36 -0300 Subject: [PATCH 032/215] Add test for old e2ee msg format --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 45 +++++++++++++++++++ .../tests/e2e/fixtures/collections/users.ts | 3 +- .../tests/e2e/fixtures/inject-initial-data.ts | 7 ++- apps/meteor/tests/e2e/fixtures/userStates.ts | 31 +++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 6b60bd4b637a1..5f42ba5a5a4e2 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -517,3 +517,48 @@ test.describe.serial('e2ee room setup', () => { await expect(page.locator('.rcx-states__title')).toContainText('Check back later'); }); }); + +test.describe.serial('e2ee support legacy formats', () => { + let poHomeChannel: HomeChannel; + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + }); + + test.beforeAll(async ({ api }) => { + test.use({ storageState: Users.userE2EE.state }); + + expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + const rid = await page.locator('[data-qa-rc-room]').getAttribute('data-qa-rc-room'); + console.log({ rid }); + + // send old format encrypted message via API + await api.post('/chat.sendMessage', { + rid, + msg: 'eyJhbGciOiJB8NgMxt0P2jW/aRt4y4++lb8LDSpxUExisX1SiXaKCO9FkfrS1HfO7gFS0nxzHu3CjfgRAK5o3A9kWc4PrHfsQnTTLSh5LW/BKpgnu8XLoEoCbP3FTfy14i5urg6QnZRpz9jUsESjXXIFSxLzEx/T2PzuAzwNROhS+omrNKFi3r9oqkfzUZgTAVzitRpkJ7VN', + e2e: true, + }); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('Old format message'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/fixtures/collections/users.ts b/apps/meteor/tests/e2e/fixtures/collections/users.ts index 5b85974fb57c2..5baaccc101345 100644 --- a/apps/meteor/tests/e2e/fixtures/collections/users.ts +++ b/apps/meteor/tests/e2e/fixtures/collections/users.ts @@ -10,7 +10,7 @@ type UserFixture = IUser & { }; export function createUserFixture(user: IUserState): UserFixture { - const { username, hashedToken, loginExpire } = user.data; + const { username, hashedToken, loginExpire, e2e } = user.data; return { _id: `${username}`, @@ -48,5 +48,6 @@ export function createUserFixture(user: IUserState): UserFixture { createdAt: new Date(), _updatedAt: new Date(), __rooms: ['GENERAL'], + e2e, }; } diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts index e7e68790cf3dd..5a28e534cc16b 100644 --- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts +++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts @@ -7,7 +7,12 @@ import { Users } from './userStates'; export default async function injectInitialData() { const connection = await MongoClient.connect(constants.URL_MONGODB); - const usersFixtures = [createUserFixture(Users.user1), createUserFixture(Users.user2), createUserFixture(Users.user3)]; + const usersFixtures = [ + createUserFixture(Users.user1), + createUserFixture(Users.user2), + createUserFixture(Users.user3), + createUserFixture(Users.userE2EE), + ]; await Promise.all( usersFixtures.map((user) => diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts index e948d6acf5a75..219e40f1ab22f 100644 --- a/apps/meteor/tests/e2e/fixtures/userStates.ts +++ b/apps/meteor/tests/e2e/fixtures/userStates.ts @@ -11,10 +11,30 @@ export type IUserState = { loginToken: string; loginExpire: Date; hashedToken: string; + e2e?: { + private_key: string; + public_key: string; + }; + e2ePassword?: string; }; state: Exclude; }; +const e2eeData: Record = { + userE2EE: { + server: { + private_key: + '{"$binary":"F4AdhH8Nq2MkGy0+IpfSH5OfN6FnTh1blyhxNucQLUxskZiccAKyXyszquv7h3Jz5nFFPj5LxH18ZUTmqcMVz7LHIlOtF74Pq+g79LqWylXO2ANfxBbNUg6xhD7FFWn9eHI9k0gZy7dPmtuSiwlJzYPYZK/ouKcnwgkhze2Nz455JAf7gREvMEUc5UUCMsaQ2ka26gMU0/zUnkRuIqL0evIU6V2Y09+GmsUo4BUQvnvA657re+mP7XosK0LU6R+nO9m/cGSezCMLrzJ8TXxpCWIwgfzedR6XxE6Xb7qDpf2dNoUXEQsgUmp0Ft0/3BiohKB0Blr9mfWt2zyfjjxOIsiTX6Du8TQqGZQ1Nr0+EhEZeXu86E3umlQkPnAmXOBQ1Bp/us2Bjd5kSRc53jqgaBHMto6HApfZvUNAcpQeZoYWe1e5et5zyZD0Uzbc+g+zLfxbhtH2RW2Q66Yq2szodx7mvcBom3X6P+iaheRRA7qgj3cyZoI1q9byyAZRUFsdKVW69+fZT07Qt3n4WRbrO4+JTHy62v2mtEZxMzHMSFXhspkzH3cd5jMARMRP8jw4q8zq9jmPxQlUrmow/PA9FnmIghsIH46wLbX35Kk+FsZx88gawuFCcixgMxHE7IFbulokl5fIZnoXxqeEPrNNfW9HWgBmY6M6kJLK5shgZeZgbBz5aeXuzYRIVnI9iRvLNKDZPz51yLEzMB85Lv0Rbys8eg+odn/82FnJjoXk2IQgwW0EQahSfD18pcniLdrT/8zPiKEnMpvvwxTXeGW0I+C0R2CPqAlECvpuQB3ApmnZobAJ5cPz8qDK+Kl+JDAeDRbnqmbVjB9gq2FbQVvm5AMosA5qOTZ53UTCr9jZYj8SzmyE4NYQAEhvsgS3btaHg/rb72Creoe4TXiaPnSC4IZB9VnYyR61Xer3OiJ4cdn9/GlVSXW0SjlEsJx470PCnMK48V/EWr+Hx1bF0l3WBY9++LR7KXMC5PWxF1prcUAOMEnOMrwYTDwwXceG9N9r8VVQHFkleF+9Durk7RpdiyoLqagGxKSjBmb80ulhBddB8tyR+LW/tJeSGSUgDYVZ3OB+YP1jYKjuTDiezmRvjiKekwa6TsMuWI1VwXl3Rqz3H1AAkXIOn+A+Kufamk4K24PbGcbsDGvzKV4FCUv2gIugMgvw4bfaSZ05fUSOxJGK8ebHaOfkG1eOMfHzoO0Rl/0ep6YwLzNNR2ULXNZ81X2a5fU6dJ5ahA0ko3BB10oeKVC7koGpNNZZgaGHBspaarvm6ex3G+7WeUPpNFcP8anyLsWneqArZl/y5axTHFY4IeXLwyyq9CBYdKcUamlXKZ3Hec8VS2+C9a33cMlTvk/6yLLhnmhXyYHN7lraOJRY8j93rOu8IHVN0QYZhOhL6gDpwioI1kIqkYOoEiczPO1IzxgISN+5RliiT5DqLBCHCqZ/PNfZ5Rd4hJdxzL1rTujG4nItQ+CInVcQ/QI5pJePknJAE8nWzTZboka55nNmH0qOgOEWPEAMbbdp/tKwutynM5v1Rdxqw1r+hHxbCl+QO9Um7XV+ArsD2LvcL83fgWTBZkbp7wt/gQW2bWLmG3TfyPxas58vkby+JFn93jWR2z1fMEgFEkcbXy2NoIcNd02PzzRwEhTJxKP695Ne7OINgShLmaCMyvADpBzf5I5O7OVwS1qCX1ypoDCwXqXxHwMGfIkpGz0Ta7N/GB6TLP0ZzExNv5FMznUo+t0ar2re95ZPqvfbdiO66CjbcS/SRaH//9AeyUnHZvQtQ1JGsJcyZs4YRiouWYkhCkVr5o3iVY1u3/beDScuSauuOxQ8VdS88HDPuYLwAIgjPkXLgmr5q3IcyJjZoZEMUi0zyFhPhfxJXFbkzggUSvXak4LNADOwj/D6YxlSbJ3m96EN/R8Yzq9qo1hAkf/FO/Two8DTk5QrzIoE1tVfZFCvGqd/LjwbLBy1k+84emDfI93LyUtEg35mParjNwmpltO3FLpWQ7Pn+KPhkyAX5xw0AaVpjQ0geE+K/ZHTWjTzUN0UrVQ7rfBg1zHUIpzLcki4lGYvOOQAP1ZUoNSR8Q6P3C2YjNLCwa+D39uA/djv2S6IfxONs9Fc4WnQ7JbOsZCTcAXkyfJi24kvXeX8KNwZFI9reGgYfF3qOpMXET9CgU/MUoJrAngjXPC9kHYdjLRCNDmi5r1pJhUJ+YtDIxS33vZ30CZAUCRy4yPo/kQb4nQZxUSQEybWEZfg96uPajPrTA=="}', + public_key: + '{"alg":"RSA-OAEP-256","e":"AQAB","ext":true,"key_ops":["encrypt"],"kty":"RSA","n":"qVLMv2Iwm_Hyhlnh4etNlHEiCXBzJWbqMwOZ6pz_JuZY2HiqbLmSfBtpvwBKZvcmP92BqLl-qZuLV_bJD_11UBS3gR6ykgU-RsTz1L-V8vA2QZEyULP4DqkMcRCV_7WE_sn_ScePgKszx1284gnngct_1Tv37zB6Ifz7gb1THRwAqOGcE2htea4yQEhyX8ZAl_-95DTWLbXqEAuofqDpXMcQo487VezBWIaDdfw2VX0qi6kM-pt03Gx8uMniyAjhK1G8Dro3wgAtz4PNIwOsdXEvWTSyoXLVMsIuZeO9OGdJKXnZFtVEMzXLyQTD1LjXlsM_TF09fbkN41Tz12ojmQ"}', + }, + client: { + private_key: + '{"alg":"RSA-OAEP-256","d":"IpABtkEzPenNwQng105CKD5NndKj1msi_CXMibzhQk37rbg3xXi9w3KPC8th5JGnb5rl6AxxI-rZrytzUD3C8AVCjes3tSG33BdA1FkFITFSSeD6_ck2pbtxDDVAARHK431VDHjdPHz11Ui3kQZHiNGCtwKGMf9Zts1eg1WjfQnQw2ta4-38mwHpq-4Cm_F1brNTTAu5XlHMws4-TDlYhY3nFU2XvoiR2RPDbMddtvXpDZIVo9s7h3jcS4JxHeJd7mWfwcR_Wf0ArRJIhckgPQtTAAjADNpw_HAdERfJyOAJUnxtHkv4uTu_k23qDpPGEi8euFpQ_1UD8B_Z1Rxylw","dp":"OS3zu_VYJZmOXl1dRXxYGP69MR4YQ3TFJ58HFIxvebD060byGHL-mwf0R6-a1hBkHfSeUI9iPipEcjQeevasPqm5CG8eYMvGU2vhsoq1gfY79rsoKjnThCO3XiUbNeM-G9MRKMRa3ooQ8fUVHyEWKFo1ajoFbVHxZuqTAOgrYT8","dq":"yXtWRU1vM5imQJhIZBt5BO1Rfn-koHTvTM3c5QDdPLyNoGTKTyeoT3P9clN6qevJKTyJJTWiwuz8ZECSksh_m9STCY1ry2HqlF2EKdCZnTQzhoJvb6d7547Witc9eh2rBjsILSxVBadLzOFe8opkkQkdkM_gN_Rr3TtXEAo1vn8","e":"AQAB","ext":true,"key_ops":["decrypt"],"kty":"RSA","n":"qVLMv2Iwm_Hyhlnh4etNlHEiCXBzJWbqMwOZ6pz_JuZY2HiqbLmSfBtpvwBKZvcmP92BqLl-qZuLV_bJD_11UBS3gR6ykgU-RsTz1L-V8vA2QZEyULP4DqkMcRCV_7WE_sn_ScePgKszx1284gnngct_1Tv37zB6Ifz7gb1THRwAqOGcE2htea4yQEhyX8ZAl_-95DTWLbXqEAuofqDpXMcQo487VezBWIaDdfw2VX0qi6kM-pt03Gx8uMniyAjhK1G8Dro3wgAtz4PNIwOsdXEvWTSyoXLVMsIuZeO9OGdJKXnZFtVEMzXLyQTD1LjXlsM_TF09fbkN41Tz12ojmQ","p":"0GJaXeKlxgcz6pX0DdwtWG38x9vN2wfLrN3F8N_0stzyPMjMpLGXOdGq1k1V6FROYvLHZsqdCpziwJ3a1PQaGUg2lO-KeBghlbDk4xfYbzSSPhVdwvUT27dysd3-_TsBvNpVCqCLb9Wgl8f0jrrRmRTSztYSLw3ckL939OJoe0M","q":"0AOMQqdGlz0Tm81uqpzCuQcQLMj-IhmPIMuuTnIU55KCmEwmlf0mkgesj-EEBsC1h6ScC5fvznGNvSGqVQAP5ANNZxGiB73q-2YgH3FpuEeHekufl260E_9tgIuqjtCv-eT_cLUhnRNyuP2ZiqRZsBWLuaQYkTubyGRi6izoofM","qi":"FXbIXivKdh0VBgMtLe5f1OjzyrSW_IfIvz8ZM66F4tUTxnNKk5vSb_q2NPyIOVYbdonuVguX-0VO54Ct16k8VdpQSMmUxGbyQAtIck2IzEzpfbRJgn06wiAI3j8q1nRFhrzhfrpJWVyuTiXBgaeOLWBz8fBpjDU7rptmcoU3tZ4"}', + }, + }, +}; + function generateContext(username: string): IUserState { const date = new Date(); date.setFullYear(date.getFullYear() + 1); @@ -33,6 +53,10 @@ function generateContext(username: string): IUserState { loginToken: token.token, loginExpire: token.when, hashedToken, + ...(e2eeData[username] && { + e2e: e2eeData[username]?.server, + e2ePassword: 'minus mobile dexter forest elvis', + }), }, state: { cookies: [ @@ -77,6 +101,12 @@ function generateContext(username: string): IUserState { name: 'Meteor.userId', value: username, }, + ...(e2eeData[username] && [ + { + name: 'private_key', + value: e2eeData[username]?.client.private_key, + }, + ]), ], }, ], @@ -88,6 +118,7 @@ export const Users = { user1: generateContext('user1'), user2: generateContext('user2'), user3: generateContext('user3'), + userE2EE: generateContext('userE2EE'), samluser1: generateContext('samluser1'), samluser2: generateContext('samluser2'), samluser4: generateContext('samluser4'), From 5228bdb53f4ded413524cc6d77b77d8ba5aad247 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 18 Jun 2024 16:39:06 -0300 Subject: [PATCH 033/215] Add tests for new upload API --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 +- apps/meteor/tests/end-to-end/api/09-rooms.js | 324 ++++++++++++++++++- 2 files changed, 324 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 3addbe8d7f4cd..4317ad0dc8ca3 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -299,7 +299,6 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); - // Not sure what to do on this case now. Maybe we should not have the backend generated attachment so message would be empty before decrypted test('expect placeholder text in place of encrypted file description, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); @@ -633,6 +632,7 @@ test.describe.serial('e2ee support legacy formats', () => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); + // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 5d394b79b120e..212ee4cce7193 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -300,7 +300,8 @@ describe('[Rooms]', function () { await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); }); - it('should correctly save e2ee file description and properties', async () => { + // Support legacy behavior (not encrypting file) + it('should correctly save file description and properties with type e2e', async () => { await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) @@ -326,6 +327,327 @@ describe('[Rooms]', function () { }); }); + describe('/rooms.media', () => { + let testChannel; + let user; + let userCredentials; + const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; + let blockedMediaTypes; + + before(async () => { + user = await createUser({ joinDefaultChannels: false }); + userCredentials = await login(user.username, password); + testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; + blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList'); + const newBlockedMediaTypes = blockedMediaTypes + .split(',') + .filter((type) => type !== 'image/svg+xml') + .join(','); + await updateSetting('FileUpload_MediaTypeBlackList', newBlockedMediaTypes); + }); + + after(() => + Promise.all([ + deleteRoom({ type: 'c', roomId: testChannel._id }), + deleteUser(user), + updateSetting('FileUpload_Restrict_to_room_members', true), + updateSetting('FileUpload_ProtectFiles', true), + updateSetting('FileUpload_MediaTypeBlackList', blockedMediaTypes), + ]), + ); + + it("don't upload a file to room with file field other than file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('test', imgURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', '[invalid-field]'); + expect(res.body).to.have.property('errorType', 'invalid-field'); + }) + .end(done); + }); + it("don't upload a file to room with empty file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', '') + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', res.body.error); + }) + .end(done); + }); + it("don't upload a file to room with more than 1 file", (done) => { + request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Just 1 file is allowed'); + }) + .end(done); + }); + + let fileNewUrl; + let fileOldUrl; + let fileId; + it('should upload a PNG file to room', async () => { + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + // expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + // expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + + fileNewUrl = message.file.url; + fileOldUrl = message.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); + fileId = message.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${fileId}/`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('image_type', 'image/png'); + expect(res.body.message.attachments[0]).to.have.property('title', '1024x1024.png'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(2); + expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + }); + }); + + it('should upload a LST file to room', async () => { + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', lstURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('format', 'LST'); + expect(res.body.message.attachments[0]).to.have.property('title', 'lst-test.lst'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(1); + expect(res.body.message.files[0]).to.have.property('name', 'lst-test.lst'); + }); + }); + + it('should not allow uploading a blocked media type to a room', async () => { + await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', lstURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-invalid-file-type'); + }); + }); + + it('should be able to get the file', async () => { + await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should be able to get the file when no access to the room if setting allows it', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', false); + await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should not be able to get the file when no access to the room if setting blocks', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', true); + await request.get(fileNewUrl).set(userCredentials).expect(403); + await request.get(fileOldUrl).set(userCredentials).expect(403); + }); + + it('should be able to get the file if member and setting blocks outside access', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', true); + await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should not be able to get the file without credentials', async () => { + await request.get(fileNewUrl).attach('file', imgURL).expect(403); + await request.get(fileOldUrl).attach('file', imgURL).expect(403); + }); + + it('should be able to get the file without credentials if setting allows', async () => { + await updateSetting('FileUpload_ProtectFiles', false); + await request.get(fileNewUrl).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).expect('Content-Type', 'image/png').expect(200); + }); + + it('should generate thumbnail for SVG files correctly', async () => { + const expectedFileName = `thumb-${svgLogoFileName}`; + let thumbUrl; + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', svgLogoURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + const { files, attachments } = message; + + expect(files).to.be.an('array'); + const hasThumbFile = files.some((file) => file.type === 'image/png' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments.find((attachment) => attachment.title === svgLogoFileName); + expect(thumbAttachment).to.be.an('object'); + thumbUrl = thumbAttachment.image_url; + }); + + await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/png'); + }); + + it('should generate thumbnail for JPEG files correctly', async () => { + const expectedFileName = `thumb-sample-jpeg.jpg`; + let thumbUrl; + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/sample-jpeg.jpg'))) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + const { message } = res.body; + const { files, attachments } = message; + + expect(files).to.be.an('array'); + const hasThumbFile = files.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments.find((attachment) => attachment.title === `sample-jpeg.jpg`); + expect(thumbAttachment).to.be.an('object'); + thumbUrl = thumbAttachment.image_url; + }); + + await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); + }); + + // Support legacy behavior (not encrypting file) + it('should correctly save file description and properties with type e2e', async () => { + let fileId; + await request + .post(api(`rooms.media/${testChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); + + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) + .set(credentials) + .field('t', 'e2e') + .field('description', 'some_file_description') + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('image_type', 'image/png'); + expect(res.body.message.attachments[0]).to.have.property('title', '1024x1024.png'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(2); + expect(res.body.message.files[0]).to.have.property('type', 'image/png'); + expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); + expect(res.body.message).to.have.property('t', 'e2e'); + }); + }); + }); + describe('/rooms.favorite', () => { let testChannel; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; From 9e8363460290e417bb920789634dbcb4e1e5c60a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 09:17:00 -0300 Subject: [PATCH 034/215] Save encrypted content info to the file upload --- apps/meteor/app/api/server/v1/rooms.ts | 20 +++++++++++--- .../app/e2e/client/rocketchat.e2e.room.js | 14 +++++++--- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 10 +++++++ .../client/lib/fileUploadHandler.ts | 4 ++- apps/meteor/client/lib/chats/ChatAPI.ts | 1 + .../client/lib/chats/flows/uploadFiles.ts | 15 +++++++++++ apps/meteor/client/lib/chats/uploads.ts | 7 ++++- .../RoomFiles/components/FileItem.tsx | 26 ++++++++++++++++--- .../server/models/raw/BaseUploadModel.ts | 1 - packages/core-typings/src/IUpload.ts | 4 +++ 10 files changed, 88 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index c05557873ba83..11cc49e853fee 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -221,12 +221,26 @@ API.v1.addRoute( const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 24); + const { fields } = file; + + let content; + + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); + } + } + const details = { name: file.filename, size: fileBuffer.length, type: file.mimetype, rid: this.urlParams.rid, userId: this.userId, + content, expiresAt, }; @@ -239,14 +253,14 @@ API.v1.addRoute( const fileStore = FileUpload.getStore('Uploads'); const uploadedFile = await fileStore.insert(details, fileBuffer); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); - const fileUrl = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); return API.v1.success({ file: { _id: uploadedFile._id, - url: fileUrl, + url: uploadedFile.path, }, }); }, diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 16e431ed7a977..db40280fd645a 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -447,6 +447,15 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } + async decryptContent(data) { + if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(data.content.ciphertext); + Object.assign(data, content); + } + + return data; + } + // Decrypt messages async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { @@ -461,10 +470,7 @@ export class E2ERoom extends Emitter { } } - if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') { - const content = await this.decrypt(message.content.ciphertext); - Object.assign(message, content); - } + message = await this.decryptContent(message); return { ...message, diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index ec21b47d1c5b1..e868db4668715 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -510,6 +510,16 @@ class E2E extends Emitter { } } + async decryptFileContent(file: IUploadWithUser): Promise { + const e2eRoom = await this.getInstanceByRoomId(file.rid); + + if (!e2eRoom) { + return file; + } + + return e2eRoom.decryptContent(file); + } + async decryptMessage(message: IMessage | IE2EEMessage): Promise { if (!isE2EEMessage(message) || message.e2e === 'done') { return message; diff --git a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts index e0887888c478d..c8e6f90966fd4 100644 --- a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts +++ b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts @@ -5,7 +5,9 @@ import { Tracker } from 'meteor/tracker'; Tracker.autorun(() => { const userId = Meteor.userId(); - if (userId) { + // Check for Meteor.loggingIn to be reactive and ensure it will process only after login finishes + // preventing race condition setting the rc_token as null forever + if (userId && Meteor.loggingIn() === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${escape(userId)}; path=/${secure}`; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index b903528f1d341..17650bfdbb8fb 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -104,6 +104,7 @@ export type UploadsAPI = { file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fb53cb253ebae..bd5a088a41dc0 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,6 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi file: File, extraData?: Pick & { description?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ) => { chat.uploads.send( file, @@ -42,6 +43,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ...extraData, }, getContent, + fileContent, ); chat.composer?.clear(); imperativeModal.close(); @@ -143,12 +145,25 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); }; + const fileContentData = { + type: file.type, + typeGroup: file.type.split('/')[0], + name: fileName, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + uploadFile( encryptedFile.file, { t: 'e2e', }, getContent, + fileContent, ); } }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 14c695efd3c09..bcce45231fd93 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -45,6 +45,7 @@ const send = async ( t?: IMessage['t']; }, getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: IE2EEMessage['content'], ): Promise => { const id = Random.id(); @@ -63,6 +64,9 @@ const send = async ( `/v1/rooms.media/${rid}`, { file, + ...(fileContent && { + content: JSON.stringify(fileContent), + }), }, { load: (event) => { @@ -168,5 +172,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string, fileUrl: string) => Promise, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent), + fileContent?: IE2EEMessage['content'], + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 3170fe4b24882..28e3a47cce6c2 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -1,8 +1,10 @@ +import { Base64 } from '@rocket.chat/base64'; import type { IUpload, IUploadWithUser } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -22,12 +24,28 @@ type FileItemProps = { const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); - const { _id, name, url, uploadedAt, type, typeGroup, user } = fileData; + const [file, setFile] = useState(fileData); + const { _id, path, name, uploadedAt, type, typeGroup, user } = file; + console.log(123, file); + + useEffect(() => { + (async () => { + if (fileData.rid && fileData.content) { + const e2eRoom = await e2e.getInstanceByRoomId(fileData.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(fileData); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + setFile({ ...decrypted }); + } + } + })(); + }, [fileData]); return ( {typeGroup === 'image' ? ( - + ) : ( { display='flex' flexGrow={1} flexShrink={1} - href={url} + href={path} textDecorationLine='none' > diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index d0e187a91b23c..4037566272b9f 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -88,7 +88,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo }, }; - console.log('confirm', filter, update); return this.updateOne(filter, update); } diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index c40e9d949407b..92710b826e126 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -48,6 +48,10 @@ export interface IUpload { Webdav?: { path: string; }; + content?: { + algorithm: string; // 'rc.v1.aes-sha2' + ciphertext: string; // Encrypted subset JSON of IUpload + }; } export type IUploadWithUser = IUpload & { user?: Pick }; From 8f81eddcc84c019b3724e7bf93a1acc0e8dc0b84 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:12:11 -0300 Subject: [PATCH 035/215] Add dimensions to image attachments --- .../client/lib/chats/flows/uploadFiles.ts | 22 +++++++++++++++---- .../RoomFiles/components/FileItem.tsx | 1 - 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index bd5a088a41dc0..a3c66bb00dddd 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -21,6 +21,19 @@ if ('serviceWorker' in navigator) { }); } +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; + export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; @@ -108,16 +121,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }; if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + attachments.push({ ...attachment, image_url: fileUrl, image_type: file.type, image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), }); - - // if (file.identify?.size) { - // attachment.image_dimensions = file.identify.size; - // } } else if (/^audio\/.+/.test(file.type)) { attachments.push({ ...attachment, diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 28e3a47cce6c2..151a5d1d7d880 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -26,7 +26,6 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); const [file, setFile] = useState(fileData); const { _id, path, name, uploadedAt, type, typeGroup, user } = file; - console.log(123, file); useEffect(() => { (async () => { From b9ff006988bd667f2ddaa64544d56e01a997a40a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:26:23 -0300 Subject: [PATCH 036/215] Fix TS --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index e868db4668715..58277fc41d1a2 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -1,7 +1,7 @@ import QueryString from 'querystring'; import URL from 'url'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUploadWithUser } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; From 20962c46bbe1399902b61df172f18eea2f140830 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 10:29:54 -0300 Subject: [PATCH 037/215] Fix tests --- apps/meteor/tests/e2e/fixtures/userStates.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts index 219e40f1ab22f..252023eae73ca 100644 --- a/apps/meteor/tests/e2e/fixtures/userStates.ts +++ b/apps/meteor/tests/e2e/fixtures/userStates.ts @@ -101,12 +101,14 @@ function generateContext(username: string): IUserState { name: 'Meteor.userId', value: username, }, - ...(e2eeData[username] && [ - { - name: 'private_key', - value: e2eeData[username]?.client.private_key, - }, - ]), + ...(e2eeData[username] + ? [ + { + name: 'private_key', + value: e2eeData[username]?.client.private_key, + }, + ] + : []), ], }, ], From 74cfee1dcde77b49aaf4e5e017cf66509744c27b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 11:18:34 -0300 Subject: [PATCH 038/215] Fix image preview --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 4 +++ .../components/ImageGallery/ImageGallery.tsx | 4 +-- .../client/lib/chats/flows/uploadFiles.ts | 12 +++++++++ .../room/ImageGallery/hooks/useImagesList.ts | 26 +++++++++++++++---- .../RoomFiles/components/FileItem.tsx | 21 ++------------- .../RoomFiles/hooks/useFilesList.ts | 26 +++++++++++++++---- packages/core-typings/src/IUpload.ts | 4 +++ 7 files changed, 66 insertions(+), 31 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 58277fc41d1a2..b170f3fe5e7a6 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -511,6 +511,10 @@ class E2E extends Emitter { } async decryptFileContent(file: IUploadWithUser): Promise { + if (!file.rid) { + return file; + } + const e2eRoom = await this.getInstanceByRoomId(file.rid); if (!e2eRoom) { diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 8dd69ab8fc25d..2cabfed460bd7 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -182,14 +182,14 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onReachBeginning={loadMore} initialSlide={images.length - 1} > - {[...images].reverse().map(({ _id, url }) => ( + {[...images].reverse().map(({ _id, path, url }) => (
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */} - +
diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index a3c66bb00dddd..b44ca3271809c 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -154,8 +154,20 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }); } + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ]; + return e2eRoom.encryptMessageContent({ attachments, + files, + file: files[0], }); }; diff --git a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts index c4f7d0770815f..d3db5fbed25bc 100644 --- a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts +++ b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts @@ -1,7 +1,9 @@ +import { Base64 } from '@rocket.chat/base64'; import type { RoomsImagesProps } from '@rocket.chat/rest-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useState } from 'react'; +import { e2e } from '../../../../../app/e2e/client/rocketchat.e2e'; import { useScrollableRecordList } from '../../../../hooks/lists/useScrollableRecordList'; import { useComponentDidUpdate } from '../../../../hooks/useComponentDidUpdate'; import { ImagesList } from '../../../../lib/lists/ImagesList'; @@ -40,12 +42,26 @@ export const useImagesList = ( count: end, }); + const items = files.map((file) => ({ + ...file, + uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, + modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, + })); + + for await (const file of items) { + if (file.rid && file.content) { + const e2eRoom = await e2e.getInstanceByRoomId(file.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(file); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + Object.assign(file, decrypted); + } + } + } + return { - items: files.map((file) => ({ - ...file, - uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, - modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, - })), + items, itemCount: total, }; }, diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 151a5d1d7d880..27db74bc2c68a 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -1,10 +1,8 @@ -import { Base64 } from '@rocket.chat/base64'; import type { IUpload, IUploadWithUser } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -24,22 +22,7 @@ type FileItemProps = { const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); - const [file, setFile] = useState(fileData); - const { _id, path, name, uploadedAt, type, typeGroup, user } = file; - - useEffect(() => { - (async () => { - if (fileData.rid && fileData.content) { - const e2eRoom = await e2e.getInstanceByRoomId(fileData.rid); - if (e2eRoom?.shouldConvertReceivedMessages()) { - const decrypted = await e2e.decryptFileContent(fileData); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); - decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; - setFile({ ...decrypted }); - } - } - })(); - }, [fileData]); + const { _id, path, name, uploadedAt, type, typeGroup, user } = fileData; return ( diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts index 7875b97576f51..66913204e7825 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -1,6 +1,8 @@ +import { Base64 } from '@rocket.chat/base64'; import { useUserRoom, useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { useScrollableRecordList } from '../../../../../hooks/lists/useScrollableRecordList'; import { useStreamUpdatesForMessageList } from '../../../../../hooks/lists/useStreamUpdatesForMessageList'; import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate'; @@ -59,12 +61,26 @@ export const useFilesList = ( }), }); + const items = files.map((file) => ({ + ...file, + uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, + modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, + })); + + for await (const file of items) { + if (file.rid && file.content) { + const e2eRoom = await e2e.getInstanceByRoomId(file.rid); + if (e2eRoom?.shouldConvertReceivedMessages()) { + const decrypted = await e2e.decryptFileContent(file); + const key = Base64.encode(JSON.stringify(decrypted.encryption)); + decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; + Object.assign(file, decrypted); + } + } + } + return { - items: files.map((file) => ({ - ...file, - uploadedAt: file.uploadedAt ? new Date(file.uploadedAt) : undefined, - modifiedAt: file.modifiedAt ? new Date(file.modifiedAt) : undefined, - })), + items, itemCount: total, }; }, diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 92710b826e126..b9b367a648f21 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -52,6 +52,10 @@ export interface IUpload { algorithm: string; // 'rc.v1.aes-sha2' ciphertext: string; // Encrypted subset JSON of IUpload }; + encryption?: { + iv: string; + key: JsonWebKey; + }; } export type IUploadWithUser = IUpload & { user?: Pick }; From 6508ebe69fbed7ceff0730dbc3c773ffea6c88f5 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 11:38:06 -0300 Subject: [PATCH 039/215] Prevent keys to be set on top of existent keys --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 5 ++++- .../methods/setUserPublicAndPrivateKeys.ts | 8 ++++++++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 4 ++-- apps/meteor/tests/end-to-end/api/01-users.js | 16 ++++++++++++++++ apps/meteor/tests/end-to-end/api/09-rooms.js | 7 +++---- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index b170f3fe5e7a6..346fa986c14eb 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -316,7 +316,10 @@ class E2E extends Emitter { this.db_private_key = private_key; } catch (error) { this.setState(E2EEState.ERROR); - return this.error('Error fetching RSA keys: ', error); + this.error('Error fetching RSA keys: ', error); + // Stop any process since we can't communicate with the server + // to get the keys. This prevents new key generation + throw error; } } diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index c00b9b872466a..92144d3405277 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -19,6 +19,14 @@ Meteor.methods({ }); } + const keys = await Users.fetchKeysByUserId(userId); + + if (keys.private_key && keys.public_key) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } + await Users.setE2EPublicAndPrivateKeysByUserId(userId, { private_key: keyPair.private_key, public_key: keyPair.public_key, diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 4317ad0dc8ca3..b64609258d8a7 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -616,13 +616,13 @@ test.describe.serial('e2ee room setup', () => { test.describe.serial('e2ee support legacy formats', () => { let poHomeChannel: HomeChannel; + test.use({ storageState: Users.userE2EE.state }); + test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); }); test.beforeAll(async ({ api }) => { - test.use({ storageState: Users.userE2EE.state }); - expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index b1d9a594996b5..0fe73ac1d56a0 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -73,6 +73,22 @@ describe('[Users]', function () { }); }); + it('should fail when trying to set keys for a user with keys already set', async () => { + await request + .post(api('e2e.setUserPublicAndPrivateKeys')) + .set(userCredentials) + .send({ + private_key: 'test', + public_key: 'test', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Keys already set [error-keys-already-set]'); + }); + }); + describe('[/users.create]', () => { before(async () => clearCustomFields()); after(async () => clearCustomFields()); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 511492810388e..d214f44fe8088 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -478,7 +478,6 @@ describe('[Rooms]', function () { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - const { message } = res.body; expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('file'); expect(res.body.file).to.have.property('_id'); @@ -486,9 +485,9 @@ describe('[Rooms]', function () { // expect(res.body.message.files[0]).to.have.property('type', 'image/png'); // expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); - fileNewUrl = message.file.url; - fileOldUrl = message.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); - fileId = message.file._id; + fileNewUrl = res.body.file.url; + fileOldUrl = res.body.file.url.replace('/file-upload/', '/ufs/GridFS:Uploads/'); + fileId = res.body.file._id; }); await request From 64a27cb58894c06bcef58f7c66bc37f1530fae4f Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 13:12:03 -0300 Subject: [PATCH 040/215] Fix API tests --- apps/meteor/tests/end-to-end/api/01-users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index 0fe73ac1d56a0..ee53820daa2a8 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -82,7 +82,7 @@ describe('[Users]', function () { public_key: 'test', }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Keys already set [error-keys-already-set]'); From b5e1ba5685833eb457bbfd76077b4e81f4e191f9 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 13:24:34 -0300 Subject: [PATCH 041/215] Fix e2ee change password --- apps/meteor/app/api/server/v1/e2e.ts | 5 ++++- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 5 +++-- .../server/methods/setUserPublicAndPrivateKeys.ts | 14 ++++++++------ packages/rest-typings/src/v1/e2e.ts | 1 + .../e2eSetUserPublicAndPrivateKeysParamsPOST.ts | 4 ++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 8cb3e8ab4236e..4c67e5f4f8b6f 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -113,6 +113,8 @@ API.v1.addRoute( * type: string * private_key: * type: string + * force: + * type: boolean * responses: * 200: * content: @@ -135,11 +137,12 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { public_key, private_key } = this.bodyParams; + const { public_key, private_key, force } = this.bodyParams; await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { public_key, private_key, + force, }); return API.v1.success(); diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 346fa986c14eb..d2d0dee2198e0 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -156,7 +156,7 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string): Promise { + async persistKeys({ public_key, private_key }: KeyPair, password: string, { force = false }): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } @@ -170,6 +170,7 @@ class E2E extends Emitter { await sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', { public_key, private_key: encodedPrivateKey, + force, }); } @@ -300,7 +301,7 @@ class E2E extends Emitter { } async changePassword(newPassword: string): Promise { - await this.persistKeys(this.getKeysFromLocalStorage(), newPassword); + await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); if (Meteor._localStorage.getItem('e2e.randomPassword')) { Meteor._localStorage.setItem('e2e.randomPassword', newPassword); diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index 92144d3405277..cd96f77a239f4 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string }): void; + 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void; } } @@ -19,12 +19,14 @@ Meteor.methods({ }); } - const keys = await Users.fetchKeysByUserId(userId); + if (!keyPair.force) { + const keys = await Users.fetchKeysByUserId(userId); - if (keys.private_key && keys.public_key) { - throw new Meteor.Error('error-keys-already-set', 'Keys already set', { - method: 'e2e.setUserPublicAndPrivateKeys', - }); + if (keys.private_key && keys.public_key) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } } await Users.setE2EPublicAndPrivateKeysByUserId(userId, { diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index d14cc642fc885..07e9d0379d6ad 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -8,6 +8,7 @@ const ajv = new Ajv({ type E2eSetUserPublicAndPrivateKeysProps = { public_key: string; private_key: string; + force?: boolean; }; const E2eSetUserPublicAndPrivateKeysSchema = { diff --git a/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts b/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts index 81ff8ce3c79da..06383408ffd9b 100644 --- a/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts +++ b/packages/rest-typings/src/v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST.ts @@ -7,6 +7,7 @@ const ajv = new Ajv({ export type e2eSetUserPublicAndPrivateKeysParamsPOST = { public_key: string; private_key: string; + force?: boolean; }; const e2eSetUserPublicAndPrivateKeysParamsPOSTSchema = { @@ -18,6 +19,9 @@ const e2eSetUserPublicAndPrivateKeysParamsPOSTSchema = { private_key: { type: 'string', }, + force: { + type: 'boolean', + }, }, additionalProperties: false, required: ['public_key', 'private_key'], From f108fd40707a7f83785858bc9109a155683ad601 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 14:05:21 -0300 Subject: [PATCH 042/215] Fix TS --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index d2d0dee2198e0..d14f4e07caa16 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -156,7 +156,11 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string, { force = false }): Promise { + async persistKeys( + { public_key, private_key }: KeyPair, + password: string, + { force }: { force: boolean } = { force: false }, + ): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } From ae76481eea6e4cbb78f30a34daca46c78cbfb034 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 14:54:59 -0300 Subject: [PATCH 043/215] Fix API tests --- apps/meteor/reporters/rocketchat.ts | 2 +- apps/meteor/tests/end-to-end/api/09-rooms.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/meteor/reporters/rocketchat.ts b/apps/meteor/reporters/rocketchat.ts index d27424a1874b0..2e0ca2333f847 100644 --- a/apps/meteor/reporters/rocketchat.ts +++ b/apps/meteor/reporters/rocketchat.ts @@ -36,7 +36,7 @@ class RocketChatReporter implements Reporter { draft: this.draft, run: this.run, }; - console.log(`Sending test result to Rocket.Chat: ${JSON.stringify(payload)}`); + // console.log(`Sending test result to Rocket.Chat: ${JSON.stringify(payload)}`); return fetch(this.url, { method: 'POST', body: JSON.stringify(payload), diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index d214f44fe8088..de4668e86f485 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -374,7 +374,6 @@ describe('[Rooms]', function () { await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) - .field('t', 'e2e') .field('description', 'some_file_description') .attach('file', imgURL) .expect('Content-Type', 'application/json') @@ -391,7 +390,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('type', 'image/png'); expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); - expect(res.body.message).to.have.property('t', 'e2e'); }); }); }); @@ -491,7 +489,7 @@ describe('[Rooms]', function () { }); await request - .post(api(`rooms.mediaConfirm/${fileId}/`)) + .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -545,7 +543,7 @@ describe('[Rooms]', function () { }); it('should not allow uploading a blocked media type to a room', async () => { - await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await updateSetting('FileUpload_MediaTypeBlackList', 'text/plain'); await request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) @@ -695,8 +693,9 @@ describe('[Rooms]', function () { await request .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) - .field('t', 'e2e') - .field('description', 'some_file_description') + .send({ + description: 'some_file_description', + }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { @@ -711,7 +710,6 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('type', 'image/png'); expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description'); - expect(res.body.message).to.have.property('t', 'e2e'); }); }); }); From 18d0e87dea0ff5305910d25929b7004e7d12ef98 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 17:18:53 -0300 Subject: [PATCH 044/215] Decrypt room's last message correctly --- apps/meteor/app/e2e/client/rocketchat.e2e.room.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index db40280fd645a..4903dc0126682 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -186,20 +186,20 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - const data = await (subscription.lastMessage?.msg && this.decrypt(subscription.lastMessage.msg)); - if (!data?.text) { + if (subscription.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } + const message = await this.decryptMessage(subscription.lastMessage); + Subscriptions.update( { _id: subscription._id, }, { $set: { - 'lastMessage.msg': data.text, - 'lastMessage.e2e': 'done', + lastMessage: message, }, }, ); From a96fa613ab4ebde29eca71912055c4a65f8d306d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 18:32:13 -0300 Subject: [PATCH 045/215] Try to fix tests --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 3e79caeb00da3..9f87d0e9395e8 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -679,8 +679,6 @@ test.describe.serial('e2ee room setup', () => { test.describe.serial('e2ee support legacy formats', () => { let poHomeChannel: HomeChannel; - test.use({ storageState: Users.userE2EE.state }); - test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); }); @@ -697,6 +695,8 @@ test.describe.serial('e2ee support legacy formats', () => { // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + await restoreState(page, Users.userE2EE); + const channelName = faker.string.uuid(); await poHomeChannel.sidenav.createEncryptedChannel(channelName); From fa2456ae6eea0e7b9e955b850493c9eae3cc82f7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Jun 2024 22:16:25 -0300 Subject: [PATCH 046/215] Fix tests --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 9f87d0e9395e8..c1295d9ad228c 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -695,6 +695,8 @@ test.describe.serial('e2ee support legacy formats', () => { // Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, api }) => { + await page.goto('/home'); + await restoreState(page, Users.userE2EE); const channelName = faker.string.uuid(); From 365cef813b599e812e316d2c52d1f2caf950d2c6 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 15:31:47 -0300 Subject: [PATCH 047/215] Fix preview of encrypted files --- .../message/hooks/useNormalizedMessage.ts | 8 ++++- .../room/ImageGallery/hooks/useImagesList.ts | 8 ++++- .../RoomFiles/hooks/useFilesList.ts | 8 ++++- apps/meteor/public/enc.js | 31 ++++++++++++++++--- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 6898c55ebad2e..2ae8a37b70f59 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -39,7 +39,13 @@ export const useNormalizedMessage = (message: TMessag return attachment; } - const key = Base64.encode(JSON.stringify(attachment.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...attachment.encryption, + name: normalizedMessage.file?.name, + type: normalizedMessage.file?.type, + }), + ); if (isFileAttachment(attachment)) { if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { diff --git a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts index d3db5fbed25bc..5c0fa39448a18 100644 --- a/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts +++ b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts @@ -53,7 +53,13 @@ export const useImagesList = ( const e2eRoom = await e2e.getInstanceByRoomId(file.rid); if (e2eRoom?.shouldConvertReceivedMessages()) { const decrypted = await e2e.decryptFileContent(file); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...decrypted.encryption, + name: decrypted.name, + type: decrypted.type, + }), + ); decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; Object.assign(file, decrypted); } diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts index 66913204e7825..f5450faa67546 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -72,7 +72,13 @@ export const useFilesList = ( const e2eRoom = await e2e.getInstanceByRoomId(file.rid); if (e2eRoom?.shouldConvertReceivedMessages()) { const decrypted = await e2e.decryptFileContent(file); - const key = Base64.encode(JSON.stringify(decrypted.encryption)); + const key = Base64.encode( + JSON.stringify({ + ...decrypted.encryption, + name: decrypted.name, + type: decrypted.type, + }), + ); decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`; Object.assign(file, decrypted); } diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index ea296f031e8ac..4f7564b442749 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -20,16 +20,19 @@ const decrypt = async (key, iv, file) => { return result; }; + const getUrlParams = (url) => { const urlObj = new URL(url); const k = base64DecodeString(urlObj.searchParams.get('key')); - const { key, iv } = JSON.parse(k); + urlObj.searchParams.delete('key'); + + const { key, iv, name, type } = JSON.parse(k); const newUrl = urlObj.href.replace('/file-decrypt/', '/'); - return { key, iv, url: newUrl }; + return { key, iv, url: newUrl, name, type }; }; self.addEventListener('fetch', (event) => { @@ -38,9 +41,11 @@ self.addEventListener('fetch', (event) => { } try { - const { url, key, iv } = getUrlParams(event.request.url); + const { url, key, iv, name, type } = getUrlParams(event.request.url); - const requestToFetch = new Request(url, event.request); + const requestToFetch = new Request(url, { + ...event.request, + }); event.respondWith( caches.match(requestToFetch).then((response) => { @@ -51,8 +56,24 @@ self.addEventListener('fetch', (event) => { return fetch(requestToFetch) .then(async (res) => { const file = await res.arrayBuffer(); + + if (res.status !== 200 || file.byteLength === 0) { + console.error('Failed to fetch file', { req: requestToFetch, res }); + return res; + } + const result = await decrypt(key, iv, file); - const response = new Response(result); + + const newHeaders = new Headers(res.headers); + newHeaders.set('Content-Disposition', 'inline; filename="'+name+'"'); + newHeaders.set('Content-Type', type); + + const response = new Response(result, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + await caches.open('v1').then((cache) => { cache.put(requestToFetch, response.clone()); }); From 81dad69fee6c93f19ec01e13e4ccd0ae48ec4e9a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 15:47:28 -0300 Subject: [PATCH 048/215] Fix download button --- apps/meteor/public/enc.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 4f7564b442749..48778063cdcbe 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -22,7 +22,7 @@ const decrypt = async (key, iv, file) => { }; const getUrlParams = (url) => { - const urlObj = new URL(url); + const urlObj = new URL(url, location.origin); const k = base64DecodeString(urlObj.searchParams.get('key')); @@ -97,7 +97,8 @@ self.addEventListener('message', async (event) => { if (event.data.type !== 'attachment-download') { return; } - const requestToFetch = new Request(url); + + const requestToFetch = new Request(event.data.url); const { url, key, iv } = getUrlParams(event.data.url); const res = (await caches.match(requestToFetch)) ?? (await fetch(url)); @@ -109,13 +110,13 @@ self.addEventListener('message', async (event) => { id: event.data.id, type: 'attachment-download-result', result, - }) - .catch((error) => { - console.error('Posting message failed:', error); - event.source.postMessage({ - id: event.data.id, - type: 'attachment-download-result', - error, - }); }); + // .catch((error) => { + // console.error('Posting message failed:', error); + // event.source.postMessage({ + // id: event.data.id, + // type: 'attachment-download-result', + // error, + // }); + // }); }); From 8402305f36c3d4dec87395258c298739dc967551 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 16:49:00 -0300 Subject: [PATCH 049/215] Fix download from files list --- .../RoomFiles/components/FileItem.tsx | 4 ++ .../RoomFiles/components/FileItemMenu.tsx | 41 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx index 27db74bc2c68a..a13014fb78342 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.tsx @@ -3,6 +3,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; import React from 'react'; +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import FileItemIcon from './FileItemIcon'; import FileItemMenu from './FileItemMenu'; @@ -24,6 +25,8 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { const format = useFormatDateAndTime(); const { _id, path, name, uploadedAt, type, typeGroup, user } = fileData; + const encryptedAnchorProps = useDownloadFromServiceWorker(path || '', name); + return ( {typeGroup === 'image' ? ( @@ -41,6 +44,7 @@ const FileItem = ({ fileData, onClickDelete }: FileItemProps) => { flexShrink={1} href={path} textDecorationLine='none' + {...(path?.includes('/file-decrypt/') ? encryptedAnchorProps : {})} > diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx index 459002dd8358d..157df8d78027c 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx @@ -1,10 +1,12 @@ import type { IUpload } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; import { Box, Menu, Icon } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useUserId } from '@rocket.chat/ui-contexts'; -import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react'; import { getURL } from '../../../../../../app/utils/client'; -import { download } from '../../../../../lib/download'; +import { download, downloadAs } from '../../../../../lib/download'; import { useRoom } from '../../../contexts/RoomContext'; import { useMessageDeletionIsAllowed } from '../hooks/useMessageDeletionIsAllowed'; @@ -13,11 +15,33 @@ type FileItemMenuProps = { onClickDelete: (id: IUpload['_id']) => void; }; +const ee = new Emitter>(); + +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; + + ee.emit(event.data.id, { result, id: event.data.id }); + } +}); + const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { const t = useTranslation(); const room = useRoom(); - const uid = useUserId(); - const isDeletionAllowed = useMessageDeletionIsAllowed(room._id, fileData, uid); + const userId = useUserId(); + const isDeletionAllowed = useMessageDeletionIsAllowed(room._id, fileData, userId); + + const { controller } = navigator.serviceWorker; + + const uid = useUniqueId(); + + useEffect( + () => + ee.once(uid, ({ result }) => { + downloadAs({ data: [new Blob([result])] }, fileData.name ?? t('Download')); + }), + [fileData, t, uid], + ); const menuOptions = { downLoad: { @@ -28,6 +52,15 @@ const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { ), action: () => { + if (fileData.path?.includes('/file-decrypt/')) { + controller?.postMessage({ + type: 'attachment-download', + url: fileData.path, + id: uid, + }); + return; + } + if (fileData.url && fileData.name) { const URL = window.webkitURL ?? window.URL; const href = getURL(fileData.url); From 09e3bb70cb7d3194674791669ea2a8bdf8c22a75 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 20 Jun 2024 17:33:04 -0300 Subject: [PATCH 050/215] Force cors on SW --- apps/meteor/public/enc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/public/enc.js b/apps/meteor/public/enc.js index 48778063cdcbe..2a0bcb9db83d2 100644 --- a/apps/meteor/public/enc.js +++ b/apps/meteor/public/enc.js @@ -45,6 +45,7 @@ self.addEventListener('fetch', (event) => { const requestToFetch = new Request(url, { ...event.request, + mode: 'cors', }); event.respondWith( From b739659579ac1aa21f65b7adb07e0fdd83ab8ae6 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 29 Jun 2024 15:49:38 +0530 Subject: [PATCH 051/215] feat: multiple file sharing feature with encryption --- .../app/api/server/lib/getUploadFormData.ts | 22 +- apps/meteor/app/api/server/v1/rooms.ts | 119 ++++++---- .../server/methods/sendFileMessage.ts | 218 +++++++++--------- apps/meteor/client/lib/chats/ChatAPI.ts | 4 +- .../client/lib/chats/flows/uploadFiles.ts | 160 +++++++------ apps/meteor/client/lib/chats/uploads.ts | 24 +- .../modals/FileUploadModal/FilePreview.tsx | 11 +- .../FileUploadModal/FileUploadModal.tsx | 130 ++++++++--- .../modals/FileUploadModal/GenericPreview.tsx | 20 +- ee/packages/api-client/src/index.ts | 17 +- packages/rest-typings/src/v1/rooms.ts | 3 +- 11 files changed, 434 insertions(+), 294 deletions(-) diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 85fc0658542d4..e2157aa56240e 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -28,9 +28,9 @@ export async function getUploadFormData< validate?: V; sizeLimit?: number; } = {}, -): Promise> { +): Promise<(UploadResult | undefined)[]> { const limits = { - files: 1, + files: 6, ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; @@ -38,8 +38,9 @@ export async function getUploadFormData< const fields = Object.create(null) as K; let uploadedFile: UploadResult | undefined; + let uploadedArray: (UploadResult | undefined)[] = []; - let returnResult = (_value: UploadResult) => { + let returnResult = (_values: (UploadResult | undefined)[]) => { // noop }; let returnError = (_error?: Error | string | null | undefined) => { @@ -51,13 +52,21 @@ export async function getUploadFormData< } function onEnd() { + uploadedArray.forEach((file) => { + if (!file) { + return returnError(new MeteorError('No file uploaded')); + } + if (options.validate !== undefined && !options.validate(fields)) { + return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); + } + }); if (!uploadedFile) { return returnError(new MeteorError('No file uploaded')); } - if (options.validate !== undefined && !options.validate(fields)) { - return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); + if (uploadedArray.length < 1) { + return returnError(new MeteorError('No file uploaded')); } - return returnResult(uploadedFile); + return returnResult(uploadedArray); } function onFile( @@ -90,6 +99,7 @@ export async function getUploadFormData< fields, fileBuffer: Buffer.concat(fileChunks), }; + uploadedArray.push(uploadedFile); }); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 11cc49e853fee..198c8e5885d52 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -205,62 +205,68 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const file = await getUploadFormData( + const file1 = await getUploadFormData( { request: this.request, }, { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, ); + let file = file1[0]; + let uploadedfilearray = []; + let uploadedfileurlarray = []; + + for (let i = 0; i < file1.length; i++) { + file = file1[i]; + if (!file) { + throw new Meteor.Error('invalid-field'); + } - if (!file) { - throw new Meteor.Error('invalid-field'); - } - - let { fileBuffer } = file; + let { fileBuffer } = file; - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 24); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); - const { fields } = file; + const { fields } = file; - let content; + let content; - if (fields.content) { - try { - content = JSON.parse(fields.content); - } catch (e) { - console.error(e); - throw new Meteor.Error('invalid-field-content'); + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + throw new Meteor.Error('invalid-field-content'); + } } - } - const details = { - name: file.filename, - size: fileBuffer.length, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - content, - expiresAt, - }; - - const stripExif = settings.get('Message_Attachments_Strip_Exif'); - if (stripExif) { - // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) - fileBuffer = await Media.stripExifFromBuffer(fileBuffer); - } + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, fileBuffer); + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } - uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + uploadedfilearray.push(uploadedFile); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + uploadedfileurlarray.push(uploadedFile.path); + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + } return API.v1.success({ file: { - _id: uploadedFile._id, - url: uploadedFile.path, + _id: uploadedfilearray, + url: uploadedfileurlarray, }, }); }, @@ -275,25 +281,38 @@ API.v1.addRoute( if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.unauthorized(); } - - const file = await Uploads.findOneById(this.urlParams.fileId); - - if (!file) { + const filesarray: Partial[] = []; + if (this.bodyParams.fileIds != undefined) { + for (let i = 0; i < this.bodyParams.fileIds.length; i++) { + const fileid = this.bodyParams?.fileIds && this.bodyParams?.fileIds[i]; + const file = await Uploads.findOneById(fileid || this.urlParams.fileId); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + filesarray.push(file); + } + } else { throw new Meteor.Error('invalid-file'); } - - file.description = this.bodyParams.description; - delete this.bodyParams.description; - + if (filesarray[0] != null) { + const file = await filesarray[0]; + file.description = this.bodyParams?.description; + delete this.bodyParams.description; + } + delete this.bodyParams.fileIds; await sendFileMessage( this.userId, - { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { roomId: this.urlParams.rid, file: filesarray, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }, ); - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); - - const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + for (let i = 0; i < this.urlParams.fileId.length; i++) { + await Uploads.confirmTemporaryFile(this.urlParams.fileId[i], this.userId); + } + let message; + if (filesarray[0] != null && filesarray[0]._id != undefined) { + message = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, this.userId); + } return API.v1.success({ message, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 485528a5e62fe..3b5e0e4edb927 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,6 +6,7 @@ import type { AtLeast, FilesAndAttachments, IMessage, + FileProp, } from '@rocket.chat/core-typings'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -30,121 +31,124 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL } export const parseFileIntoMessageAttachments = async ( - file: Partial, + filearr: Partial[], roomId: string, user: IUser, ): Promise => { - validateFileRequiredFields(file); - - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - const attachments: MessageAttachment[] = []; - - const files = [ - { - _id: file._id, - name: file.name || '', - type: file.type || 'file', - size: file.size || 0, - format: file.identify?.format || '', - }, - ]; - - if (/^image\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file?.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type as string, - image_size: file.size, - }; - - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; - } - - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); + const filesarray: FileProp[] = []; + filearr.forEach(async (file: Partial) => { + validateFileRequiredFields(file); + + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + + const files = [ + { + _id: file._id, + name: file.name || '', + type: file.type || 'file', + size: file.size || 0, + format: file.identify?.format || '', + }, + ]; + + if (/^image\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type as string, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; } - } catch (e) { - SystemLogger.error(e); + + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // } + // } catch (e) { + // SystemLogger.error(e); + // } + attachments.push(attachment); + } else if (/^audio\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type as string, + audio_size: file.size, + }; + attachments.push(attachment); + } else if (/^video\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type as string, + video_size: file.size as number, + }; + attachments.push(attachment); + } else { + const attachment = { + title: file.name, + type: 'file', + format: getFileExtension(file.name), + description: file.description, + title_link: fileUrl, + title_link_download: true, + size: file.size as number, + }; + attachments.push(attachment); } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type as string, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type as string, - video_size: file.size as number, - }; - attachments.push(attachment); - } else { - const attachment = { - title: file.name, - type: 'file', - format: getFileExtension(file.name), - description: file.description, - title_link: fileUrl, - title_link_download: true, - size: file.size as number, - }; - attachments.push(attachment); - } - return { files, attachments }; + filesarray.push(...files); + }); + return { files: filesarray, attachments }; }; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendFileMessage: (roomId: string, _store: string, file: Partial, msgData?: Record) => boolean; + sendFileMessage: (roomId: string, _store: string, file: Partial[], msgData?: Record) => boolean; } } @@ -156,7 +160,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial; + file: Partial[]; msgData?: Record; }, { @@ -182,7 +186,6 @@ export const sendFileMessage = async ( if (user?.type !== 'app' && !(await canAccessRoomAsync(room, user))) { return false; } - check( msgData, Match.Maybe({ @@ -202,7 +205,6 @@ export const sendFileMessage = async ( ), }), ); - const data = { rid: roomId, ts: new Date(), @@ -211,7 +213,6 @@ export const sendFileMessage = async ( msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, }; - if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; @@ -234,7 +235,6 @@ Meteor.methods({ method: 'sendFileMessage', } as any); } - return sendFileMessage(userId, { roomId, file, msgData }); }, }); diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 3fa159ad1c3b7..dafb5d49a3055 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -101,9 +101,9 @@ export type UploadsAPI = { wipeFailedOnes(): void; cancel(id: Upload['id']): void; send( - file: File, + file: File[] | File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index b44ca3271809c..b8ac332c3e9a8 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -43,10 +43,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; + function updateQueue(finalFiles: File[]) { + queue.length = 0; + finalFiles.forEach((file) => { + queue.push(file); + }); + } + const uploadFile = ( - file: File, + file: File[] | File, extraData?: Pick & { description?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ) => { chat.uploads.send( @@ -60,11 +67,9 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ); chat.composer?.clear(); imperativeModal.close(); - uploadNextFile(); }; - const uploadNextFile = (): void => { - const file = queue.pop(); + const file = queue[0]; if (!file) { chat.composer?.dismissAllQuotedMessages(); return; @@ -74,12 +79,13 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi component: FileUploadModal, props: { file, + queue, + updateQueue, fileName: file.name, fileDescription: chat.composer?.text ?? '', showDescription: room && !isRoomFederated(room), onClose: (): void => { imperativeModal.close(); - uploadNextFile(); }, onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { @@ -87,104 +93,110 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi value: fileName, }); - // encrypt attachment description const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { description }); + uploadFile(queue, { description }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { description }); + uploadFile(queue, { description }); return; } - const encryptedFile = await e2eRoom.encryptFile(file); - - if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string): Promise => { + const encryptedFilesarray: any = await Promise.all( + queue.map(async (file) => { + return await e2eRoom.encryptFile(file); + }), + ); + const filesarray = encryptedFilesarray.map((file: any) => { + return file?.file; + }); + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; - - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description, - title_link: fileUrl, - title_link_download: true, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - }; - - if (/^image\/.+/.test(file.type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - - attachments.push({ - ...attachment, - image_url: fileUrl, - image_type: file.type, - image_size: file.size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl, - audio_type: file.type, - audio_size: file.size, - }); - } else if (/^video\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - video_url: fileUrl, - video_type: file.type, - video_size: file.size, - }); - } else { - attachments.push({ - ...attachment, - size: file.size, - // format: getFileExtension(file.name), - }); - } - - const files = [ - { - _id, - name: file.name, - type: file.type, - size: file.size, + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: queue[i].name, + type: 'file', + description, + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + }; + + if (/^image\/.+/.test(queue[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(queue[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: queue[i].type, + image_size: queue[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(queue[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: queue[i].type, + audio_size: queue[i].size, + }); + } else if (/^video\/.+/.test(queue[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: queue[i].type, + video_size: queue[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: queue[i].size, + // format: getFileExtension(file.name), + }); + } + + const files = { + _id: _id[i], + name: queue[i].name, + type: queue[i].type, + size: queue[i].size, // "format": "png" - }, - ]; + }; + arrayoffiles.push(files); + } return e2eRoom.encryptMessageContent({ attachments, - files, + files: arrayoffiles, file: files[0], }); }; const fileContentData = { - type: file.type, - typeGroup: file.type.split('/')[0], + type: queue[0].type, + typeGroup: queue[0].type.split('/')[0], name: fileName, encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, }, }; const fileContent = await e2eRoom.encryptMessageContent(fileContentData); uploadFile( - encryptedFile.file, + filesarray, { t: 'e2e', }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index bcce45231fd93..bc722bd4fd666 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -30,7 +30,7 @@ const wipeFailedOnes = (): void => { }; const send = async ( - file: File, + file: File[] | File, { description, msg, @@ -44,16 +44,18 @@ const send = async ( tmid?: string; t?: IMessage['t']; }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise => { + if (!Array.isArray(file)) { + file = [file]; + } const id = Random.id(); - updateUploads((uploads) => [ ...uploads, { id, - name: file.name, + name: file[0].name || file[0]?.file?.name, percentage: 0, }, ]); @@ -116,17 +118,21 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - let content; + let content: IE2EEMessage['content']; + let fileIds: string[] = result.file._id.map((file: any) => file._id); + let fileUrlarray: string[] = result.file.url; + if (getContent) { - content = await getContent(result.file._id, result.file.url); + content = await getContent(fileIds, fileUrlarray); } - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { msg, tmid, description, t, content, + fileIds, }); } }; @@ -169,9 +175,9 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes wipeFailedOnes, cancel, send: ( - file: File, + file: File[] | File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: IE2EEMessage['content'], ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index c898c8b0081dc..72f2308ffeda6 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -41,16 +41,23 @@ const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefine type FilePreviewProps = { file: File; + key: number; + index: number; + onRemove: (index: number) => void; }; -const FilePreview = ({ file }: FilePreviewProps): ReactElement => { +const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { const fileType = getFileType(file.type); + const handleRemove = () => { + onRemove(index); + }; + if (shouldShowMediaPreview(file, fileType)) { return ; } - return ; + return ; }; export default FilePreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 312776397b32c..d15fbf41c1c4c 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,4 +1,17 @@ -import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldError, TextInput, Button } from '@rocket.chat/fuselage'; +import { + Modal, + Box, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldError, + TextInput, + Button, + Scrollable, + Tile, + Icon, +} from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -9,8 +22,10 @@ import FilePreview from './FilePreview'; type FileUploadModalProps = { onClose: () => void; + queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; + updateQueue: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -19,6 +34,8 @@ type FileUploadModalProps = { const FileUploadModal = ({ onClose, + queue = [], + updateQueue, file, fileName, fileDescription, @@ -26,7 +43,6 @@ const FileUploadModal = ({ invalidContentType, showDescription = true, }: FileUploadModalProps): ReactElement => { - const [name, setName] = useState(fileName); const [description, setDescription] = useState(fileDescription || ''); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -34,35 +50,84 @@ const FileUploadModal = ({ const ref = useAutoFocus(); - const handleName = (e: ChangeEvent): void => { - setName(e.currentTarget.value); - }; - const handleDescription = (e: ChangeEvent): void => { setDescription(e.currentTarget.value); }; + const [queue1, setQueue1] = useState(queue); - const handleSubmit: FormEventHandler = (e): void => { + const handleremove = (index: number) => { + const temp = queue1.filter((_, i) => { + return i !== index; + }); + setQueue1(temp); + }; + + const handleAddfile = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.click(); + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + const files = Array.from(target.files as FileList); + setQueue1([...queue1, ...files]); + updateQueue([...queue1, ...files]); + }; + }; + + const handleSubmit: FormEventHandler = async (e): Promise => { e.preventDefault(); - if (!name) { - return dispatchToastMessage({ + if (queue.length > 6) { + dispatchToastMessage({ type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), + message: "You can't upload more than 6 files at once", }); + onClose(); + return; } - // -1 maxFileSize means there is no limit - if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - onClose(); - return dispatchToastMessage({ - type: 'error', - message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), - }); + // Iterate over each file in the queue + for (const queuedFile of queue) { + const { name: queuedFileName, size: queuedFileSize, type: queuedFileType } = queuedFile; + if (!queuedFileName) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + return; + } + + // Validate file size + if (maxFileSize > -1 && (queuedFileSize || 0) > maxFileSize) { + onClose(); + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}+" hello testing"`, + }); + return; + } + + // Validate file content type + if (invalidContentType) { + dispatchToastMessage({ + type: 'error', + message: t('FileUpload_MediaType_NotAccepted__type__', { type: queuedFileType }), + }); + onClose(); + return; + } } + // description, + // msg, // Assuming msg is defined elsewhere + // }); - onSubmit(name, description); - }; + // Clear the composer after each file submission + // chat.composer?.clear(); + onSubmit(fileName, description); + // Close the modal after all files are submitted + // imperativeModal.close(); + }; useEffect(() => { if (invalidContentType) { dispatchToastMessage({ @@ -90,17 +155,13 @@ const FileUploadModal = ({ - - - + + + {queue1.length > 0 && + queue1.map((file, index) => )} + + - - {t('Upload_file_name')} - - - - {!name && {t('error-the-field-is-required', { field: t('Name') })}} - {showDescription && ( {t('Upload_file_description')} @@ -111,12 +172,19 @@ const FileUploadModal = ({ )} - + + + + - diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index dd251bec6769b..3d07c8d0f3c48 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,13 +1,23 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; - import { formatBytes } from '../../../../lib/utils/formatBytes'; -const GenericPreview = ({ file }: { file: File }): ReactElement => ( - - - {`${file.name} - ${formatBytes(file.size, 2)}`} +type GenericPreviewProps = { + file: File; + index: number; // Add index as a prop + onRemove: (index: number) => void; // Function to handle file removal with index +}; + +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => ( + + + + {`${file.name} - ${formatBytes(file.size, 2)}`} + + + onRemove(index)} /> {/* Pass index to onRemove */} + ); diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index c1648e87c6152..8417a521cb238 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -221,7 +221,6 @@ export class RestClient implements RestClientInterface { } send(endpoint: string, method: string, { headers, ...options }: Omit = {}): Promise { - console.log(`hello ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); return fetch(`${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, { ...options, headers: { ...this.getCredentialsAsHeaders(), ...this.headers, ...headers }, @@ -276,13 +275,21 @@ export class RestClient implements RestClientInterface { const data = new FormData(); Object.entries(params as any).forEach(([key, value]) => { - if (value instanceof File) { + if (Array.isArray(value)) { + value.forEach((file) => { + if (file instanceof File) { + data.append(key, file, file.name); + return; + } + file && data.append(key, file as any); + }); + } else if (value instanceof File) { data.append(key, value, value.name); - return; + } else { + value && data.append(key, value as any); } - value && data.append(key, value as any); }); - console.log(`hello from file ${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`); + xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => { xhr.setRequestHeader(key, value); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index ee05f10f2c14b..069e5bcb26ee3 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -622,7 +622,7 @@ export type RoomsEndpoints = { }; '/v1/rooms.media/:rid': { - POST: (params: { file: File }) => { file: { url: string } }; + POST: (params: { file: File | File[] }) => { file: { url: string } }; }; '/v1/rooms.mediaConfirm/:rid/:fileId': { @@ -637,6 +637,7 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; content?: IE2EEMessage['content']; + fileIds?: string[]; }) => { message: IMessage | null }; }; From 74ed1f2621152bc5aaed3642ef05658623a73ef9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 29 Jun 2024 19:15:40 +0530 Subject: [PATCH 052/215] fix: file name issue --- apps/meteor/client/lib/chats/uploads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 49f4ea6230579..88c1bad7d9f3a 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -55,7 +55,7 @@ const send = async ( ...uploads, { id, - name: fileContent?.raw.name || file[0].name || file[0]?.file?.name, + name: file[0].name || fileContent?.raw.name || file[0]?.file?.name, percentage: 0, }, ]); From c93da3082453d4c274d13da128f5fe10d41eb01a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 7 Jul 2024 15:35:52 +0530 Subject: [PATCH 053/215] multiple files shared one at a time --- .../app/api/server/lib/getUploadFormData.ts | 22 +-- apps/meteor/app/api/server/v1/rooms.ts | 159 +++++++++++++----- .../client/lib/chats/flows/uploadFiles.ts | 15 +- apps/meteor/client/lib/chats/uploads.ts | 98 +++++------ .../room/composer/messageBox/MessageBox.tsx | 1 + .../FileUploadModal/FileUploadModal.tsx | 4 +- 6 files changed, 186 insertions(+), 113 deletions(-) diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index e2157aa56240e..85fc0658542d4 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -28,9 +28,9 @@ export async function getUploadFormData< validate?: V; sizeLimit?: number; } = {}, -): Promise<(UploadResult | undefined)[]> { +): Promise> { const limits = { - files: 6, + files: 1, ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; @@ -38,9 +38,8 @@ export async function getUploadFormData< const fields = Object.create(null) as K; let uploadedFile: UploadResult | undefined; - let uploadedArray: (UploadResult | undefined)[] = []; - let returnResult = (_values: (UploadResult | undefined)[]) => { + let returnResult = (_value: UploadResult) => { // noop }; let returnError = (_error?: Error | string | null | undefined) => { @@ -52,21 +51,13 @@ export async function getUploadFormData< } function onEnd() { - uploadedArray.forEach((file) => { - if (!file) { - return returnError(new MeteorError('No file uploaded')); - } - if (options.validate !== undefined && !options.validate(fields)) { - return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); - } - }); if (!uploadedFile) { return returnError(new MeteorError('No file uploaded')); } - if (uploadedArray.length < 1) { - return returnError(new MeteorError('No file uploaded')); + if (options.validate !== undefined && !options.validate(fields)) { + return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`)); } - return returnResult(uploadedArray); + return returnResult(uploadedFile); } function onFile( @@ -99,7 +90,6 @@ export async function getUploadFormData< fields, fileBuffer: Buffer.concat(fileChunks), }; - uploadedArray.push(uploadedFile); }); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3aa410869ed19..878358f317afb 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -211,73 +211,143 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const file1 = await getUploadFormData( + const file = await getUploadFormData( { request: this.request, }, { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, ); - let file = file1[0]; - let uploadedfilearray = []; - let uploadedfileurlarray = []; - - for (let i = 0; i < file1.length; i++) { - file = file1[i]; - if (!file) { - throw new Meteor.Error('invalid-field'); - } - let { fileBuffer } = file; + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 24); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); - const { fields } = file; + const { fields } = file; - let content; + let content; - if (fields.content) { - try { - content = JSON.parse(fields.content); - } catch (e) { - throw new Meteor.Error('invalid-field-content'); - } + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); } + } - const details = { - name: file.filename, - size: fileBuffer.length, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - content, - expiresAt, - }; + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; - const stripExif = settings.get('Message_Attachments_Strip_Exif'); - if (stripExif) { - // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) - fileBuffer = await Media.stripExifFromBuffer(fileBuffer); - } + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, fileBuffer); - uploadedfilearray.push(uploadedFile); + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); - uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); - uploadedfileurlarray.push(uploadedFile.path); + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); - } return API.v1.success({ file: { - _id: uploadedfilearray, - url: uploadedfileurlarray, + _id: uploadedFile._id, + url: uploadedFile.path, }, }); }, }, ); +// API.v1.addRoute( +// 'rooms.media/:rid', +// { authRequired: true }, +// { +// async post() { +// if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { +// return API.v1.unauthorized(); +// } + +// const file1 = await getUploadFormData( +// { +// request: this.request, +// }, +// { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, +// ); +// let file = file1[0]; +// let uploadedfilearray = []; +// let uploadedfileurlarray = []; + +// for (let i = 0; i < file1.length; i++) { +// file = file1[i]; +// if (!file) { +// throw new Meteor.Error('invalid-field'); +// } + +// let { fileBuffer } = file; + +// const expiresAt = new Date(); +// expiresAt.setHours(expiresAt.getHours() + 24); + +// const { fields } = file; + +// let content; + +// if (fields.content) { +// try { +// content = JSON.parse(fields.content); +// } catch (e) { +// throw new Meteor.Error('invalid-field-content'); +// } +// } + +// const details = { +// name: file.filename, +// size: fileBuffer.length, +// type: file.mimetype, +// rid: this.urlParams.rid, +// userId: this.userId, +// content, +// expiresAt, +// }; + +// const stripExif = settings.get('Message_Attachments_Strip_Exif'); +// if (stripExif) { +// // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) +// fileBuffer = await Media.stripExifFromBuffer(fileBuffer); +// } + +// const fileStore = FileUpload.getStore('Uploads'); +// const uploadedFile = await fileStore.insert(details, fileBuffer); +// uploadedfilearray.push(uploadedFile); + +// uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); +// uploadedfileurlarray.push(uploadedFile.path); + +// await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); +// } +// return API.v1.success({ +// file: { +// _id: uploadedfilearray, +// url: uploadedfileurlarray, +// }, +// }); +// }, +// }, +// ); API.v1.addRoute( 'rooms.mediaConfirm/:rid/:fileId', @@ -305,6 +375,7 @@ API.v1.addRoute( file.description = this.bodyParams?.description; delete this.bodyParams.description; } + // this.bodyParams.msg = '@test1 @test2 File(s) uploaded successfully'; delete this.bodyParams.fileIds; await sendFileMessage( this.userId, diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 67e357278100f..9fdb6b5a903ab 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -87,7 +87,8 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi onClose: (): void => { imperativeModal.close(); }, - onSubmit: async (fileName: string, description?: string): Promise => { + // onSubmit: async (fileName: string, description?: string): Promise => { + onSubmit: async (fileName: string, msg?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, @@ -96,25 +97,28 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(queue, { description }); + uploadFile(queue, { msg }); return; } const shouldConvertSentMessages = e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(queue, { description }); + uploadFile(queue, { msg }); return; } - + console.log('uploading file', file); + console.log('message ', msg); const encryptedFilesarray: any = await Promise.all( queue.map(async (file) => { return await e2eRoom.encryptFile(file); }), ); + console.log('encryptedFilesarray', encryptedFilesarray); const filesarray = encryptedFilesarray.map((file: any) => { return file?.file; }); + console.log('filesarray', filesarray); if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -123,7 +127,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: queue[i].name, type: 'file', - description, title_link: fileUrl[i], title_link_download: true, encryption: { @@ -187,6 +190,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: queue[0].type, typeGroup: queue[0].type.split('/')[0], name: fileName, + msg: msg || '', encryption: { key: encryptedFilesarray[0].key, iv: encryptedFilesarray[0].iv, @@ -194,6 +198,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }; const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + console.log('fileContent', fileContent); uploadFile( filesarray, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 88c1bad7d9f3a..a028ba8a3802f 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -60,27 +60,30 @@ const send = async ( }, ]); - try { - await new Promise((resolve, reject) => { + var fileIds: string[] = []; + var fileUrls: string[] = []; + const promisearray: Promise[] = []; + + file.map((f) => { + new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { - file, + file: f, ...(fileContent && { content: JSON.stringify(fileContent.encrypted), }), }, { - load: (event) => { - resolve(event); - }, + // load: () => resolve(), progress: (event) => { if (!event.lengthComputable) { return; } const progress = (event.loaded / event.total) * 100; if (progress === 100) { - return; + resolve(); + // return; } updateUploads((uploads) => @@ -118,55 +121,56 @@ const send = async ( xhr.onload = async () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - let content: IE2EEMessage['content']; - let fileIds: string[] = result.file._id.map((file: any) => file._id); - let fileUrlarray: string[] = result.file.url; - - if (getContent) { - content = await getContent(fileIds, fileUrlarray); + fileIds.push(result.file._id); + fileUrls.push(result.file.url); + if (fileIds.length === file.length) { + try { + let content; + if (getContent) { + content = await getContent(fileIds, fileUrls); + } + + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + msg, + tmid, + description, + t, + content, + fileIds, + }); + + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + } catch (error) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } } - - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - msg, - tmid, - description, - t, - content, - fileIds, - }); + // resolve(); } }; - if (uploads.length) { - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - emitter.once(`cancelling-${id}`, () => { xhr.abort(); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + reject(new Error('Upload cancelled')); }); }); - - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error: unknown) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - } + }); }; export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 4b7a466d6de09..1dcd435b004d3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -403,6 +403,7 @@ const MessageBox = ({ {isRecordingVideo && } {isRecordingAudio && } + Date: Wed, 17 Jul 2024 01:30:14 +0530 Subject: [PATCH 054/215] Change message API to handle file upload --- .../app/lib/server/methods/sendMessage.ts | 38 +++++++++++++++++-- apps/meteor/client/lib/chats/uploads.ts | 24 ++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index a490b5c4c67f6..dbbbd1b1dc76e 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; -import { Messages, Users } from '@rocket.chat/models'; +import { Messages, Uploads, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -14,6 +14,10 @@ import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; +import { canAccessRoomIdAsync } from '/app/authorization/server/functions/canAccessRoom'; +import { API } from '/app/api/server'; +import { IUpload } from '@rocket.chat/core-typings'; +import { sendFileMessage } from '/app/file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -111,12 +115,12 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]): any; + sendMessage(message: AtLeast, previewUrls?: string[], filesToConfirm?: string[], msgData?: any): any; } } Meteor.methods({ - async sendMessage(message, previewUrls) { + async sendMessage(message, previewUrls, filesToConfirm, msgData) { check(message, Object); const uid = Meteor.userId(); @@ -126,6 +130,34 @@ Meteor.methods({ }); } + if (filesToConfirm != undefined) { + if (!(await canAccessRoomIdAsync(message.rid, uid))) { + return API.v1.unauthorized(); + } + const filesarray: Partial[] = []; + for (let i = 0; i < filesToConfirm.length; i++) { + const fileid = filesToConfirm[i]; + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + filesarray.push(file); + } + await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); + + for (let i = 0; i < filesToConfirm.length; i++) { + await Uploads.confirmTemporaryFile(filesToConfirm[i], uid); + } + let resmessage; + if (filesarray[0] != null && filesarray[0]._id != undefined) { + resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); + } + + return API.v1.success({ + resmessage, + }); + } + try { return await executeSendMessage(uid, message, previewUrls); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index a028ba8a3802f..07390e0f0c12e 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -124,20 +124,36 @@ const send = async ( fileIds.push(result.file._id); fileUrls.push(result.file.url); if (fileIds.length === file.length) { + if (msg == undefined) { + msg = ''; + } + const text: IMessage = { + rid, + msg, + _id: id, + }; try { let content; if (getContent) { content = await getContent(fileIds, fileUrls); } - - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + const msgData = { msg, tmid, description, t, content, - fileIds, - }); + }; + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + // msg, + // tmid, + // description, + // t, + // content, + // fileIds, + // }); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { From 2e344a1ea108763f94e5c8f45ee17601f8b5d52e Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 17 Jul 2024 14:29:47 +0530 Subject: [PATCH 055/215] fix: Ensure correct preview of multiple images in shared in single message --- apps/meteor/client/providers/ImageGalleryProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index e2365e534ca30..02b04dffeeed4 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -23,8 +23,10 @@ const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { return setSingleImageUrl(target.dataset.id); } if (target?.classList.contains('gallery-item')) { + const id1 = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; + const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; - return setImageId(target.dataset.id || id); + return setImageId(id1 || target.dataset.id || id); } if (target?.classList.contains('gallery-item-container')) { return setImageId(target.dataset.id); From 671fd31f54534df9b2ed2cc9622d2edd93e203fa Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 18 Jul 2024 12:42:14 +0530 Subject: [PATCH 056/215] solved merge conflict --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 3ba1e642c5416..fa7c2d7a610b0 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -156,9 +156,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi attachments.push({ ...attachment, size: queue[i].size, - format: getFileExtension(file.name), + format: getFileExtension(queue[i].name), }); } + const files = { _id: _id[i], From 9a97c3d0fe384c373c14372597777862128178f0 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 25 Jul 2024 13:49:24 +0530 Subject: [PATCH 057/215] fix: type error of fileContent and remove extra code --- apps/meteor/app/api/server/v1/rooms.ts | 114 ++---------------- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 2 +- apps/meteor/client/lib/chats/uploads.ts | 15 +-- 4 files changed, 16 insertions(+), 117 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 878358f317afb..fc4175c996acd 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -272,82 +272,6 @@ API.v1.addRoute( }, }, ); -// API.v1.addRoute( -// 'rooms.media/:rid', -// { authRequired: true }, -// { -// async post() { -// if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { -// return API.v1.unauthorized(); -// } - -// const file1 = await getUploadFormData( -// { -// request: this.request, -// }, -// { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, -// ); -// let file = file1[0]; -// let uploadedfilearray = []; -// let uploadedfileurlarray = []; - -// for (let i = 0; i < file1.length; i++) { -// file = file1[i]; -// if (!file) { -// throw new Meteor.Error('invalid-field'); -// } - -// let { fileBuffer } = file; - -// const expiresAt = new Date(); -// expiresAt.setHours(expiresAt.getHours() + 24); - -// const { fields } = file; - -// let content; - -// if (fields.content) { -// try { -// content = JSON.parse(fields.content); -// } catch (e) { -// throw new Meteor.Error('invalid-field-content'); -// } -// } - -// const details = { -// name: file.filename, -// size: fileBuffer.length, -// type: file.mimetype, -// rid: this.urlParams.rid, -// userId: this.userId, -// content, -// expiresAt, -// }; - -// const stripExif = settings.get('Message_Attachments_Strip_Exif'); -// if (stripExif) { -// // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) -// fileBuffer = await Media.stripExifFromBuffer(fileBuffer); -// } - -// const fileStore = FileUpload.getStore('Uploads'); -// const uploadedFile = await fileStore.insert(details, fileBuffer); -// uploadedfilearray.push(uploadedFile); - -// uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); -// uploadedfileurlarray.push(uploadedFile.path); - -// await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); -// } -// return API.v1.success({ -// file: { -// _id: uploadedfilearray, -// url: uploadedfileurlarray, -// }, -// }); -// }, -// }, -// ); API.v1.addRoute( 'rooms.mediaConfirm/:rid/:fileId', @@ -357,39 +281,25 @@ API.v1.addRoute( if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = []; - if (this.bodyParams.fileIds != undefined) { - for (let i = 0; i < this.bodyParams.fileIds.length; i++) { - const fileid = this.bodyParams?.fileIds && this.bodyParams?.fileIds[i]; - const file = await Uploads.findOneById(fileid || this.urlParams.fileId); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - filesarray.push(file); - } - } else { + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { throw new Meteor.Error('invalid-file'); } - if (filesarray[0] != null) { - const file = await filesarray[0]; - file.description = this.bodyParams?.description; - delete this.bodyParams.description; - } - // this.bodyParams.msg = '@test1 @test2 File(s) uploaded successfully'; - delete this.bodyParams.fileIds; + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + await sendFileMessage( this.userId, - { roomId: this.urlParams.rid, file: filesarray, msgData: this.bodyParams }, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }, ); - for (let i = 0; i < this.urlParams.fileId.length; i++) { - await Uploads.confirmTemporaryFile(this.urlParams.fileId[i], this.userId); - } - let message; - if (filesarray[0] != null && filesarray[0]._id != undefined) { - message = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, this.userId); - } + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); return API.v1.success({ message, diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 0ef931d46bc91..0fa55d4dbcc14 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -104,7 +104,7 @@ export type UploadsAPI = { file: File[] | File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: IE2EEMessage['content'], + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fa7c2d7a610b0..cade8623b30eb 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -42,7 +42,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi file: File[] | File, extraData?: Pick & { description?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { chat.uploads.send( file, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 07390e0f0c12e..fac41d6bbc09b 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -45,7 +45,7 @@ const send = async ( t?: IMessage['t']; }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { if (!Array.isArray(file)) { file = [file]; @@ -62,7 +62,6 @@ const send = async ( var fileIds: string[] = []; var fileUrls: string[] = []; - const promisearray: Promise[] = []; file.map((f) => { new Promise((resolve, reject) => { @@ -144,16 +143,7 @@ const send = async ( t, content, }; - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - // msg, - // tmid, - // description, - // t, - // content, - // fileIds, - // }); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { @@ -176,7 +166,6 @@ const send = async ( } } } - // resolve(); } }; @@ -198,6 +187,6 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes file: File[] | File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); From 62fbc56ea7a6cb80e7580019c26ccd44b25e784c Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 31 Jul 2024 08:24:55 +0530 Subject: [PATCH 058/215] added support for single file upload --- .../app/file-upload/server/methods/sendFileMessage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 0ede869af08da..d683fac8fdbc8 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -31,12 +31,15 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL } export const parseFileIntoMessageAttachments = async ( - filearr: Partial[], + filearr: Partial[] | Partial, roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; const filesarray: FileProp[] = []; + if (!Array.isArray(filearr)) { + filearr = [filearr]; + } filearr.forEach(async (file: Partial) => { validateFileRequiredFields(file); @@ -160,7 +163,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial[]; + file: Partial[] | Partial; msgData?: Record; }, { From f88cd2610ae0590cf76e61ba27bd1028de8834f9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 1 Aug 2024 01:00:48 +0530 Subject: [PATCH 059/215] UI update added cross button on hover and added the functionality of removing files in image also --- .../modals/FileUploadModal/FilePreview.tsx | 4 +- .../modals/FileUploadModal/GenericPreview.tsx | 71 +++++++++++++++---- .../modals/FileUploadModal/ImagePreview.tsx | 33 +++++++-- .../modals/FileUploadModal/MediaPreview.tsx | 22 +++--- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index 72f2308ffeda6..57a0cf8e47b5c 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -8,7 +8,7 @@ import MediaPreview from './MediaPreview'; export enum FilePreviewType { IMAGE = 'image', AUDIO = 'audio', - VIDEO = 'video', + // VIDEO = 'video', // currently showing it in simple generic view } const getFileType = (fileType: File['type']): FilePreviewType | undefined => { @@ -54,7 +54,7 @@ const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement }; if (shouldShowMediaPreview(file, fileType)) { - return ; + return ; } return ; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index 3d07c8d0f3c48..09b75ed278498 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,24 +1,67 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import { formatBytes } from '../../../../lib/utils/formatBytes'; type GenericPreviewProps = { file: File; - index: number; // Add index as a prop - onRemove: (index: number) => void; // Function to handle file removal with index + index: number; + onRemove: (index: number) => void; }; -const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => ( - - - - {`${file.name} - ${formatBytes(file.size, 2)}`} - - - onRemove(index)} /> {/* Pass index to onRemove */} - - -); +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const buttonStyle: React.CSSProperties = { + position: 'absolute' as const, + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + return ( + + + {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( + + ) : ( + + )} + + + {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} + {`${file.name.split('.')[1]}`} + + {/*
*/} + onRemove(index)} /> + + ); +}; export default GenericPreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx index a0b0121676718..de15c8444acb7 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -8,11 +8,14 @@ import PreviewSkeleton from './PreviewSkeleton'; type ImagePreviewProps = { url: string; file: File; + onRemove: (index: number) => void; + index: number; }; -const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { +const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { const [error, setError] = useState(false); const [loading, setLoading] = useState(true); + const [isHovered, setIsHovered] = useState(false); const handleLoad = (): void => setLoading(false); const handleError = (): void => { @@ -20,23 +23,39 @@ const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { setError(true); }; + const handleMouseEnter = (): void => setIsHovered(true); + const handleMouseLeave = (): void => setIsHovered(false); + + const buttonStyle: React.CSSProperties = { + position: 'absolute', + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + if (error) { - return ; + return ; } return ( - <> + {loading && } - + onRemove(index)} /> + ); }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index 6b48a47a79efc..bee5e911675d0 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -34,9 +34,11 @@ const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader[ type MediaPreviewProps = { file: File; fileType: FilePreviewType; + onRemove: (index: number) => void; + index: number; }; -const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { +const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { const [loaded, url] = useFileAsDataURL(file); const t = useTranslation(); @@ -54,17 +56,17 @@ const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { } if (fileType === FilePreviewType.IMAGE) { - return ; + return ; } - if (fileType === FilePreviewType.VIDEO) { - return ( - - - {t('Browser_does_not_support_video_element')} - - ); - } + // if (fileType === FilePreviewType.VIDEO) { + // return ( + // + // + // {t('Browser_does_not_support_video_element')} + // + // ); + // } if (fileType === FilePreviewType.AUDIO) { return ; From 5a9f6466ef8d0a0e86ca071742b5a08b94aced1d Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 00:42:54 +0530 Subject: [PATCH 060/215] merge develop --- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 707e55480e0f6..2c145fd16f59c 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,7 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Users } from '@rocket.chat/models'; +import { Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; From 771314ee32d959fb9b513aba61b0e81f6c1283d2 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:17:40 +0530 Subject: [PATCH 061/215] feat: Added a file upload preview inside the messageBox --- .../room/composer/messageBox/MessageBox.tsx | 247 +++++++++++++++++- .../MessageBoxActionsToolbar.tsx | 4 +- .../hooks/useFileUploadAction.ts | 4 +- 3 files changed, 245 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index c29a9149d86ce..95d47926cd5ac 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction, @@ -12,10 +12,10 @@ import { MessageComposerHint, MessageComposerButton, } from '@rocket.chat/ui-composer'; -import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts'; +import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; -import React, { memo, useRef, useReducer, useCallback } from 'react'; +import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; @@ -45,6 +45,11 @@ import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; +import FilePreview from '../../modals/FileUploadModal/FilePreview'; +import { Box } from '@rocket.chat/fuselage'; +import fileSize from 'filesize'; +import { e2e } from '/app/e2e/client'; +import { getFileExtension } from '/lib/utils/getFileExtension'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -117,7 +122,60 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); + const [uploadfiles, setUploadfiles] = useState([]); + const dispatchToastMessage = useToastMessageDispatch(); + const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; + function handlefileUpload(fileslist: File[], resetFileInput?: () => void) { + setUploadfiles((prevFiles) => [...prevFiles, ...fileslist]); + resetFileInput?.(); + } + + const handleremove = (index: number) => { + if (!uploadfiles) { + return; + } + const temp = uploadfiles.filter((_, i) => { + return i !== index; + }); + setUploadfiles(temp); + }; + + const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); + }; + const uploadFile = ( + file: File[] | File, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + ) => { + if (!chat) { + console.error('Chat context not found'); + return; + } + const msg = chat.composer?.text ?? ''; + chat.composer?.clear(); + setUploadfiles([]); + chat.uploads.send( + file, + { + msg, + ...extraData, + }, + getContent, + fileContent, + ); + }; const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -158,7 +216,166 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); - const handleSendMessage = useMutableCallback(() => { + const handleSendMessage = useMutableCallback(async () => { + if (uploadfiles !== undefined && uploadfiles.length > 0) { + const msg = chat.composer?.text ?? ''; + if (uploadfiles.length > 6) { + dispatchToastMessage({ + type: 'error', + message: "You can't upload more than 6 files at once", + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + for (const queuedFile of uploadfiles) { + const { name, size } = queuedFile; + if (!name) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + + // Validate file size + if (maxFileSize > -1 && (size || 0) > maxFileSize) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + chat.composer?.clear(); + setUploadfiles([]); + return; + } + } + + Object.defineProperty(uploadfiles[0], 'name', { + writable: true, + value: uploadfiles[0].name, + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(uploadfiles, { msg }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(uploadfiles, { msg }); + return; + } + const encryptedFilesarray: any = await Promise.all( + uploadfiles.map(async (file) => { + return await e2eRoom.encryptFile(file); + }), + ); + const filesarray = encryptedFilesarray.map((file: any) => { + return file?.file; + }); + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: uploadfiles[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(uploadfiles[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: uploadfiles[i].type, + image_size: uploadfiles[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: uploadfiles[i].type, + audio_size: uploadfiles[i].size, + }); + } else if (/^video\/.+/.test(uploadfiles[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: uploadfiles[i].type, + video_size: uploadfiles[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: uploadfiles[i].size, + format: getFileExtension(uploadfiles[i].name), + }); + } + + const files = { + _id: _id[i], + name: uploadfiles[i].name, + type: uploadfiles[i].type, + size: uploadfiles[i].size, + // "format": "png" + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: uploadfiles[0], + }); + }; + + const fileContentData = { + type: uploadfiles[0].type, + typeGroup: uploadfiles[0].type.split('/')[0], + name: uploadfiles[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + uploadFile( + filesarray, + { + t: 'e2e', + }, + getContent, + fileContent, + ); + } + chat.composer?.clear(); + return; + } const text = chat.composer?.text ?? ''; chat.composer?.clear(); clearPopup(); @@ -416,6 +633,21 @@ const MessageBox = ({ aria-activedescendant={ariaActiveDescendant} />
+ + {uploadfiles !== undefined && uploadfiles.length > 0 && ( + <> + {uploadfiles.map((file, index) => ( + + ))} + + )} + @@ -455,10 +688,10 @@ const MessageBox = ({ 0))} onClick={handleSendMessage} - secondary={typing || isEditing} - info={typing || isEditing} + secondary={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} + info={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} /> )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 90216f1590699..69dbd4655a9c0 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,6 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; + handlefiles?: any; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -45,6 +46,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + handlefiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -57,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handlefiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index f911b2b63b1f4..888d7b765a79a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -7,7 +7,7 @@ import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean, handlefiles?: any): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); @@ -30,7 +30,7 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => }); return file; }); - chat?.flows.uploadFiles(filesToUpload, resetFileInput); + handlefiles(filesToUpload, resetFileInput); }; fileInputRef.current?.addEventListener('change', handleUploadChange); From 755c08b07bb856aac9b8c967bd466c947f4543b7 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:46:37 +0530 Subject: [PATCH 062/215] Fix issue in uploads.ts causing duplicate messages on send --- apps/meteor/client/lib/chats/uploads.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index fac41d6bbc09b..9a51487326f26 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -128,7 +128,6 @@ const send = async ( } const text: IMessage = { rid, - msg, _id: id, }; try { From f2bd5523a9dff4109e6f56f80ea00d3984abb50b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 22:53:20 +0530 Subject: [PATCH 063/215] Cleanup: Remove unnecessary code --- apps/meteor/app/api/server/v1/rooms.ts | 1 - ee/packages/api-client/src/index.ts | 14 +++----------- packages/rest-typings/src/v1/rooms.ts | 3 +-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index fc4175c996acd..e3296b98ef178 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -140,7 +140,6 @@ API.v1.addRoute( }, ); -// TODO: deprecate API API.v1.addRoute( 'rooms.upload/:rid', { diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index 9a16f8d1efde9..a11e032e91b29 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -260,19 +260,11 @@ export class RestClient implements RestClientInterface { const data = new FormData(); Object.entries(params as any).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((file) => { - if (file instanceof File) { - data.append(key, file, file.name); - return; - } - file && data.append(key, file as any); - }); - } else if (value instanceof File) { + if (value instanceof File) { data.append(key, value, value.name); - } else { - value && data.append(key, value as any); + return; } + value && data.append(key, value as any); }); xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 68d6305fb8122..1c0b6a360f7b6 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -622,7 +622,7 @@ export type RoomsEndpoints = { }; '/v1/rooms.media/:rid': { - POST: (params: { file: File | File[] }) => { file: { url: string } }; + POST: (params: { file: File }) => { file: { url: string } }; }; '/v1/rooms.mediaConfirm/:rid/:fileId': { @@ -637,7 +637,6 @@ export type RoomsEndpoints = { customFields?: string; t?: IMessage['t']; content?: IE2EEMessage['content']; - fileIds?: string[]; }) => { message: IMessage | null }; }; From 39ff2a3fb13cac185db88fdea2a14271fee2f327 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 7 Aug 2024 23:11:16 +0530 Subject: [PATCH 064/215] removed unused space --- apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 95d47926cd5ac..e4e7bb98a7b4d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -620,7 +620,6 @@ const MessageBox = ({ {isRecordingVideo && } {isRecordingAudio && } - Date: Thu, 8 Aug 2024 09:18:16 +0530 Subject: [PATCH 065/215] fix: lint errors --- .../app/file-upload/server/methods/sendFileMessage.ts | 2 +- apps/meteor/app/lib/server/methods/sendMessage.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 05f92bc6de658..5b0904e5afa6d 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -16,7 +16,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; -import { SystemLogger } from '../../../../server/lib/logger/system'; +// import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 2c145fd16f59c..41887ad36c0ee 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -15,10 +15,10 @@ import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -import { canAccessRoomIdAsync } from '/app/authorization/server/functions/canAccessRoom'; -import { API } from '/app/api/server'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { API } from '../../../../app/api/server/api'; import { IUpload } from '@rocket.chat/core-typings'; -import { sendFileMessage } from '/app/file-upload/server/methods/sendFileMessage'; +import { sendFileMessage } from '../../../../app/file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { From f4b60d5af443d6c7ed281160c4154090b1618d44 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 11:43:38 +0530 Subject: [PATCH 066/215] fix: lint and TS errors --- .../server/methods/sendFileMessage.ts | 5 +++-- .../client/lib/chats/flows/uploadFiles.ts | 9 +++++--- apps/meteor/client/lib/chats/uploads.ts | 10 ++++++++- .../room/composer/messageBox/MessageBox.tsx | 14 ++++++++----- .../FileUploadModal/FileUploadModal.tsx | 21 +++++-------------- .../modals/FileUploadModal/MediaPreview.tsx | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5b0904e5afa6d..70f70bb22f652 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -32,7 +32,7 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL export const parseFileIntoMessageAttachments = async ( filearr: Partial[] | Partial, - roomId: string, + // roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; @@ -219,7 +219,8 @@ export const sendFileMessage = async ( }; if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + // const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, user); data.file = files[0]; data.files = files; data.attachments = attachments; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index cade8623b30eb..4e7ebebac63bf 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -40,7 +40,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File[] | File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { @@ -159,7 +159,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi format: getFileExtension(queue[i].name), }); } - const files = { _id: _id[i], @@ -194,13 +193,17 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; uploadFile( filesarray, { t: 'e2e', }, getContent, - fileContent, + uploadFileData, ); } }, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 9a51487326f26..de17f7050d1d0 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -55,7 +55,7 @@ const send = async ( ...uploads, { id, - name: file[0].name || fileContent?.raw.name || file[0]?.file?.name, + name: file[0].name || fileContent?.raw.name || 'unknown', percentage: 0, }, ]); @@ -126,9 +126,17 @@ const send = async ( if (msg == undefined) { msg = ''; } + // const text: IMessage = { + // rid, + // _id: id, + // }; const text: IMessage = { rid, _id: id, + msg: '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), }; try { let content; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index e4e7bb98a7b4d..06300c4bcbf51 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -48,8 +48,9 @@ import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; import FilePreview from '../../modals/FileUploadModal/FilePreview'; import { Box } from '@rocket.chat/fuselage'; import fileSize from 'filesize'; -import { e2e } from '/app/e2e/client'; -import { getFileExtension } from '/lib/utils/getFileExtension'; +// import { e2e } from '/app/e2e/client'; +import { e2e } from '../../../../../app/e2e/client'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -155,7 +156,7 @@ const MessageBox = ({ }; const uploadFile = ( file: File[] | File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ) => { @@ -240,7 +241,6 @@ const MessageBox = ({ return; } - // Validate file size if (maxFileSize > -1 && (size || 0) > maxFileSize) { dispatchToastMessage({ type: 'error', @@ -364,13 +364,17 @@ const MessageBox = ({ const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; uploadFile( filesarray, { t: 'e2e', }, getContent, - fileContent, + uploadFileData, ); } chat.composer?.clear(); diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index bb8175a975ae3..c32a59772f412 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,17 +1,4 @@ -import { - Modal, - Box, - Field, - FieldGroup, - FieldLabel, - FieldRow, - FieldError, - TextInput, - Button, - Scrollable, - Tile, - Icon, -} from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, TextInput, Button, Scrollable, Tile, Icon } from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -25,7 +12,7 @@ type FileUploadModalProps = { queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; - updateQueue: (queue: File[]) => void; + updateQueue?: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -71,7 +58,9 @@ const FileUploadModal = ({ const target = e.target as HTMLInputElement; const files = Array.from(target.files as FileList); setQueue1([...queue1, ...files]); - updateQueue([...queue1, ...files]); + if (updateQueue !== undefined) { + updateQueue([...queue1, ...files]); + } }; }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index bee5e911675d0..a5eed5a03db30 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useState, memo } from 'react'; -import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; +// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view import { FilePreviewType } from './FilePreview'; import ImagePreview from './ImagePreview'; import PreviewSkeleton from './PreviewSkeleton'; From 1c0dddeb6f73b623955d3901ef8f1a064d724d78 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 12:02:39 +0530 Subject: [PATCH 067/215] fix: ensure file handling as array to resolve type errors in uploads --- apps/meteor/client/lib/chats/uploads.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index de17f7050d1d0..47637e2e5893c 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -47,23 +47,21 @@ const send = async ( getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { - if (!Array.isArray(file)) { - file = [file]; - } + const files = Array.isArray(file) ? file : [file]; const id = Random.id(); updateUploads((uploads) => [ ...uploads, { id, - name: file[0].name || fileContent?.raw.name || 'unknown', + name: files[0].name || fileContent?.raw.name || 'unknown', percentage: 0, }, ]); - var fileIds: string[] = []; - var fileUrls: string[] = []; + const fileIds: string[] = []; + const fileUrls: string[] = []; - file.map((f) => { + files.forEach((f) => { new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, @@ -74,7 +72,6 @@ const send = async ( }), }, { - // load: () => resolve(), progress: (event) => { if (!event.lengthComputable) { return; @@ -82,7 +79,6 @@ const send = async ( const progress = (event.loaded / event.total) * 100; if (progress === 100) { resolve(); - // return; } updateUploads((uploads) => @@ -122,14 +118,11 @@ const send = async ( const result = JSON.parse(xhr.responseText); fileIds.push(result.file._id); fileUrls.push(result.file.url); - if (fileIds.length === file.length) { + if (fileIds.length === files.length) { if (msg == undefined) { msg = ''; } - // const text: IMessage = { - // rid, - // _id: id, - // }; + const text: IMessage = { rid, _id: id, @@ -138,6 +131,7 @@ const send = async ( u: { _id: id, username: id }, _updatedAt: new Date(), }; + try { let content; if (getContent) { From f5213b0f46dbc55e9808351a9f3f0f3fc7beefff Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 19:06:22 +0530 Subject: [PATCH 068/215] fix: lint error and TS errors --- .../server/methods/sendFileMessage.ts | 5 ++--- .../app/lib/server/methods/sendMessage.ts | 21 +++++++++---------- .../room/composer/messageBox/MessageBox.tsx | 5 ++--- .../modals/FileUploadModal/GenericPreview.tsx | 1 + 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 70f70bb22f652..5b0904e5afa6d 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -32,7 +32,7 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL export const parseFileIntoMessageAttachments = async ( filearr: Partial[] | Partial, - // roomId: string, + roomId: string, user: IUser, ): Promise => { const attachments: MessageAttachment[] = []; @@ -219,8 +219,7 @@ export const sendFileMessage = async ( }; if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { - // const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - const { files, attachments } = await parseFileIntoMessageAttachments(file, user); + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); data.file = files[0]; data.files = files; data.attachments = attachments; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 41887ad36c0ee..b164e86674d86 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, IUpload } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; @@ -16,9 +16,8 @@ import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { API } from '../../../../app/api/server/api'; -import { IUpload } from '@rocket.chat/core-typings'; -import { sendFileMessage } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { API } from '../../../api/server/api'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -96,7 +95,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast({ throw new Error("Cannot send system messages using 'sendMessage'"); } - if (filesToConfirm != undefined) { + if (filesToConfirm !== undefined) { if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } const filesarray: Partial[] = []; - for (let i = 0; i < filesToConfirm.length; i++) { - const fileid = filesToConfirm[i]; + for (const fileid of filesToConfirm) { const file = await Uploads.findOneById(fileid); if (!file) { throw new Meteor.Error('invalid-file'); @@ -152,11 +150,11 @@ Meteor.methods({ } await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); - for (let i = 0; i < filesToConfirm.length; i++) { - await Uploads.confirmTemporaryFile(filesToConfirm[i], uid); + for (const fileid of filesToConfirm) { + await Uploads.confirmTemporaryFile(fileid, uid); } let resmessage; - if (filesarray[0] != null && filesarray[0]._id != undefined) { + if (filesarray[0] !== null && filesarray[0]._id !== undefined) { resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); } @@ -176,6 +174,7 @@ Meteor.methods({ } }, }); + // Limit a user, who does not have the "bot" role, to sending 5 msgs/second RateLimiter.limitMethod('sendMessage', 5, 1000, { async userId(userId: IUser['_id']) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 06300c4bcbf51..f21d69347b639 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -45,12 +45,11 @@ import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; -import FilePreview from '../../modals/FileUploadModal/FilePreview'; -import { Box } from '@rocket.chat/fuselage'; import fileSize from 'filesize'; -// import { e2e } from '/app/e2e/client'; import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; +import { Box } from '@rocket.chat/fuselage'; +import FilePreview from '../../modals/FileUploadModal/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index 09b75ed278498..b7d2367fc643f 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,6 +1,7 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; + import { formatBytes } from '../../../../lib/utils/formatBytes'; type GenericPreviewProps = { From 6f398b4ad830b06016a9739439a121c6b43949c8 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 21:53:54 +0530 Subject: [PATCH 069/215] fix: lint and TS errors --- .../app/lib/server/methods/sendMessage.ts | 31 +-- .../client/lib/chats/flows/uploadFiles.ts | 18 +- apps/meteor/client/lib/chats/uploads.ts | 2 +- .../room/composer/messageBox/MessageBox.tsx | 188 +++++++++++++++++- 4 files changed, 200 insertions(+), 39 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index b164e86674d86..31d3129b0c589 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -8,16 +8,16 @@ import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { API } from '../../../api/server/api'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; +import { metrics } from '../../../metrics/server'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { API } from '../../../api/server/api'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { @@ -140,19 +140,20 @@ Meteor.methods({ if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = []; - for (const fileid of filesToConfirm) { - const file = await Uploads.findOneById(fileid); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - filesarray.push(file); - } + const filesarray: Partial[] = await Promise.all( + filesToConfirm.map(async (fileid) => { + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + return file; + }), + ); + await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); - for (const fileid of filesToConfirm) { - await Uploads.confirmTemporaryFile(fileid, uid); - } + await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); + let resmessage; if (filesarray[0] !== null && filesarray[0]._id !== undefined) { resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 4e7ebebac63bf..2631f7e446416 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -75,7 +75,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi onClose: (): void => { imperativeModal.close(); }, - // onSubmit: async (fileName: string, description?: string): Promise => { onSubmit: async (fileName: string, msg?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, @@ -95,18 +94,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi uploadFile(queue, { msg }); return; } - console.log('uploading file', file); - console.log('message ', msg); - const encryptedFilesarray: any = await Promise.all( - queue.map(async (file) => { - return await e2eRoom.encryptFile(file); - }), - ); - console.log('encryptedFilesarray', encryptedFilesarray); - const filesarray = encryptedFilesarray.map((file: any) => { - return file?.file; - }); - console.log('filesarray', filesarray); + + const encryptedFilesarray: any = await Promise.all(queue.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -165,7 +156,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi name: queue[i].name, type: queue[i].type, size: queue[i].size, - // "format": "png" }; arrayoffiles.push(files); } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 47637e2e5893c..d1116d580af4b 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -119,7 +119,7 @@ const send = async ( fileIds.push(result.file._id); fileUrls.push(result.file.url); if (fileIds.length === files.length) { - if (msg == undefined) { + if (msg === undefined) { msg = ''; } diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f21d69347b639..4bebc068e2156 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -269,14 +269,10 @@ const MessageBox = ({ uploadFile(uploadfiles, { msg }); return; } - const encryptedFilesarray: any = await Promise.all( - uploadfiles.map(async (file) => { - return await e2eRoom.encryptFile(file); - }), - ); - const filesarray = encryptedFilesarray.map((file: any) => { - return file?.file; - }); + + const encryptedFilesarray: any = await Promise.all(uploadfiles.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments = []; @@ -335,7 +331,6 @@ const MessageBox = ({ name: uploadfiles[i].name, type: uploadfiles[i].type, size: uploadfiles[i].size, - // "format": "png" }; arrayoffiles.push(files); } @@ -391,6 +386,181 @@ const MessageBox = ({ }); }); + // const handleSendMessage = useMutableCallback(async () => { + // if (uploadfiles !== undefined && uploadfiles.length > 0) { + // const msg = chat.composer?.text ?? ''; + // if (uploadfiles.length > 6) { + // dispatchToastMessage({ + // type: 'error', + // message: "You can't upload more than 6 files at once", + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + // for (const queuedFile of uploadfiles) { + // const { name, size } = queuedFile; + // if (!name) { + // dispatchToastMessage({ + // type: 'error', + // message: t('error-the-field-is-required', { field: t('Name') }), + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + + // if (maxFileSize > -1 && (size || 0) > maxFileSize) { + // dispatchToastMessage({ + // type: 'error', + // message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + // }); + // chat.composer?.clear(); + // setUploadfiles([]); + // return; + // } + // } + + // Object.defineProperty(uploadfiles[0], 'name', { + // writable: true, + // value: uploadfiles[0].name, + // }); + + // const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + // if (!e2eRoom) { + // uploadFile(uploadfiles, { msg }); + // return; + // } + + // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + // if (!shouldConvertSentMessages) { + // uploadFile(uploadfiles, { msg }); + // return; + // } + // const encryptedFilesarray: any = await Promise.all( + // uploadfiles.map(async (file) => { + // return await e2eRoom.encryptFile(file); + // }), + // ); + // const filesarray = encryptedFilesarray.map((file: any) => { + // return file?.file; + // }); + // if (encryptedFilesarray[0]) { + // const getContent = async (_id: string[], fileUrl: string[]): Promise => { + // const attachments = []; + // const arrayoffiles = []; + // for (let i = 0; i < _id.length; i++) { + // const attachment: FileAttachmentProps = { + // title: uploadfiles[i].name, + // type: 'file', + // title_link: fileUrl[i], + // title_link_download: true, + // encryption: { + // key: encryptedFilesarray[i].key, + // iv: encryptedFilesarray[i].iv, + // }, + // hashes: { + // sha256: encryptedFilesarray[i].hash, + // }, + // }; + + // if (/^image\/.+/.test(uploadfiles[i].type)) { + // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + + // attachments.push({ + // ...attachment, + // image_url: fileUrl[i], + // image_type: uploadfiles[i].type, + // image_size: uploadfiles[i].size, + // ...(dimensions && { + // image_dimensions: dimensions, + // }), + // }); + // } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + // attachments.push({ + // ...attachment, + // audio_url: fileUrl[i], + // audio_type: uploadfiles[i].type, + // audio_size: uploadfiles[i].size, + // }); + // } else if (/^video\/.+/.test(uploadfiles[i].type)) { + // attachments.push({ + // ...attachment, + // video_url: fileUrl[i], + // video_type: uploadfiles[i].type, + // video_size: uploadfiles[i].size, + // }); + // } else { + // attachments.push({ + // ...attachment, + // size: uploadfiles[i].size, + // format: getFileExtension(uploadfiles[i].name), + // }); + // } + + // const files = { + // _id: _id[i], + // name: uploadfiles[i].name, + // type: uploadfiles[i].type, + // size: uploadfiles[i].size, + // // "format": "png" + // }; + // arrayoffiles.push(files); + // } + + // return e2eRoom.encryptMessageContent({ + // attachments, + // files: arrayoffiles, + // file: uploadfiles[0], + // }); + // }; + + // const fileContentData = { + // type: uploadfiles[0].type, + // typeGroup: uploadfiles[0].type.split('/')[0], + // name: uploadfiles[0].name, + // msg: msg || '', + // encryption: { + // key: encryptedFilesarray[0].key, + // iv: encryptedFilesarray[0].iv, + // }, + // hashes: { + // sha256: encryptedFilesarray[0].hash, + // }, + // }; + + // const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + // const uploadFileData = { + // raw: {}, + // encrypted: fileContent, + // }; + // uploadFile( + // filesarray, + // { + // t: 'e2e', + // }, + // getContent, + // uploadFileData, + // ); + // } + // chat.composer?.clear(); + // return; + // } + // const text = chat.composer?.text ?? ''; + // chat.composer?.clear(); + // clearPopup(); + + // onSend?.({ + // value: text, + // tshow, + // previewUrls, + // isSlashCommandAllowed, + // }); + // }); + const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); From cdc0ff5ff9f82be59af8bdf4b4e70e478ebf129d Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 21:56:23 +0530 Subject: [PATCH 070/215] removed unused code --- .../room/composer/messageBox/MessageBox.tsx | 176 ------------------ 1 file changed, 176 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 4bebc068e2156..bf1ccb3a57f02 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -385,182 +385,6 @@ const MessageBox = ({ isSlashCommandAllowed, }); }); - - // const handleSendMessage = useMutableCallback(async () => { - // if (uploadfiles !== undefined && uploadfiles.length > 0) { - // const msg = chat.composer?.text ?? ''; - // if (uploadfiles.length > 6) { - // dispatchToastMessage({ - // type: 'error', - // message: "You can't upload more than 6 files at once", - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - // for (const queuedFile of uploadfiles) { - // const { name, size } = queuedFile; - // if (!name) { - // dispatchToastMessage({ - // type: 'error', - // message: t('error-the-field-is-required', { field: t('Name') }), - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - - // if (maxFileSize > -1 && (size || 0) > maxFileSize) { - // dispatchToastMessage({ - // type: 'error', - // message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - // }); - // chat.composer?.clear(); - // setUploadfiles([]); - // return; - // } - // } - - // Object.defineProperty(uploadfiles[0], 'name', { - // writable: true, - // value: uploadfiles[0].name, - // }); - - // const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - // if (!e2eRoom) { - // uploadFile(uploadfiles, { msg }); - // return; - // } - - // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - // if (!shouldConvertSentMessages) { - // uploadFile(uploadfiles, { msg }); - // return; - // } - // const encryptedFilesarray: any = await Promise.all( - // uploadfiles.map(async (file) => { - // return await e2eRoom.encryptFile(file); - // }), - // ); - // const filesarray = encryptedFilesarray.map((file: any) => { - // return file?.file; - // }); - // if (encryptedFilesarray[0]) { - // const getContent = async (_id: string[], fileUrl: string[]): Promise => { - // const attachments = []; - // const arrayoffiles = []; - // for (let i = 0; i < _id.length; i++) { - // const attachment: FileAttachmentProps = { - // title: uploadfiles[i].name, - // type: 'file', - // title_link: fileUrl[i], - // title_link_download: true, - // encryption: { - // key: encryptedFilesarray[i].key, - // iv: encryptedFilesarray[i].iv, - // }, - // hashes: { - // sha256: encryptedFilesarray[i].hash, - // }, - // }; - - // if (/^image\/.+/.test(uploadfiles[i].type)) { - // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); - - // attachments.push({ - // ...attachment, - // image_url: fileUrl[i], - // image_type: uploadfiles[i].type, - // image_size: uploadfiles[i].size, - // ...(dimensions && { - // image_dimensions: dimensions, - // }), - // }); - // } else if (/^audio\/.+/.test(uploadfiles[i].type)) { - // attachments.push({ - // ...attachment, - // audio_url: fileUrl[i], - // audio_type: uploadfiles[i].type, - // audio_size: uploadfiles[i].size, - // }); - // } else if (/^video\/.+/.test(uploadfiles[i].type)) { - // attachments.push({ - // ...attachment, - // video_url: fileUrl[i], - // video_type: uploadfiles[i].type, - // video_size: uploadfiles[i].size, - // }); - // } else { - // attachments.push({ - // ...attachment, - // size: uploadfiles[i].size, - // format: getFileExtension(uploadfiles[i].name), - // }); - // } - - // const files = { - // _id: _id[i], - // name: uploadfiles[i].name, - // type: uploadfiles[i].type, - // size: uploadfiles[i].size, - // // "format": "png" - // }; - // arrayoffiles.push(files); - // } - - // return e2eRoom.encryptMessageContent({ - // attachments, - // files: arrayoffiles, - // file: uploadfiles[0], - // }); - // }; - - // const fileContentData = { - // type: uploadfiles[0].type, - // typeGroup: uploadfiles[0].type.split('/')[0], - // name: uploadfiles[0].name, - // msg: msg || '', - // encryption: { - // key: encryptedFilesarray[0].key, - // iv: encryptedFilesarray[0].iv, - // }, - // hashes: { - // sha256: encryptedFilesarray[0].hash, - // }, - // }; - - // const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - // const uploadFileData = { - // raw: {}, - // encrypted: fileContent, - // }; - // uploadFile( - // filesarray, - // { - // t: 'e2e', - // }, - // getContent, - // uploadFileData, - // ); - // } - // chat.composer?.clear(); - // return; - // } - // const text = chat.composer?.text ?? ''; - // chat.composer?.clear(); - // clearPopup(); - - // onSend?.({ - // value: text, - // tshow, - // previewUrls, - // isSlashCommandAllowed, - // }); - // }); - const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); From a1faa22add2d59fede70d82ab52ddf855deeb509 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 8 Aug 2024 23:23:01 +0530 Subject: [PATCH 071/215] fix: reorder imports to fix ESLint errors and removed unused changes --- apps/meteor/app/lib/server/methods/sendMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 31d3129b0c589..6aa937951f11c 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -8,12 +8,12 @@ import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { API } from '../../../api/server/api'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; -import { metrics } from '../../../metrics/server'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; +import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; @@ -95,7 +95,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast Date: Thu, 8 Aug 2024 23:35:33 +0530 Subject: [PATCH 072/215] fix: changed the variable name with camelCase and removed unwanted chages --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 - .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 6 +++--- .../MessageBoxActionsToolbar/hooks/useFileUploadAction.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 6aa937951f11c..572615e711968 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -175,7 +175,6 @@ Meteor.methods({ } }, }); - // Limit a user, who does not have the "bot" role, to sending 5 msgs/second RateLimiter.limitMethod('sendMessage', 5, 1000, { async userId(userId: IUser['_id']) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 69dbd4655a9c0..bf6f61c77bad3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,7 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - handlefiles?: any; + handleFiles?: any; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -46,7 +46,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, - handlefiles, + handleFiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -59,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handlefiles); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 888d7b765a79a..304a0973ddc68 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -7,7 +7,7 @@ import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean, handlefiles?: any): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean, handleFiles?: any): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); @@ -30,7 +30,7 @@ export const useFileUploadAction = (disabled: boolean, handlefiles?: any): Gener }); return file; }); - handlefiles(filesToUpload, resetFileInput); + handleFiles(filesToUpload, resetFileInput); }; fileInputRef.current?.addEventListener('change', handleUploadChange); From c07808308039e18c8fce323dd7375e99d515f2d9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 9 Aug 2024 02:53:32 +0530 Subject: [PATCH 073/215] fix: changed variable names to cameCase --- .../room/composer/messageBox/MessageBox.tsx | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index bf1ccb3a57f02..a414f655757b4 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -122,23 +122,19 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); - const [uploadfiles, setUploadfiles] = useState([]); + const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handlefileUpload(fileslist: File[], resetFileInput?: () => void) { - setUploadfiles((prevFiles) => [...prevFiles, ...fileslist]); + function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { + setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); resetFileInput?.(); } - const handleremove = (index: number) => { - if (!uploadfiles) { - return; - } - const temp = uploadfiles.filter((_, i) => { - return i !== index; - }); - setUploadfiles(temp); + const handleRemoveFile = (index: number) => { + const temp = [...filesToUpload]; + temp.splice(index, 1); + setFilesToUpload(temp); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -165,7 +161,7 @@ const MessageBox = ({ } const msg = chat.composer?.text ?? ''; chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); chat.uploads.send( file, { @@ -217,18 +213,18 @@ const MessageBox = ({ }); const handleSendMessage = useMutableCallback(async () => { - if (uploadfiles !== undefined && uploadfiles.length > 0) { + if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - if (uploadfiles.length > 6) { + if (filesToUpload.length > 6) { dispatchToastMessage({ type: 'error', message: "You can't upload more than 6 files at once", }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } - for (const queuedFile of uploadfiles) { + for (const queuedFile of filesToUpload) { const { name, size } = queuedFile; if (!name) { dispatchToastMessage({ @@ -236,7 +232,7 @@ const MessageBox = ({ message: t('error-the-field-is-required', { field: t('Name') }), }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } @@ -246,31 +242,31 @@ const MessageBox = ({ message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, }); chat.composer?.clear(); - setUploadfiles([]); + setFilesToUpload([]); return; } } - Object.defineProperty(uploadfiles[0], 'name', { + Object.defineProperty(filesToUpload[0], 'name', { writable: true, - value: uploadfiles[0].name, + value: filesToUpload[0].name, }); const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(uploadfiles, { msg }); + uploadFile(filesToUpload, { msg }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(uploadfiles, { msg }); + uploadFile(filesToUpload, { msg }); return; } - const encryptedFilesarray: any = await Promise.all(uploadfiles.map((file) => e2eRoom.encryptFile(file))); + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); const filesarray = encryptedFilesarray.map((file: any) => file?.file); if (encryptedFilesarray[0]) { @@ -279,7 +275,7 @@ const MessageBox = ({ const arrayoffiles = []; for (let i = 0; i < _id.length; i++) { const attachment: FileAttachmentProps = { - title: uploadfiles[i].name, + title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], title_link_download: true, @@ -292,45 +288,45 @@ const MessageBox = ({ }, }; - if (/^image\/.+/.test(uploadfiles[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(uploadfiles[i])); + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); attachments.push({ ...attachment, image_url: fileUrl[i], - image_type: uploadfiles[i].type, - image_size: uploadfiles[i].size, + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, ...(dimensions && { image_dimensions: dimensions, }), }); - } else if (/^audio\/.+/.test(uploadfiles[i].type)) { + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { attachments.push({ ...attachment, audio_url: fileUrl[i], - audio_type: uploadfiles[i].type, - audio_size: uploadfiles[i].size, + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, }); - } else if (/^video\/.+/.test(uploadfiles[i].type)) { + } else if (/^video\/.+/.test(filesToUpload[i].type)) { attachments.push({ ...attachment, video_url: fileUrl[i], - video_type: uploadfiles[i].type, - video_size: uploadfiles[i].size, + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, }); } else { attachments.push({ ...attachment, - size: uploadfiles[i].size, - format: getFileExtension(uploadfiles[i].name), + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), }); } const files = { _id: _id[i], - name: uploadfiles[i].name, - type: uploadfiles[i].type, - size: uploadfiles[i].size, + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, }; arrayoffiles.push(files); } @@ -338,14 +334,14 @@ const MessageBox = ({ return e2eRoom.encryptMessageContent({ attachments, files: arrayoffiles, - file: uploadfiles[0], + file: filesToUpload[0], }); }; const fileContentData = { - type: uploadfiles[0].type, - typeGroup: uploadfiles[0].type.split('/')[0], - name: uploadfiles[0].name, + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, msg: msg || '', encryption: { key: encryptedFilesarray[0].key, @@ -636,10 +632,10 @@ const MessageBox = ({ width: '100%', }} > - {uploadfiles !== undefined && uploadfiles.length > 0 && ( + {filesToUpload.length > 0 && ( <> - {uploadfiles.map((file, index) => ( - + {filesToUpload.map((file, index) => ( + ))} )} @@ -669,7 +665,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} - handlefiles={handlefileUpload} + handleFiles={handleFileUpload} /> @@ -684,10 +680,10 @@ const MessageBox = ({ 0))} + disabled={!canSend || (!typing && !isEditing && !(filesToUpload.length > 0))} onClick={handleSendMessage} - secondary={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} - info={typing || isEditing || (uploadfiles !== undefined && uploadfiles.length > 0)} + secondary={typing || isEditing || filesToUpload.length > 0} + info={typing || isEditing || filesToUpload.length > 0} /> )} From 0e4d827f05be4e0af9fe14618a0fb53678960b20 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 9 Aug 2024 18:58:47 +0530 Subject: [PATCH 074/215] fix: added toast message while uploading file --- .../room/composer/messageBox/MessageBox.tsx | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index a414f655757b4..8970aa8251424 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -125,12 +125,53 @@ const MessageBox = ({ const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { - setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); + + function handleFileUpload(filesList: File[], resetFileInput?: () => void) { + setFilesToUpload((prevFiles) => { + let newFilesToUpload = [...prevFiles, ...filesList]; + + if (newFilesToUpload.length > 6) { + newFilesToUpload = newFilesToUpload.slice(0, 6); + dispatchToastMessage({ + type: 'error', + message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", + }); + } + + const validFiles = newFilesToUpload.filter((queuedFile) => { + const { name, size } = queuedFile; + + if (!name) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + return false; + } + + if (maxFileSize > -1 && (size || 0) > maxFileSize) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + return false; + } + + return true; + }); + + return validFiles; + }); resetFileInput?.(); } + // function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { + // setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); + + // resetFileInput?.(); + // } + const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; temp.splice(index, 1); @@ -215,37 +256,6 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - if (filesToUpload.length > 6) { - dispatchToastMessage({ - type: 'error', - message: "You can't upload more than 6 files at once", - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - for (const queuedFile of filesToUpload) { - const { name, size } = queuedFile; - if (!name) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - - if (maxFileSize > -1 && (size || 0) > maxFileSize) { - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - }); - chat.composer?.clear(); - setFilesToUpload([]); - return; - } - } Object.defineProperty(filesToUpload[0], 'name', { writable: true, From 60a80f992f1e04458b5efc6053cce721e7dc4fa0 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 19:20:06 +0530 Subject: [PATCH 075/215] Added the files to upload in the sendMessage in the executeSendMessage --- .../app/lib/server/functions/sendMessage.ts | 24 +++++++++++-- .../app/lib/server/methods/sendMessage.ts | 36 ++++++++++++------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 4a5b8313ebcd1..28393b64bc069 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -13,6 +13,7 @@ import { settings } from '../../../settings/server'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; +import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -215,11 +216,30 @@ export function prepareMessageObject( /** * Validates and sends the message object. */ -export const sendMessage = async function (user: any, message: any, room: any, upsert = false, previewUrls?: string[]) { +export const sendMessage = async function ( + user: any, + message: any, + room: any, + upsert = false, + previewUrls?: string[], + filesArray?: Partial[] | Partial, +) { if (!user || !message || !room._id) { return false; } + if (filesArray !== undefined && (typeof filesArray !== undefined || message?.t !== 'e2e')) { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments( + Array.isArray(filesArray) ? filesArray : [filesArray], + roomId, + user, + ); + message.file = files[0]; + message.files = files; + message.attachments = attachments; + } + await validateMessage(message, room, user); prepareMessageObject(message, room._id, user); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 572615e711968..a575f5c27caa1 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -19,7 +19,12 @@ import { MessageTypes } from '../../../ui-utils/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { +export async function executeSendMessage( + uid: IUser['_id'], + message: AtLeast, + previewUrls?: string[], + filesArray?: Partial[], +) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { method: 'sendMessage', @@ -96,7 +101,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } + let filesArray: Partial[] = []; if (filesToConfirm !== undefined) { if (!(await canAccessRoomIdAsync(message.rid, uid))) { return API.v1.unauthorized(); } - const filesarray: Partial[] = await Promise.all( + filesArray = await Promise.all( filesToConfirm.map(async (fileid) => { const file = await Uploads.findOneById(fileid); if (!file) { @@ -149,23 +155,27 @@ Meteor.methods({ return file; }), ); - - await sendFileMessage(uid, { roomId: message.rid, file: filesarray, msgData }, { parseAttachmentsForE2EE: false }); + message.msg = msgData?.msg; + message.tmid = msgData?.tmid; + // description, + message.t = msgData?.t; + message.content = msgData?.content; + // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); - let resmessage; - if (filesarray[0] !== null && filesarray[0]._id !== undefined) { - resmessage = await Messages.getMessageByFileIdAndUsername(filesarray[0]._id, uid); - } + // let resmessage; + // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { + // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); + // } - return API.v1.success({ - resmessage, - }); + // return API.v1.success({ + // resmessage, + // }); } try { - return await executeSendMessage(uid, message, previewUrls); + return await executeSendMessage(uid, message, previewUrls, filesArray); } catch (error: any) { if ((error.error || error.message) === 'error-not-allowed') { throw new Meteor.Error(error.error || error.message, error.reason, { From 7ac5841cafa7a19a89c962f1f671c1329f7bc6e1 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 19:28:32 +0530 Subject: [PATCH 076/215] fix: Revert back as using message API --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5b0904e5afa6d..8398cdd86e907 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -163,7 +163,7 @@ export const sendFileMessage = async ( msgData, }: { roomId: string; - file: Partial[] | Partial; + file: Partial; msgData?: Record; }, { @@ -189,6 +189,7 @@ export const sendFileMessage = async ( if (user?.type !== 'app' && !(await canAccessRoomAsync(room, user))) { return false; } + check( msgData, Match.Maybe({ From 9f7421944bed4ac3dd8e61e0e4d8d142b6680da3 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 10 Aug 2024 20:19:48 +0530 Subject: [PATCH 077/215] Removed unused import --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index a575f5c27caa1..e172216edb2d5 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -12,7 +12,6 @@ import { API } from '../../../api/server/api'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; From 9afeee679a2a88f9744c1d06da3ca86a3d750d1b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 11 Aug 2024 00:13:55 +0530 Subject: [PATCH 078/215] Remove unwanted code --- .../client/views/room/composer/messageBox/MessageBox.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 8970aa8251424..38e5e39a3c33e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -166,12 +166,6 @@ const MessageBox = ({ resetFileInput?.(); } - // function handleFileUpload(fileslist: File[], resetFileInput?: () => void) { - // setFilesToUpload((prevFiles) => [...prevFiles, ...fileslist]); - - // resetFileInput?.(); - // } - const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; temp.splice(index, 1); From 97f743b4c9d38e4c925dd1ff56ceaaecfad4ec94 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 12 Aug 2024 11:47:02 +0530 Subject: [PATCH 079/215] Added defineProperty for all the files selected --- .../client/views/room/composer/messageBox/MessageBox.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 38e5e39a3c33e..ac3048567b267 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -251,9 +251,11 @@ const MessageBox = ({ if (filesToUpload.length > 0) { const msg = chat.composer?.text ?? ''; - Object.defineProperty(filesToUpload[0], 'name', { - writable: true, - value: filesToUpload[0].name, + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); }); const e2eRoom = await e2e.getInstanceByRoomId(room._id); From 68f45dcbf427c1038d44074004c8a9694f4ef10a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 13 Aug 2024 15:43:43 +0530 Subject: [PATCH 080/215] Added different function for files and encrypted files sharing --- .../room/composer/messageBox/MessageBox.tsx | 275 +++++++++--------- 1 file changed, 142 insertions(+), 133 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index ac3048567b267..ec02917952543 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -126,10 +126,9 @@ const MessageBox = ({ const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFileUpload(filesList: File[], resetFileInput?: () => void) { + function handleFilesToUpload(filesList: File[], resetFileInput?: () => void) { setFilesToUpload((prevFiles) => { let newFilesToUpload = [...prevFiles, ...filesList]; - if (newFilesToUpload.length > 6) { newFilesToUpload = newFilesToUpload.slice(0, 6); dispatchToastMessage({ @@ -194,19 +193,154 @@ const MessageBox = ({ console.error('Chat context not found'); return; } - const msg = chat.composer?.text ?? ''; - chat.composer?.clear(); - setFilesToUpload([]); chat.uploads.send( file, { - msg, ...extraData, }, getContent, fileContent, ); + chat.composer?.clear(); + setFilesToUpload([]); + }; + + const handleEncryptedFilesShared = async (filesToUpload: File[], msg: string, e2eRoom: any) => { + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: filesToUpload[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); + + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, + }); + } else if (/^video\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), + }); + } + + const files = { + _id: _id[i], + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: filesToUpload[0], + }); + }; + + const fileContentData = { + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; + uploadFile( + filesarray, + { + t: 'e2e', + }, + getContent, + uploadFileData, + ); + } }; + const handleSendFiles = async (filesToUpload: File[]) => { + if (!chat || !room) { + return; + } + + const msg = chat.composer?.text ?? ''; + + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(filesToUpload, { msg }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(filesToUpload, { msg }); + return; + } + handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); + chat.composer?.clear(); + setFilesToUpload([]); + return; + }; + const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -249,132 +383,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (filesToUpload.length > 0) { - const msg = chat.composer?.text ?? ''; - - filesToUpload.forEach((file) => { - Object.defineProperty(file, 'name', { - writable: true, - value: file.name, - }); - }); - - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(filesToUpload, { msg }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(filesToUpload, { msg }); - return; - } - - const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); - - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - if (/^image\/.+/.test(filesToUpload[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - - attachments.push({ - ...attachment, - image_url: fileUrl[i], - image_type: filesToUpload[i].type, - image_size: filesToUpload[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl[i], - audio_type: filesToUpload[i].type, - audio_size: filesToUpload[i].size, - }); - } else if (/^video\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - video_url: fileUrl[i], - video_type: filesToUpload[i].type, - video_size: filesToUpload[i].size, - }); - } else { - attachments.push({ - ...attachment, - size: filesToUpload[i].size, - format: getFileExtension(filesToUpload[i].name), - }); - } - - const files = { - _id: _id[i], - name: filesToUpload[i].name, - type: filesToUpload[i].type, - size: filesToUpload[i].size, - }; - arrayoffiles.push(files); - } - - return e2eRoom.encryptMessageContent({ - attachments, - files: arrayoffiles, - file: filesToUpload[0], - }); - }; - - const fileContentData = { - type: filesToUpload[0].type, - typeGroup: filesToUpload[0].type.split('/')[0], - name: filesToUpload[0].name, - msg: msg || '', - encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, - }, - hashes: { - sha256: encryptedFilesarray[0].hash, - }, - }; - - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, - }; - uploadFile( - filesarray, - { - t: 'e2e', - }, - getContent, - uploadFileData, - ); - } - chat.composer?.clear(); - return; + return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; chat.composer?.clear(); @@ -671,7 +680,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} - handleFiles={handleFileUpload} + handleFiles={handleFilesToUpload} /> From d7b92cc33e29950e12d1a720b1fd597205d4ab30 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 13 Aug 2024 19:22:51 +0530 Subject: [PATCH 081/215] changed uploadFiles.ts and FileUploadModal.tsx to handle single file upload (for handing live audio sharing and video sharing) --- .../client/lib/chats/flows/uploadFiles.ts | 177 +++++++++--------- .../FileUploadModal/FileUploadModal.tsx | 121 +++--------- 2 files changed, 116 insertions(+), 182 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 2631f7e446416..32e17da8ac6fa 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -31,18 +31,11 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; - function updateQueue(finalFiles: File[]) { - queue.length = 0; - finalFiles.forEach((file) => { - queue.push(file); - }); - } - const uploadFile = ( - file: File[] | File, - extraData?: Pick & { msg?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + file: File, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { chat.uploads.send( file, @@ -55,9 +48,11 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi ); chat.composer?.clear(); imperativeModal.close(); + uploadNextFile(); }; + const uploadNextFile = (): void => { - const file = queue[0]; + const file = queue.pop(); if (!file) { chat.composer?.dismissAllQuotedMessages(); return; @@ -67,133 +62,131 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi component: FileUploadModal, props: { file, - queue, - updateQueue, fileName: file.name, fileDescription: chat.composer?.text ?? '', showDescription: room && !isRoomFederated(room), onClose: (): void => { imperativeModal.close(); + uploadNextFile(); }, - onSubmit: async (fileName: string, msg?: string): Promise => { + onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, }); + // encrypt attachment description const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(queue, { msg }); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(queue, { msg }); + uploadFile(file, { description }); return; } - const encryptedFilesarray: any = await Promise.all(queue.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); + const encryptedFile = await e2eRoom.encryptFile(file); - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { + if (encryptedFile) { + const getContent = async (_id: string, fileUrl: string): Promise => { const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { - const attachment: FileAttachmentProps = { - title: queue[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - if (/^image\/.+/.test(queue[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(queue[i])); - - attachments.push({ - ...attachment, - image_url: fileUrl[i], - image_type: queue[i].type, - image_size: queue[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(queue[i].type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl[i], - audio_type: queue[i].type, - audio_size: queue[i].size, - }); - } else if (/^video\/.+/.test(queue[i].type)) { - attachments.push({ - ...attachment, - video_url: fileUrl[i], - video_type: queue[i].type, - video_size: queue[i].size, - }); - } else { - attachments.push({ - ...attachment, - size: queue[i].size, - format: getFileExtension(queue[i].name), - }); - } - - const files = { - _id: _id[i], - name: queue[i].name, - type: queue[i].type, - size: queue[i].size, - }; - arrayoffiles.push(files); + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + format: getFileExtension(file.name), + }); } + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ]; + return e2eRoom.encryptMessageContent({ attachments, - files: arrayoffiles, + files, file: files[0], }); }; const fileContentData = { - type: queue[0].type, - typeGroup: queue[0].type.split('/')[0], + type: file.type, + typeGroup: file.type.split('/')[0], name: fileName, - msg: msg || '', encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, + key: encryptedFile.key, + iv: encryptedFile.iv, }, hashes: { - sha256: encryptedFilesarray[0].hash, + sha256: encryptedFile.hash, }, }; - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), }; + uploadFile( - filesarray, + encryptedFile.file, { t: 'e2e', }, getContent, - uploadFileData, + fileContent, ); } }, diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index c32a59772f412..312776397b32c 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,4 +1,4 @@ -import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, TextInput, Button, Scrollable, Tile, Icon } from '@rocket.chat/fuselage'; +import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldError, TextInput, Button } from '@rocket.chat/fuselage'; import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; @@ -9,10 +9,8 @@ import FilePreview from './FilePreview'; type FileUploadModalProps = { onClose: () => void; - queue?: File[]; onSubmit: (name: string, description?: string) => void; file: File; - updateQueue?: (queue: File[]) => void; fileName: string; fileDescription?: string; invalidContentType: boolean; @@ -21,8 +19,6 @@ type FileUploadModalProps = { const FileUploadModal = ({ onClose, - queue = [], - updateQueue, file, fileName, fileDescription, @@ -30,6 +26,7 @@ const FileUploadModal = ({ invalidContentType, showDescription = true, }: FileUploadModalProps): ReactElement => { + const [name, setName] = useState(fileName); const [description, setDescription] = useState(fileDescription || ''); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -37,88 +34,35 @@ const FileUploadModal = ({ const ref = useAutoFocus(); - const handleDescription = (e: ChangeEvent): void => { - setDescription(e.currentTarget.value); + const handleName = (e: ChangeEvent): void => { + setName(e.currentTarget.value); }; - const [queue1, setQueue1] = useState(queue); - const handleremove = (index: number) => { - const temp = queue1.filter((_, i) => { - return i !== index; - }); - setQueue1(temp); - }; - - const handleAddfile = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = true; - input.click(); - input.onchange = (e) => { - const target = e.target as HTMLInputElement; - const files = Array.from(target.files as FileList); - setQueue1([...queue1, ...files]); - if (updateQueue !== undefined) { - updateQueue([...queue1, ...files]); - } - }; + const handleDescription = (e: ChangeEvent): void => { + setDescription(e.currentTarget.value); }; - const handleSubmit: FormEventHandler = async (e): Promise => { + const handleSubmit: FormEventHandler = (e): void => { e.preventDefault(); - if (queue.length > 6) { - dispatchToastMessage({ + if (!name) { + return dispatchToastMessage({ type: 'error', - message: "You can't upload more than 6 files at once", + message: t('error-the-field-is-required', { field: t('Name') }), }); - onClose(); - return; } - // Iterate over each file in the queue - for (const queuedFile of queue) { - const { name: queuedFileName, size: queuedFileSize, type: queuedFileType } = queuedFile; - if (!queuedFileName) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); - return; - } - - // Validate file size - if (maxFileSize > -1 && (queuedFileSize || 0) > maxFileSize) { - onClose(); - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}+" hello testing"`, - }); - return; - } - - // Validate file content type - if (invalidContentType) { - dispatchToastMessage({ - type: 'error', - message: t('FileUpload_MediaType_NotAccepted__type__', { type: queuedFileType }), - }); - onClose(); - return; - } + // -1 maxFileSize means there is no limit + if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { + onClose(); + return dispatchToastMessage({ + type: 'error', + message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), + }); } - // description, - // msg, // Assuming msg is defined elsewhere - // }); - - // Clear the composer after each file submission - // chat.composer?.clear(); - const msg = description; - console.log('hello testing from send msg here ', msg); - onSubmit(fileName, msg); - // Close the modal after all files are submitted - // imperativeModal.close(); + onSubmit(name, description); }; + useEffect(() => { if (invalidContentType) { dispatchToastMessage({ @@ -146,13 +90,17 @@ const FileUploadModal = ({ - - - {queue1.length > 0 && - queue1.map((file, index) => )} - - + + + + + {t('Upload_file_name')} + + + + {!name && {t('error-the-field-is-required', { field: t('Name') })}} + {showDescription && ( {t('Upload_file_description')} @@ -163,19 +111,12 @@ const FileUploadModal = ({ )} - - - - + - From a0f6c832cdd6d2d12687b458dfd6662b97e63310 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 14 Aug 2024 13:35:30 +0530 Subject: [PATCH 082/215] Added the type check for the filesToUpload and also removed the references to chat in useFileUploadAction --- .../client/views/room/composer/messageBox/MessageBox.tsx | 6 ++++-- .../MessageBoxActionsToolbar.tsx | 2 +- .../hooks/useFileUploadAction.ts | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index ec02917952543..499f2e44bb9c1 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -100,6 +100,8 @@ type MessageBoxProps = { isEmbedded?: boolean; }; +type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; + const MessageBox = ({ tmid, onSend, @@ -126,7 +128,7 @@ const MessageBox = ({ const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - function handleFilesToUpload(filesList: File[], resetFileInput?: () => void) { + const handleFilesToUpload: HandleFilesToUpload = (filesList, resetFileInput) => { setFilesToUpload((prevFiles) => { let newFilesToUpload = [...prevFiles, ...filesList]; if (newFilesToUpload.length > 6) { @@ -163,7 +165,7 @@ const MessageBox = ({ }); resetFileInput?.(); - } + }; const handleRemoveFile = (index: number) => { const temp = [...filesToUpload]; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index bf6f61c77bad3..698d28ad2e637 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,7 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - handleFiles?: any; + handleFiles: (filesList: File[], resetFileInput?: () => void) => void; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 304a0973ddc68..db68b7dce134d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -3,15 +3,16 @@ import { useEffect } from 'react'; import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useFileInput } from '../../../../../../hooks/useFileInput'; -import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean, handleFiles?: any): GenericMenuItemProps => { +export const useFileUploadAction = ( + disabled: boolean, + handleFiles: (filesList: File[], resetFileInput?: () => void) => void, +): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); - const chat = useChat(); useEffect(() => { const resetFileInput = () => { @@ -35,7 +36,7 @@ export const useFileUploadAction = (disabled: boolean, handleFiles?: any): Gener fileInputRef.current?.addEventListener('change', handleUploadChange); return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); - }, [chat, fileInputRef]); + }, [fileInputRef]); const handleUpload = () => { fileInputRef?.current?.click(); From de319d8829cb5c8d447f01cbdbaaa29584ba295f Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 14 Aug 2024 14:40:19 +0530 Subject: [PATCH 083/215] Added isUploading and removed the unnecessary changes --- .../file-upload/server/methods/sendFileMessage.ts | 3 ++- .../views/room/composer/messageBox/MessageBox.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 8398cdd86e907..fc2e88ab3d381 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -151,7 +151,7 @@ export const parseFileIntoMessageAttachments = async ( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendFileMessage: (roomId: string, _store: string, file: Partial[], msgData?: Record) => boolean; + sendFileMessage: (roomId: string, _store: string, file: Partial, msgData?: Record) => boolean; } } @@ -241,6 +241,7 @@ Meteor.methods({ method: 'sendFileMessage', } as any); } + return sendFileMessage(userId, { roomId, file, msgData }); }, }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 499f2e44bb9c1..186be5565194c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -124,6 +124,7 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); + const [isUploading, setIsUploading] = useState(false); const [filesToUpload, setFilesToUpload] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; @@ -161,6 +162,7 @@ const MessageBox = ({ return true; }); + setIsUploading(validFiles.length > 0); return validFiles; }); @@ -171,6 +173,7 @@ const MessageBox = ({ const temp = [...filesToUpload]; temp.splice(index, 1); setFilesToUpload(temp); + setIsUploading(temp.length > 0); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -384,7 +387,7 @@ const MessageBox = ({ }); const handleSendMessage = useMutableCallback(async () => { - if (filesToUpload.length > 0) { + if (isUploading) { return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; @@ -398,6 +401,7 @@ const MessageBox = ({ isSlashCommandAllowed, }); }); + const closeEditing = (event: KeyboardEvent | MouseEvent) => { if (chat.currentEditing) { event.preventDefault(); @@ -649,7 +653,7 @@ const MessageBox = ({ width: '100%', }} > - {filesToUpload.length > 0 && ( + {isUploading && ( <> {filesToUpload.map((file, index) => ( @@ -697,10 +701,10 @@ const MessageBox = ({ 0))} + disabled={!canSend || (!typing && !isEditing && !isUploading)} onClick={handleSendMessage} - secondary={typing || isEditing || filesToUpload.length > 0} - info={typing || isEditing || filesToUpload.length > 0} + secondary={typing || isEditing || isUploading} + info={typing || isEditing || isUploading} /> )} From 3ad5e639c238570ad8164faebd874e1da9e05925 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 15 Aug 2024 17:01:11 +0530 Subject: [PATCH 084/215] Added a folder of FilePreview near the messageBox and also changed the filePreview of modal to old view --- .../messageBox/FilePreview/FilePreview.tsx | 63 +++++++++++++++ .../messageBox/FilePreview/GenericPreview.tsx | 68 ++++++++++++++++ .../messageBox/FilePreview/ImagePreview.tsx | 62 +++++++++++++++ .../messageBox/FilePreview/MediaPreview.tsx | 78 +++++++++++++++++++ .../FilePreview/PreviewSkeleton.tsx | 7 ++ .../room/composer/messageBox/MessageBox.tsx | 2 +- .../modals/FileUploadModal/FilePreview.tsx | 15 +--- .../modals/FileUploadModal/GenericPreview.tsx | 68 ++-------------- .../modals/FileUploadModal/ImagePreview.tsx | 33 ++------ .../modals/FileUploadModal/MediaPreview.tsx | 24 +++--- 10 files changed, 308 insertions(+), 112 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx new file mode 100644 index 0000000000000..959f30aaccc4e --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx @@ -0,0 +1,63 @@ +import type { ReactElement } from 'react'; +import React from 'react'; + +import { isIE11 } from '../../../../../lib/utils/isIE11'; +import GenericPreview from './GenericPreview'; +import MediaPreview from './MediaPreview'; + +export enum FilePreviewType { + IMAGE = 'image', + AUDIO = 'audio', + // VIDEO = 'video', // currently showing it in simple generic view +} + +const getFileType = (fileType: File['type']): FilePreviewType | undefined => { + if (!fileType) { + return; + } + for (const type of Object.values(FilePreviewType)) { + if (fileType.indexOf(type) > -1) { + return type; + } + } +}; + +const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefined): boolean => { + if (!fileType) { + return false; + } + if (isIE11) { + return false; + } + // Avoid preview if file size bigger than 10mb + if (file.size > 10000000) { + return false; + } + if (!Object.values(FilePreviewType).includes(fileType)) { + return false; + } + return true; +}; + +type FilePreviewProps = { + file: File; + key: number; + index: number; + onRemove: (index: number) => void; +}; + +const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { + const fileType = getFileType(file.type); + + const handleRemove = () => { + onRemove(index); + }; + + if (shouldShowMediaPreview(file, fileType)) { + return ; + } + + return ; +}; + +export default FilePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx new file mode 100644 index 0000000000000..977d17d7205fa --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -0,0 +1,68 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; + +import { formatBytes } from '../../../../../lib/utils/formatBytes'; + +type GenericPreviewProps = { + file: File; + index: number; + onRemove: (index: number) => void; +}; + +const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const buttonStyle: React.CSSProperties = { + position: 'absolute' as const, + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + return ( + + + {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( + + ) : ( + + )} + + + {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} + {`${file.name.split('.')[1]}`} + + {/*
*/} + onRemove(index)} /> + + ); +}; +export default GenericPreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx new file mode 100644 index 0000000000000..de15c8444acb7 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -0,0 +1,62 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React, { useState } from 'react'; + +import GenericPreview from './GenericPreview'; +import PreviewSkeleton from './PreviewSkeleton'; + +type ImagePreviewProps = { + url: string; + file: File; + onRemove: (index: number) => void; + index: number; +}; + +const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { + const [error, setError] = useState(false); + const [loading, setLoading] = useState(true); + const [isHovered, setIsHovered] = useState(false); + + const handleLoad = (): void => setLoading(false); + const handleError = (): void => { + setLoading(false); + setError(true); + }; + + const handleMouseEnter = (): void => setIsHovered(true); + const handleMouseLeave = (): void => setIsHovered(false); + + const buttonStyle: React.CSSProperties = { + position: 'absolute', + right: 0, + top: 0, + backgroundColor: 'gray', + display: isHovered ? 'block' : 'none', + cursor: 'pointer', + zIndex: 1, + color: 'white', + borderRadius: '100%', + }; + + if (error) { + return ; + } + + return ( + + {loading && } + + onRemove(index)} /> + + ); +}; + +export default ImagePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx new file mode 100644 index 0000000000000..a5eed5a03db30 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx @@ -0,0 +1,78 @@ +import { AudioPlayer, Box, Icon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { useEffect, useState, memo } from 'react'; + +// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view +import { FilePreviewType } from './FilePreview'; +import ImagePreview from './ImagePreview'; +import PreviewSkeleton from './PreviewSkeleton'; + +type ReaderOnloadCallback = (url: FileReader['result']) => void; + +const readFileAsDataURL = (file: File, callback: ReaderOnloadCallback): void => { + const reader = new FileReader(); + reader.onload = (e): void => callback(e?.target?.result || null); + + return reader.readAsDataURL(file); +}; + +const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader['result']] => { + const [loaded, setLoaded] = useState(false); + const [url, setUrl] = useState(null); + + useEffect(() => { + setLoaded(false); + readFileAsDataURL(file, (url) => { + setUrl(url); + setLoaded(true); + }); + }, [file]); + return [loaded, url]; +}; + +type MediaPreviewProps = { + file: File; + fileType: FilePreviewType; + onRemove: (index: number) => void; + index: number; +}; + +const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { + const [loaded, url] = useFileAsDataURL(file); + const t = useTranslation(); + + if (!loaded) { + return ; + } + + if (typeof url !== 'string') { + return ( + + + {t('FileUpload_Cannot_preview_file')} + + ); + } + + if (fileType === FilePreviewType.IMAGE) { + return ; + } + + // if (fileType === FilePreviewType.VIDEO) { + // return ( + // + // + // {t('Browser_does_not_support_video_element')} + // + // ); + // } + + if (fileType === FilePreviewType.AUDIO) { + return ; + } + + throw new Error('Wrong props provided for MediaPreview'); +}; + +export default memo(MediaPreview); diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx new file mode 100644 index 0000000000000..72017e26e9f55 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx @@ -0,0 +1,7 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React from 'react'; + +const PreviewSkeleton = (): ReactElement => ; + +export default PreviewSkeleton; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 186be5565194c..89add073785de 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -49,7 +49,7 @@ import fileSize from 'filesize'; import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { Box } from '@rocket.chat/fuselage'; -import FilePreview from '../../modals/FileUploadModal/FilePreview'; +import FilePreview from './FilePreview/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx index 57a0cf8e47b5c..c898c8b0081dc 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx @@ -8,7 +8,7 @@ import MediaPreview from './MediaPreview'; export enum FilePreviewType { IMAGE = 'image', AUDIO = 'audio', - // VIDEO = 'video', // currently showing it in simple generic view + VIDEO = 'video', } const getFileType = (fileType: File['type']): FilePreviewType | undefined => { @@ -41,23 +41,16 @@ const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefine type FilePreviewProps = { file: File; - key: number; - index: number; - onRemove: (index: number) => void; }; -const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { +const FilePreview = ({ file }: FilePreviewProps): ReactElement => { const fileType = getFileType(file.type); - const handleRemove = () => { - onRemove(index); - }; - if (shouldShowMediaPreview(file, fileType)) { - return ; + return ; } - return ; + return ; }; export default FilePreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx index b7d2367fc643f..dd251bec6769b 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/GenericPreview.tsx @@ -1,68 +1,14 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import { formatBytes } from '../../../../lib/utils/formatBytes'; -type GenericPreviewProps = { - file: File; - index: number; - onRemove: (index: number) => void; -}; +const GenericPreview = ({ file }: { file: File }): ReactElement => ( + + + {`${file.name} - ${formatBytes(file.size, 2)}`} + +); -const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { - const [isHovered, setIsHovered] = useState(false); - - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - const buttonStyle: React.CSSProperties = { - position: 'absolute' as const, - right: 0, - top: 0, - backgroundColor: 'gray', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - return ( - - - {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( - - ) : ( - - )} - - - {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} - {`${file.name.split('.')[1]}`} - - {/*
*/} - onRemove(index)} /> - - ); -}; export default GenericPreview; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx index de15c8444acb7..a0b0121676718 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/ImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; +import { Box } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -8,14 +8,11 @@ import PreviewSkeleton from './PreviewSkeleton'; type ImagePreviewProps = { url: string; file: File; - onRemove: (index: number) => void; - index: number; }; -const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { +const ImagePreview = ({ url, file }: ImagePreviewProps): ReactElement => { const [error, setError] = useState(false); const [loading, setLoading] = useState(true); - const [isHovered, setIsHovered] = useState(false); const handleLoad = (): void => setLoading(false); const handleError = (): void => { @@ -23,39 +20,23 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE setError(true); }; - const handleMouseEnter = (): void => setIsHovered(true); - const handleMouseLeave = (): void => setIsHovered(false); - - const buttonStyle: React.CSSProperties = { - position: 'absolute', - right: 0, - top: 0, - backgroundColor: 'gray', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - if (error) { - return ; + return ; } return ( - + <> {loading && } - onRemove(index)} /> - + ); }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index a5eed5a03db30..6b48a47a79efc 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useState, memo } from 'react'; -// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view +import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; import { FilePreviewType } from './FilePreview'; import ImagePreview from './ImagePreview'; import PreviewSkeleton from './PreviewSkeleton'; @@ -34,11 +34,9 @@ const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader[ type MediaPreviewProps = { file: File; fileType: FilePreviewType; - onRemove: (index: number) => void; - index: number; }; -const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { +const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { const [loaded, url] = useFileAsDataURL(file); const t = useTranslation(); @@ -56,17 +54,17 @@ const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): R } if (fileType === FilePreviewType.IMAGE) { - return ; + return ; } - // if (fileType === FilePreviewType.VIDEO) { - // return ( - // - // - // {t('Browser_does_not_support_video_element')} - // - // ); - // } + if (fileType === FilePreviewType.VIDEO) { + return ( + + + {t('Browser_does_not_support_video_element')} + + ); + } if (fileType === FilePreviewType.AUDIO) { return ; From 72bc00d938918a4a9b77afc29b100fda7e78c4d4 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 16 Aug 2024 23:17:45 +0530 Subject: [PATCH 085/215] feat: Enable file attachments after typing a message --- .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 698d28ad2e637..2ce667bd866ad 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -59,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording, handleFiles); + const fileUploadAction = useFileUploadAction(!canSend || isRecording, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); From b3db9fcccaf882083320ee5fcb9088157684e59a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 17 Aug 2024 01:21:06 +0530 Subject: [PATCH 086/215] added title to the generic and image preview --- .../room/composer/messageBox/FilePreview/GenericPreview.tsx | 1 + .../views/room/composer/messageBox/FilePreview/ImagePreview.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx index 977d17d7205fa..67bc0a5b6c45b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -40,6 +40,7 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl position='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + title={file.name} > {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx index de15c8444acb7..d73366a45f051 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -43,7 +43,7 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE } return ( - + {loading && } Date: Sat, 17 Aug 2024 12:41:23 +0530 Subject: [PATCH 087/215] fix: issue while sharing the message after file shared --- apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 89add073785de..25e070e322839 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -388,6 +388,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (isUploading) { + setIsUploading(!isUploading); return handleSendFiles(filesToUpload); } const text = chat.composer?.text ?? ''; From 11aac8e2d05819dda44f77fd453e56f1a70c942f Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 18 Aug 2024 13:50:25 +0530 Subject: [PATCH 088/215] passed uploadIdsToConfirm to the sendMessage in executeSendMessage --- .../app/lib/server/functions/sendMessage.ts | 47 +++++++++++++---- .../app/lib/server/methods/sendMessage.ts | 50 +++++-------------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index cd9febd587063..8777178e8f541 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,11 +1,13 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; +import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; +import { API } from '../../../api/server/api'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; @@ -222,22 +224,45 @@ export const sendMessage = async function ( room: any, upsert = false, previewUrls?: string[], - filesArray?: Partial[] | Partial, + uploadIdsToConfirm?: string[], ) { if (!user || !message || !room._id) { return false; } - if (filesArray !== undefined && (typeof filesArray !== undefined || message?.t !== 'e2e')) { - const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments( - Array.isArray(filesArray) ? filesArray : [filesArray], - roomId, - user, + if (uploadIdsToConfirm !== undefined) { + if (!(await canAccessRoomIdAsync(message.rid, user._id))) { + return API.v1.unauthorized(); + } + const filesToConfirm: Partial[] = await Promise.all( + uploadIdsToConfirm.map(async (fileid) => { + const file = await Uploads.findOneById(fileid); + if (!file) { + throw new Meteor.Error('invalid-file'); + } + return file; + }), ); - message.file = files[0]; - message.files = files; - message.attachments = attachments; + + // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); + if (message?.t !== 'e2e') { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments(filesToConfirm, roomId, user); + // message.file = files[0]; // as uploading multiple files + message.files = files; + message.attachments = attachments; + } + + await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); + + // let resmessage; + // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { + // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); + // } + + // return API.v1.success({ + // resmessage, + // }); } await validateMessage(message, room, user); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index e172216edb2d5..9fa1dfbdfc403 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,15 +1,13 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser, IUpload } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Uploads, Users } from '@rocket.chat/models'; +import { Messages, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { API } from '../../../api/server/api'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { metrics } from '../../../metrics/server'; @@ -22,7 +20,7 @@ export async function executeSendMessage( uid: IUser['_id'], message: AtLeast, previewUrls?: string[], - filesArray?: Partial[], + uploadIdsToConfirm?: string[], ) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { @@ -100,7 +98,7 @@ export async function executeSendMessage( } metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - return await sendMessage(user, message, room, false, previewUrls, filesArray); + return await sendMessage(user, message, room, false, previewUrls, uploadIdsToConfirm); } catch (err: any) { SystemLogger.error({ msg: 'Error sending message:', err }); @@ -121,12 +119,17 @@ export async function executeSendMessage( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendMessage(message: AtLeast, previewUrls?: string[], filesToConfirm?: string[], msgData?: any): any; + sendMessage( + message: AtLeast, + previewUrls?: string[], + uploadIdsToConfirm?: string[], + msgData?: any, + ): any; } } Meteor.methods({ - async sendMessage(message, previewUrls, filesToConfirm, msgData) { + async sendMessage(message, previewUrls, uploadIdsToConfirm, msgData) { check(message, Object); const uid = Meteor.userId(); @@ -139,42 +142,15 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } - let filesArray: Partial[] = []; - - if (filesToConfirm !== undefined) { - if (!(await canAccessRoomIdAsync(message.rid, uid))) { - return API.v1.unauthorized(); - } - filesArray = await Promise.all( - filesToConfirm.map(async (fileid) => { - const file = await Uploads.findOneById(fileid); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - return file; - }), - ); + if (msgData !== undefined) { message.msg = msgData?.msg; message.tmid = msgData?.tmid; - // description, message.t = msgData?.t; message.content = msgData?.content; - // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); - - await Promise.all(filesToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, uid))); - - // let resmessage; - // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { - // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); - // } - - // return API.v1.success({ - // resmessage, - // }); } try { - return await executeSendMessage(uid, message, previewUrls, filesArray); + return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { if ((error.error || error.message) === 'error-not-allowed') { throw new Meteor.Error(error.error || error.message, error.reason, { From 9ff1b9f8020c74d32b68fe8e2a70100600d5b042 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 18 Aug 2024 17:58:53 +0530 Subject: [PATCH 089/215] Added newe function parseMultipleFilesIntoMessageAttachments --- .../server/methods/sendFileMessage.ts | 125 ++++++++++++++++-- .../app/lib/server/functions/sendMessage.ts | 4 +- 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index fc2e88ab3d381..50d82017f6136 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -16,7 +16,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; -// import { SystemLogger } from '../../../../server/lib/logger/system'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; @@ -30,17 +30,10 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } -export const parseFileIntoMessageAttachments = async ( - filearr: Partial[] | Partial, - roomId: string, - user: IUser, -): Promise => { +export const parseMultipleFilesIntoMessageAttachments = async (files: Partial[], user: IUser): Promise => { const attachments: MessageAttachment[] = []; const filesarray: FileProp[] = []; - if (!Array.isArray(filearr)) { - filearr = [filearr]; - } - filearr.forEach(async (file: Partial) => { + files.forEach(async (file: Partial) => { validateFileRequiredFields(file); await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); @@ -148,6 +141,118 @@ export const parseFileIntoMessageAttachments = async ( return { files: filesarray, attachments }; }; +export const parseFileIntoMessageAttachments = async ( + file: Partial, + roomId: string, + user: IUser, +): Promise => { + validateFileRequiredFields(file); + + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + + const attachments: MessageAttachment[] = []; + + const files = [ + { + _id: file._id, + name: file.name || '', + type: file.type || 'file', + size: file.size || 0, + format: file.identify?.format || '', + }, + ]; + + if (/^image\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type as string, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; + } + + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + } + } catch (e) { + SystemLogger.error(e); + } + attachments.push(attachment); + } else if (/^audio\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type as string, + audio_size: file.size, + }; + attachments.push(attachment); + } else if (/^video\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type as string, + video_size: file.size as number, + }; + attachments.push(attachment); + } else { + const attachment = { + title: file.name, + type: 'file', + format: getFileExtension(file.name), + description: file.description, + title_link: fileUrl, + title_link_download: true, + size: file.size as number, + }; + attachments.push(attachment); + } + return { files, attachments }; +}; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 8777178e8f541..e3b4b5d37ec51 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -15,7 +15,7 @@ import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { parseMultipleFilesIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -247,7 +247,7 @@ export const sendMessage = async function ( // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); if (message?.t !== 'e2e') { const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments(filesToConfirm, roomId, user); + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, user); // message.file = files[0]; // as uploading multiple files message.files = files; message.attachments = attachments; From 2370b14ca8185ebbd1610a5f8a7c10309008f9de Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 19 Aug 2024 21:50:57 +0530 Subject: [PATCH 090/215] added ui changes and also added the transition --- .../messageBox/FilePreview/GenericPreview.tsx | 30 ++++++++----- .../messageBox/FilePreview/ImagePreview.tsx | 12 ++--- .../room/composer/messageBox/MessageBox.tsx | 45 ++++++++++++------- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx index 67bc0a5b6c45b..e8a4112a8bf20 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx @@ -23,9 +23,9 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl const buttonStyle: React.CSSProperties = { position: 'absolute' as const, - right: 0, - top: 0, - backgroundColor: 'gray', + right: '-10px', + top: '-8px', + backgroundColor: '#6d6c6c', display: isHovered ? 'block' : 'none', cursor: 'pointer', zIndex: 1, @@ -35,34 +35,42 @@ const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactEl return ( - + {/* currently using div */} +
{file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( - + ) : ( - + )} - +
{`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} - {`${file.name.split('.')[1]}`} + {`${file.name.split('.')[1]}`} {/*
*/} - onRemove(index)} /> + onRemove(index)} /> ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx index d73366a45f051..9c4e54c7df6f2 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx @@ -27,10 +27,10 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE const handleMouseLeave = (): void => setIsHovered(false); const buttonStyle: React.CSSProperties = { - position: 'absolute', - right: 0, - top: 0, - backgroundColor: 'gray', + position: 'absolute' as const, + right: '-10px', + top: '-8px', + backgroundColor: '#6d6c6c', display: isHovered ? 'block' : 'none', cursor: 'pointer', zIndex: 1, @@ -49,12 +49,12 @@ const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactE is='img' src={url} maxWidth='150px' - style={{ objectFit: 'contain', borderRadius: '10px' }} + style={{ objectFit: 'contain', borderRadius: '10px', height: '45px' }} onLoad={handleLoad} onError={handleError} display={loading ? 'none' : 'initial'} /> - onRemove(index)} /> + onRemove(index)} /> ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 25e070e322839..3bf14496ab73f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -168,12 +168,22 @@ const MessageBox = ({ resetFileInput?.(); }; + const handleRemoveFile = (indexToRemove: number) => { + const updatedFiles = [...filesToUpload]; - const handleRemoveFile = (index: number) => { - const temp = [...filesToUpload]; - temp.splice(index, 1); - setFilesToUpload(temp); - setIsUploading(temp.length > 0); + const element = document.getElementById(`file-preview-${indexToRemove}`); + if (element) { + element.style.transition = 'opacity 0.3s ease-in-out'; + element.style.opacity = '0'; + } + + setTimeout(() => { + updatedFiles.splice(indexToRemove, 1); + setFilesToUpload(updatedFiles); + if (element) { + element.style.opacity = '1'; + } + }, 300); }; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { @@ -649,18 +659,21 @@ const MessageBox = ({
- {isUploading && ( - <> - {filesToUpload.map((file, index) => ( - - ))} - - )} + {filesToUpload.map((file, index) => ( +
+ +
+ ))}
From 972219418807dce7325b38c202375ba3ebd3521b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 21 Aug 2024 18:19:54 +0530 Subject: [PATCH 091/215] shifted confirm files at last after save message and attached multiple files by calling parseFileIntoMessageAttachments multiple times --- .../server/methods/sendFileMessage.ts | 182 ++++-------------- .../app/lib/server/functions/sendMessage.ts | 41 ++-- 2 files changed, 53 insertions(+), 170 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 50d82017f6136..49299c259b60e 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,7 +6,6 @@ import type { AtLeast, FilesAndAttachments, IMessage, - FileProp, } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; @@ -30,117 +29,6 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } -export const parseMultipleFilesIntoMessageAttachments = async (files: Partial[], user: IUser): Promise => { - const attachments: MessageAttachment[] = []; - const filesarray: FileProp[] = []; - files.forEach(async (file: Partial) => { - validateFileRequiredFields(file); - - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - - const files = [ - { - _id: file._id, - name: file.name || '', - type: file.type || 'file', - size: file.size || 0, - format: file.identify?.format || '', - }, - ]; - - if (/^image\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file?.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type as string, - image_size: file.size, - }; - - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; - } - - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // } - // } catch (e) { - // SystemLogger.error(e); - // } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type as string, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type as string, - video_size: file.size as number, - }; - attachments.push(attachment); - } else { - const attachment = { - title: file.name, - type: 'file', - format: getFileExtension(file.name), - description: file.description, - title_link: fileUrl, - title_link_download: true, - size: file.size as number, - }; - attachments.push(attachment); - } - filesarray.push(...files); - }); - return { files: filesarray, attachments }; -}; - export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, @@ -180,39 +68,43 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - }); - } - } catch (e) { - SystemLogger.error(e); - } + // try { + // attachment.image_preview = await FileUpload.resizeImagePreview(file); + // console.log('attachement image preview ' + attachment.image_preview); + // const thumbResult = await FileUpload.createImageThumbnail(file); + // console.log('thumbResult ' + thumbResult); + // if (thumbResult) { + // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + // const thumbnail = await FileUpload.uploadImageThumbnail( + // { + // thumbFileName, + // thumbFileType, + // originalFileId, + // }, + // thumbBuffer, + // roomId, + // user._id, + // ); + // console.log('thumbnail ' + thumbnail); + // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + // attachment.image_url = thumbUrl; + // attachment.image_type = thumbnail.type; + // attachment.image_dimensions = { + // width, + // height, + // }; + // files.push({ + // _id: thumbnail._id, + // name: thumbnail.name || '', + // type: thumbnail.type || 'file', + // size: thumbnail.size || 0, + // format: thumbnail.identify?.format || '', + // }); + // console.log(files); + // } + // } catch (e) { + // SystemLogger.error(e); + // } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index e3b4b5d37ec51..09f502e4fbc9c 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,13 +1,11 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; -import { API } from '../../../api/server/api'; -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; @@ -15,7 +13,7 @@ import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseMultipleFilesIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; +import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -231,9 +229,6 @@ export const sendMessage = async function ( } if (uploadIdsToConfirm !== undefined) { - if (!(await canAccessRoomIdAsync(message.rid, user._id))) { - return API.v1.unauthorized(); - } const filesToConfirm: Partial[] = await Promise.all( uploadIdsToConfirm.map(async (fileid) => { const file = await Uploads.findOneById(fileid); @@ -243,26 +238,18 @@ export const sendMessage = async function ( return file; }), ); - - // await sendFileMessage(uid, { roomId: message.rid, file: filesArray, msgData }, { parseAttachmentsForE2EE: false }); if (message?.t !== 'e2e') { - const roomId = message.rid; - const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, user); - // message.file = files[0]; // as uploading multiple files - message.files = files; - message.attachments = attachments; + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + filesToConfirm.forEach(async (file) => { + const roomId = message.rid; + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + message.files = messageFiles; + message.attachments = messageAttachments; } - - await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); - - // let resmessage; - // if (filesArray[0] !== null && filesArray[0]._id !== undefined) { - // resmessage = await Messages.getMessageByFileIdAndUsername(filesArray[0]._id, uid); - // } - - // return API.v1.success({ - // resmessage, - // }); } await validateMessage(message, room, user); @@ -337,6 +324,10 @@ export const sendMessage = async function ( // TODO: is there an opportunity to send returned data to notifyOnMessageChange? await afterSaveMessage(message, room); + if (uploadIdsToConfirm !== undefined) { + await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); + } + void notifyOnMessageChange({ id: message._id }); void notifyOnRoomChangedById(message.rid); From 84bfcae6f3f13e2dce988554b80df2289d1fa57b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 22 Aug 2024 17:03:16 +0530 Subject: [PATCH 092/215] fix: image thumbnail display and remove extra msgData parameter --- .../server/methods/sendFileMessage.ts | 74 +++++++++---------- .../app/lib/server/functions/sendMessage.ts | 6 +- .../app/lib/server/methods/sendMessage.ts | 16 +--- apps/meteor/client/lib/chats/uploads.ts | 21 ++---- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 49299c259b60e..e9fadc442e780 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -68,43 +68,43 @@ export const parseFileIntoMessageAttachments = async ( attachment.image_dimensions = file.identify.size; } - // try { - // attachment.image_preview = await FileUpload.resizeImagePreview(file); - // console.log('attachement image preview ' + attachment.image_preview); - // const thumbResult = await FileUpload.createImageThumbnail(file); - // console.log('thumbResult ' + thumbResult); - // if (thumbResult) { - // const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - // const thumbnail = await FileUpload.uploadImageThumbnail( - // { - // thumbFileName, - // thumbFileType, - // originalFileId, - // }, - // thumbBuffer, - // roomId, - // user._id, - // ); - // console.log('thumbnail ' + thumbnail); - // const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - // attachment.image_url = thumbUrl; - // attachment.image_type = thumbnail.type; - // attachment.image_dimensions = { - // width, - // height, - // }; - // files.push({ - // _id: thumbnail._id, - // name: thumbnail.name || '', - // type: thumbnail.type || 'file', - // size: thumbnail.size || 0, - // format: thumbnail.identify?.format || '', - // }); - // console.log(files); - // } - // } catch (e) { - // SystemLogger.error(e); - // } + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + console.log('attachement image preview ' + attachment.image_preview); + const thumbResult = await FileUpload.createImageThumbnail(file); + console.log('thumbResult ' + thumbResult); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + user._id, + ); + console.log('thumbnail ' + thumbnail); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + }); + console.log(files); + } + } catch (e) { + SystemLogger.error(e); + } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { const attachment: FileAttachmentProps = { diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 09f502e4fbc9c..c2d4bdc7e248c 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -241,9 +241,13 @@ export const sendMessage = async function ( if (message?.t !== 'e2e') { const messageFiles: FileProp[] = []; const messageAttachments: MessageAttachment[] = []; - filesToConfirm.forEach(async (file) => { + const fileAttachmentPromises = filesToConfirm.map(async (file) => { const roomId = message.rid; const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + }); + const filesAndAttachments = await Promise.all(fileAttachmentPromises); + filesAndAttachments.forEach(({ files, attachments }) => { messageFiles.push(...files); messageAttachments.push(...attachments); }); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 9fa1dfbdfc403..b91e9951f00b7 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -119,17 +119,12 @@ export async function executeSendMessage( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendMessage( - message: AtLeast, - previewUrls?: string[], - uploadIdsToConfirm?: string[], - msgData?: any, - ): any; + sendMessage(message: AtLeast, previewUrls?: string[], uploadIdsToConfirm?: string[]): any; } } Meteor.methods({ - async sendMessage(message, previewUrls, uploadIdsToConfirm, msgData) { + async sendMessage(message, previewUrls, uploadIdsToConfirm) { check(message, Object); const uid = Meteor.userId(); @@ -142,13 +137,6 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } - if (msgData !== undefined) { - message.msg = msgData?.msg; - message.tmid = msgData?.tmid; - message.t = msgData?.t; - message.content = msgData?.content; - } - try { return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index d1116d580af4b..7e94cbe2a45f3 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -123,28 +123,23 @@ const send = async ( msg = ''; } - const text: IMessage = { - rid, - _id: id, - msg: '', - ts: new Date(), - u: { _id: id, username: id }, - _updatedAt: new Date(), - }; - try { let content; if (getContent) { content = await getContent(fileIds, fileUrls); } - const msgData = { - msg, + const text: IMessage = { + rid, + _id: id, + msg: msg || '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), tmid, - description, t, content, }; - await sdk.call('sendMessage', text, fileUrls, fileIds, msgData); + await sdk.call('sendMessage', text, fileUrls, fileIds); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { From 335e886cf8ebb9b5ac22531bb08686d932b06cfc Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 22 Aug 2024 19:59:35 +0530 Subject: [PATCH 093/215] removed console logs --- apps/meteor/app/file-upload/server/methods/sendFileMessage.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index e9fadc442e780..73dfd0216a732 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -70,9 +70,7 @@ export const parseFileIntoMessageAttachments = async ( try { attachment.image_preview = await FileUpload.resizeImagePreview(file); - console.log('attachement image preview ' + attachment.image_preview); const thumbResult = await FileUpload.createImageThumbnail(file); - console.log('thumbResult ' + thumbResult); if (thumbResult) { const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; const thumbnail = await FileUpload.uploadImageThumbnail( @@ -85,7 +83,6 @@ export const parseFileIntoMessageAttachments = async ( roomId, user._id, ); - console.log('thumbnail ' + thumbnail); const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); attachment.image_url = thumbUrl; attachment.image_type = thumbnail.type; @@ -100,7 +97,6 @@ export const parseFileIntoMessageAttachments = async ( size: thumbnail.size || 0, format: thumbnail.identify?.format || '', }); - console.log(files); } } catch (e) { SystemLogger.error(e); From a5598f7b191a10387b6831532848346782e03d68 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 00:22:27 +0530 Subject: [PATCH 094/215] added a function for parse and also solved eslint error of reshuffling --- .../app/lib/server/functions/sendMessage.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index c2d4bdc7e248c..54a66a4988ff5 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment, IUser } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -8,12 +8,12 @@ import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; +import { parseFileIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -import { parseFileIntoMessageAttachments } from '../../../../app/file-upload/server/methods/sendFileMessage'; // TODO: most of the types here are wrong, but I don't want to change them now @@ -213,6 +213,39 @@ export function prepareMessageObject( } } +const parseFilesIntoMessageAttachments = async ( + filesToConfirm: Partial[], + roomId: string, + user: IUser, +): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + + const fileAttachmentPromises = filesToConfirm.map(async (file) => { + try { + if (!file) { + console.warn('Skipping undefined file'); + return { files: [], attachments: [] }; + } + + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + } catch (error) { + console.error('Error processing file:', file, error); + return { files: [], attachments: [] }; + } + }); + + const filesAndAttachments = await Promise.all(fileAttachmentPromises); + + filesAndAttachments.forEach(({ files, attachments }) => { + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + + return { files: messageFiles, attachments: messageAttachments }; +}; + /** * Validates and sends the message object. */ @@ -239,20 +272,9 @@ export const sendMessage = async function ( }), ); if (message?.t !== 'e2e') { - const messageFiles: FileProp[] = []; - const messageAttachments: MessageAttachment[] = []; - const fileAttachmentPromises = filesToConfirm.map(async (file) => { - const roomId = message.rid; - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - return { files, attachments }; - }); - const filesAndAttachments = await Promise.all(fileAttachmentPromises); - filesAndAttachments.forEach(({ files, attachments }) => { - messageFiles.push(...files); - messageAttachments.push(...attachments); - }); - message.files = messageFiles; - message.attachments = messageAttachments; + const { files, attachments } = await parseFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + message.files = files; + message.attachments = attachments; } } From 7ea18d70e49f7526bcdcb5dfb2cb5b9e1dd0696a Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 12:01:36 +0530 Subject: [PATCH 095/215] fix: solved lint errors messageBox and uploadfiles --- apps/meteor/app/lib/server/methods/sendMessage.ts | 1 + apps/meteor/client/lib/chats/flows/uploadFiles.ts | 8 ++++---- apps/meteor/client/lib/chats/uploads.ts | 14 ++++++++++---- .../views/room/composer/messageBox/MessageBox.tsx | 10 +++++----- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index b91e9951f00b7..dcb203275bf41 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -137,6 +137,7 @@ Meteor.methods({ if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } + try { return await executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm); } catch (error: any) { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 32e17da8ac6fa..dde1eaee1f50a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -33,7 +33,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { description?: string }, + extraData?: Pick & { msg?: string }, getContent?: (fileId: string, fileUrl: string) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { @@ -79,14 +79,14 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { description }); + uploadFile(file); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { description }); + uploadFile(file); return; } @@ -99,7 +99,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: file.name, type: 'file', - description, title_link: fileUrl, title_link_download: true, encryption: { @@ -166,6 +165,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, + msg: description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 7e94cbe2a45f3..4920d93a19f8a 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -32,13 +32,11 @@ const wipeFailedOnes = (): void => { const send = async ( file: File[] | File, { - description, msg, rid, tmid, t, }: { - description?: string; msg?: string; rid: string; tmid?: string; @@ -141,6 +139,14 @@ const send = async ( }; await sdk.call('sendMessage', text, fileUrls, fileIds); + // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { + // msg, + // tmid, + // description, + // t, + // content, + // }); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { updateUploads((uploads) => @@ -181,8 +187,8 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File[] | File, - { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, + { msg, t }: { msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), + ): Promise => send(file, { msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 3bf14496ab73f..5d4913cbab9b2 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,6 @@ /* eslint-disable complexity */ import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction, @@ -14,15 +15,18 @@ import { } from '@rocket.chat/ui-composer'; import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import fileSize from 'filesize'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; +import { e2e } from '../../../../../app/e2e/client'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; @@ -40,16 +44,12 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; +import FilePreview from './FilePreview/FilePreview'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; -import fileSize from 'filesize'; -import { e2e } from '../../../../../app/e2e/client'; -import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; -import { Box } from '@rocket.chat/fuselage'; -import FilePreview from './FilePreview/FilePreview'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; From 822e2c5d8b70139d703405f8f1b2d859104cedc6 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 14:36:18 +0530 Subject: [PATCH 096/215] fix: lint and TS errors --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 6 ++- .../room/composer/messageBox/MessageBox.tsx | 39 +++++++++++-------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 0fa55d4dbcc14..45b9dc7c8bf36 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { msg, t, e2e }: { msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index dde1eaee1f50a..6424ae25d7f0a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,7 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, extraData?: Pick & { msg?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { chat.uploads.send( @@ -93,8 +93,10 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - const getContent = async (_id: string, fileUrl: string): Promise => { + const getContent = async (filesId: string[], filesUrl: string[]): Promise => { const attachments = []; + const _id = filesId[0]; + const fileUrl = filesUrl[0]; const attachment: FileAttachmentProps = { title: file.name, diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 5d4913cbab9b2..9de73683943e6 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -226,9 +226,10 @@ const MessageBox = ({ if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { + const attachments: FileAttachmentProps[] = []; + const arrayoffiles: any = []; + + const promises = _id.map(async (id, i) => { const attachment: FileAttachmentProps = { title: filesToUpload[i].name, type: 'file', @@ -243,10 +244,11 @@ const MessageBox = ({ }, }; + let updatedAttachment; + if (/^image\/.+/.test(filesToUpload[i].type)) { const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - - attachments.push({ + updatedAttachment = { ...attachment, image_url: fileUrl[i], image_type: filesToUpload[i].type, @@ -254,38 +256,43 @@ const MessageBox = ({ ...(dimensions && { image_dimensions: dimensions, }), - }); + }; } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - attachments.push({ + updatedAttachment = { ...attachment, audio_url: fileUrl[i], audio_type: filesToUpload[i].type, audio_size: filesToUpload[i].size, - }); + }; } else if (/^video\/.+/.test(filesToUpload[i].type)) { - attachments.push({ + updatedAttachment = { ...attachment, video_url: fileUrl[i], video_type: filesToUpload[i].type, video_size: filesToUpload[i].size, - }); + }; } else { - attachments.push({ + updatedAttachment = { ...attachment, size: filesToUpload[i].size, format: getFileExtension(filesToUpload[i].name), - }); + }; } + attachments.push(updatedAttachment); + const files = { - _id: _id[i], + _id: id, name: filesToUpload[i].name, type: filesToUpload[i].type, size: filesToUpload[i].size, }; - arrayoffiles.push(files); - } + arrayoffiles.push(files); + }); + await Promise.all(promises); + console.log('messageBox attachments ' + attachments); + console.log('messgaeBox files ' + arrayoffiles); return e2eRoom.encryptMessageContent({ attachments, files: arrayoffiles, @@ -353,7 +360,6 @@ const MessageBox = ({ handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); chat.composer?.clear(); setFilesToUpload([]); - return; }; const { isMobile } = useLayout(); @@ -665,6 +671,7 @@ const MessageBox = ({ > {filesToUpload.map((file, index) => (
Date: Fri, 23 Aug 2024 15:23:24 +0530 Subject: [PATCH 097/215] fix: lint error and converted description to msg --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 10 ++++++++-- apps/meteor/client/lib/chats/uploads.ts | 17 +++++------------ .../room/composer/messageBox/MessageBox.tsx | 9 ++++----- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 45b9dc7c8bf36..b963bc8550bef 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { msg, t, e2e }: { msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 6424ae25d7f0a..c4440afe4e3e4 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -2,6 +2,7 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rock import { isRoomFederated } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; +import { settings } from '../../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; @@ -79,14 +80,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file); + uploadFile(file, { msg: description }); + return; + } + + if (!settings.get('E2E_Enable_Encrypt_Files')) { + uploadFile(file, { msg: description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file); + uploadFile(file, { msg: description }); return; } diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 4920d93a19f8a..ad29d514bc32c 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -32,11 +32,13 @@ const wipeFailedOnes = (): void => { const send = async ( file: File[] | File, { + description, msg, rid, tmid, t, }: { + description?: string; msg?: string; rid: string; tmid?: string; @@ -129,7 +131,7 @@ const send = async ( const text: IMessage = { rid, _id: id, - msg: msg || '', + msg: msg || description || '', ts: new Date(), u: { _id: id, username: id }, _updatedAt: new Date(), @@ -138,15 +140,6 @@ const send = async ( content, }; await sdk.call('sendMessage', text, fileUrls, fileIds); - - // await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileIds[0]}`, { - // msg, - // tmid, - // description, - // t, - // content, - // }); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error) { updateUploads((uploads) => @@ -187,8 +180,8 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, send: ( file: File[] | File, - { msg, t }: { msg?: string; t?: IMessage['t'] }, + { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => send(file, { msg, rid, tmid, t }, getContent, fileContent), + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 9de73683943e6..63ad32d372144 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -227,7 +227,7 @@ const MessageBox = ({ if (encryptedFilesarray[0]) { const getContent = async (_id: string[], fileUrl: string[]): Promise => { const attachments: FileAttachmentProps[] = []; - const arrayoffiles: any = []; + const uploadFiles: any = []; const promises = _id.map(async (id, i) => { const attachment: FileAttachmentProps = { @@ -288,14 +288,13 @@ const MessageBox = ({ size: filesToUpload[i].size, }; - arrayoffiles.push(files); + uploadFiles.push(files); }); await Promise.all(promises); - console.log('messageBox attachments ' + attachments); - console.log('messgaeBox files ' + arrayoffiles); + return e2eRoom.encryptMessageContent({ attachments, - files: arrayoffiles, + files: uploadFiles, file: filesToUpload[0], }); }; From b13bdd230f84632baf925663403c62def1847fd9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 23 Aug 2024 15:52:20 +0530 Subject: [PATCH 098/215] fix: added description for TS error --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index c4440afe4e3e4..630b2a39b27bd 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -34,7 +34,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const uploadFile = ( file: File, - extraData?: Pick & { msg?: string }, + extraData?: Pick & { description?: string }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { @@ -80,19 +80,19 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } if (!settings.get('E2E_Enable_Encrypt_Files')) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { msg: description }); + uploadFile(file, { description }); return; } @@ -107,6 +107,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const attachment: FileAttachmentProps = { title: file.name, type: 'file', + description, title_link: fileUrl, title_link_download: true, encryption: { @@ -173,7 +174,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, - msg: description, + description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, From 0fe3aee5ee8981e9b493c1449f93fcb8105544fd Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 24 Aug 2024 15:23:44 +0530 Subject: [PATCH 099/215] Changed the location of parsing file into attachments and also added filter to it --- .../server/methods/sendFileMessage.ts | 33 ++++++++++++++++ .../app/lib/server/functions/sendMessage.ts | 39 ++----------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 73dfd0216a732..c63bb0e9e2efa 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -6,6 +6,7 @@ import type { AtLeast, FilesAndAttachments, IMessage, + FileProp, } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; @@ -29,6 +30,38 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL }); } +export const parseMultipleFilesIntoMessageAttachments = async ( + filesToConfirm: Partial[], + roomId: string, + user: IUser, +): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { + const messageFiles: FileProp[] = []; + const messageAttachments: MessageAttachment[] = []; + + const filesAndAttachments = await Promise.all( + filesToConfirm + .filter((files: Partial) => !!files) + .map(async (file) => { + try { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + return { files, attachments }; + } catch (error) { + console.error('Error processing file:', file, error); + return { files: [], attachments: [] }; + } + }), + ); + + filesAndAttachments + .filter(({ files, attachments }) => files.length || attachments.length) + .forEach(({ files, attachments }) => { + messageFiles.push(...files); + messageAttachments.push(...attachments); + }); + + return { files: messageFiles, attachments: messageAttachments }; +}; + export const parseFileIntoMessageAttachments = async ( file: Partial, roomId: string, diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 54a66a4988ff5..cfcf21eb29c07 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload, FileProp, MessageAttachment, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -8,7 +8,7 @@ import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; -import { parseFileIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; +import { parseMultipleFilesIntoMessageAttachments } from '../../../file-upload/server/methods/sendFileMessage'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; @@ -213,39 +213,6 @@ export function prepareMessageObject( } } -const parseFilesIntoMessageAttachments = async ( - filesToConfirm: Partial[], - roomId: string, - user: IUser, -): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { - const messageFiles: FileProp[] = []; - const messageAttachments: MessageAttachment[] = []; - - const fileAttachmentPromises = filesToConfirm.map(async (file) => { - try { - if (!file) { - console.warn('Skipping undefined file'); - return { files: [], attachments: [] }; - } - - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - return { files, attachments }; - } catch (error) { - console.error('Error processing file:', file, error); - return { files: [], attachments: [] }; - } - }); - - const filesAndAttachments = await Promise.all(fileAttachmentPromises); - - filesAndAttachments.forEach(({ files, attachments }) => { - messageFiles.push(...files); - messageAttachments.push(...attachments); - }); - - return { files: messageFiles, attachments: messageAttachments }; -}; - /** * Validates and sends the message object. */ @@ -272,7 +239,7 @@ export const sendMessage = async function ( }), ); if (message?.t !== 'e2e') { - const { files, attachments } = await parseFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); message.files = files; message.attachments = attachments; } From 65c7be7dfd69d20b7679c29e7110c1b0e0156386 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 31 Aug 2024 16:23:32 +0530 Subject: [PATCH 100/215] fix: maintain file upload order to ensure consistent fileId, fileUrl assignment, resolving encryption/decryption issues due to sequence discrepancies. --- apps/meteor/client/lib/chats/uploads.ts | 105 +++++++++++------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index ad29d514bc32c..bf444690a1445 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -58,11 +58,8 @@ const send = async ( }, ]); - const fileIds: string[] = []; - const fileUrls: string[] = []; - - files.forEach((f) => { - new Promise((resolve, reject) => { + const uploadPromises = files.map((f) => { + return new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { @@ -77,10 +74,6 @@ const send = async ( return; } const progress = (event.loaded / event.total) * 100; - if (progress === 100) { - resolve(); - } - updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { @@ -113,54 +106,10 @@ const send = async ( }, ); - xhr.onload = async () => { + xhr.onload = () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - fileIds.push(result.file._id); - fileUrls.push(result.file.url); - if (fileIds.length === files.length) { - if (msg === undefined) { - msg = ''; - } - - try { - let content; - if (getContent) { - content = await getContent(fileIds, fileUrls); - } - const text: IMessage = { - rid, - _id: id, - msg: msg || description || '', - ts: new Date(), - u: { _id: id, username: id }, - _updatedAt: new Date(), - tmid, - t, - content, - }; - await sdk.call('sendMessage', text, fileUrls, fileIds); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - } - } + resolve({ fileId: result.file._id, fileUrl: result.file.url }); } }; @@ -171,6 +120,52 @@ const send = async ( }); }); }); + + try { + const results = await Promise.all(uploadPromises); + const fileIds = results.map((result) => result.fileId); + const fileUrls = results.map((result) => result.fileUrl); + + if (msg === undefined) { + msg = ''; + } + + let content; + if (getContent) { + content = await getContent(fileIds, fileUrls); + } + const text: IMessage = { + rid, + _id: id, + msg: msg || description || '', + ts: new Date(), + u: { _id: id, username: id }, + _updatedAt: new Date(), + tmid, + t, + content, + }; + await sdk.call('sendMessage', text, fileUrls, fileIds); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + } catch (error) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } }; export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ From c4fb1046cfbcfa5466dc30d42afe0bfb70ef1cd9 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sat, 31 Aug 2024 16:41:25 +0530 Subject: [PATCH 101/215] Added different function for file upload --- .../composer/messageBox/HandleFileUploads.ts | 193 ++++++++++++++++++ .../room/composer/messageBox/MessageBox.tsx | 182 +---------------- 2 files changed, 196 insertions(+), 179 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts new file mode 100644 index 0000000000000..ef7bfc7f8c93d --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -0,0 +1,193 @@ +import type { IMessage, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; + +import { e2e } from '../../../../../app/e2e/client'; +import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; + +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; +const uploadFile = ( + file: File[] | File, + chat: any, + extraData?: Pick & { msg?: string }, + getContent?: (fileId: string[], fileUrl: string[]) => Promise, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + setFilesToUpload?: (files: File[]) => void, +) => { + if (!chat) { + console.error('Chat context not found'); + return; + } + chat.uploads.send( + file, + { + ...extraData, + }, + getContent, + fileContent, + ); + chat.composer?.clear(); + setFilesToUpload?.([]); +}; + +const handleEncryptedFilesShared = async ( + filesToUpload: File[], + chat: any, + msg: string, + e2eRoom: any, + setFilesToUpload?: (files: File[]) => void, +) => { + const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); + + const filesarray = encryptedFilesarray.map((file: any) => file?.file); + + const imgDimensions = await Promise.all( + filesToUpload.map((file) => { + if (/^image\/.+/.test(file.type)) { + return getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + } + return null; + }), + ); + + if (encryptedFilesarray[0]) { + const getContent = async (_id: string[], fileUrl: string[]): Promise => { + const attachments = []; + const arrayoffiles = []; + for (let i = 0; i < _id.length; i++) { + const attachment: FileAttachmentProps = { + title: filesToUpload[i].name, + type: 'file', + title_link: fileUrl[i], + title_link_download: true, + encryption: { + key: encryptedFilesarray[i].key, + iv: encryptedFilesarray[i].iv, + }, + hashes: { + sha256: encryptedFilesarray[i].hash, + }, + }; + + if (/^image\/.+/.test(filesToUpload[i].type)) { + const dimensions = imgDimensions[i]; + attachments.push({ + ...attachment, + image_url: fileUrl[i], + image_type: filesToUpload[i].type, + image_size: filesToUpload[i].size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl[i], + audio_type: filesToUpload[i].type, + audio_size: filesToUpload[i].size, + }); + } else if (/^video\/.+/.test(filesToUpload[i].type)) { + attachments.push({ + ...attachment, + video_url: fileUrl[i], + video_type: filesToUpload[i].type, + video_size: filesToUpload[i].size, + }); + } else { + attachments.push({ + ...attachment, + size: filesToUpload[i].size, + format: getFileExtension(filesToUpload[i].name), + }); + } + + const files = { + _id: _id[i], + name: filesToUpload[i].name, + type: filesToUpload[i].type, + size: filesToUpload[i].size, + }; + arrayoffiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayoffiles, + file: filesToUpload[0], + }); + }; + + const fileContentData = { + type: filesToUpload[0].type, + typeGroup: filesToUpload[0].type.split('/')[0], + name: filesToUpload[0].name, + msg: msg || '', + encryption: { + key: encryptedFilesarray[0].key, + iv: encryptedFilesarray[0].iv, + }, + hashes: { + sha256: encryptedFilesarray[0].hash, + }, + }; + + const fileContent = await e2eRoom.encryptMessageContent(fileContentData); + + const uploadFileData = { + raw: {}, + encrypted: fileContent, + }; + uploadFile( + filesarray, + chat, + { + t: 'e2e', + }, + getContent, + uploadFileData, + setFilesToUpload, + ); + } +}; +export const handleSendFiles = async (filesToUpload: File[], chat: any, room: any, setFilesToUpload?: (files: File[]) => void) => { + if (!chat || !room) { + return; + } + + const msg = chat.composer?.text ?? ''; + + filesToUpload.forEach((file) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(filesToUpload, chat, { msg }); + setFilesToUpload?.([]); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(filesToUpload, chat, { msg }); + setFilesToUpload?.([]); + return; + } + handleEncryptedFilesShared(filesToUpload, chat, msg, e2eRoom, setFilesToUpload); + chat.composer?.clear(); +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 63ad32d372144..f1a075710effb 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IMessage, ISubscription, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -21,12 +21,10 @@ import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; -import { e2e } from '../../../../../app/e2e/client'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime'; -import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; @@ -45,6 +43,7 @@ import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; import FilePreview from './FilePreview/FilePreview'; +import { handleSendFiles } from './HandleFileUploads'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxReplies from './MessageBoxReplies'; @@ -186,181 +185,6 @@ const MessageBox = ({ }, 300); }; - const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ - height: img.height, - width: img.width, - }); - }; - img.src = dataURL; - }); - }; - const uploadFile = ( - file: File[] | File, - extraData?: Pick & { msg?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ) => { - if (!chat) { - console.error('Chat context not found'); - return; - } - chat.uploads.send( - file, - { - ...extraData, - }, - getContent, - fileContent, - ); - chat.composer?.clear(); - setFilesToUpload([]); - }; - - const handleEncryptedFilesShared = async (filesToUpload: File[], msg: string, e2eRoom: any) => { - const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); - const filesarray = encryptedFilesarray.map((file: any) => file?.file); - - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments: FileAttachmentProps[] = []; - const uploadFiles: any = []; - - const promises = _id.map(async (id, i) => { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - let updatedAttachment; - - if (/^image\/.+/.test(filesToUpload[i].type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(filesToUpload[i])); - updatedAttachment = { - ...attachment, - image_url: fileUrl[i], - image_type: filesToUpload[i].type, - image_size: filesToUpload[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }; - } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - updatedAttachment = { - ...attachment, - audio_url: fileUrl[i], - audio_type: filesToUpload[i].type, - audio_size: filesToUpload[i].size, - }; - } else if (/^video\/.+/.test(filesToUpload[i].type)) { - updatedAttachment = { - ...attachment, - video_url: fileUrl[i], - video_type: filesToUpload[i].type, - video_size: filesToUpload[i].size, - }; - } else { - updatedAttachment = { - ...attachment, - size: filesToUpload[i].size, - format: getFileExtension(filesToUpload[i].name), - }; - } - - attachments.push(updatedAttachment); - - const files = { - _id: id, - name: filesToUpload[i].name, - type: filesToUpload[i].type, - size: filesToUpload[i].size, - }; - - uploadFiles.push(files); - }); - await Promise.all(promises); - - return e2eRoom.encryptMessageContent({ - attachments, - files: uploadFiles, - file: filesToUpload[0], - }); - }; - - const fileContentData = { - type: filesToUpload[0].type, - typeGroup: filesToUpload[0].type.split('/')[0], - name: filesToUpload[0].name, - msg: msg || '', - encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, - }, - hashes: { - sha256: encryptedFilesarray[0].hash, - }, - }; - - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, - }; - uploadFile( - filesarray, - { - t: 'e2e', - }, - getContent, - uploadFileData, - ); - } - }; - const handleSendFiles = async (filesToUpload: File[]) => { - if (!chat || !room) { - return; - } - - const msg = chat.composer?.text ?? ''; - - filesToUpload.forEach((file) => { - Object.defineProperty(file, 'name', { - writable: true, - value: file.name, - }); - }); - - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(filesToUpload, { msg }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(filesToUpload, { msg }); - return; - } - handleEncryptedFilesShared(filesToUpload, msg, e2eRoom); - chat.composer?.clear(); - setFilesToUpload([]); - }; - const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; const sendOnEnter = sendOnEnterBehavior == null || sendOnEnterBehavior === 'normal' || (sendOnEnterBehavior === 'desktop' && !isMobile); @@ -404,7 +228,7 @@ const MessageBox = ({ const handleSendMessage = useMutableCallback(async () => { if (isUploading) { setIsUploading(!isUploading); - return handleSendFiles(filesToUpload); + return handleSendFiles(filesToUpload, chat, room, setFilesToUpload); } const text = chat.composer?.text ?? ''; chat.composer?.clear(); From 12abfc544046638487ee1e21b73329379d57c647 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Mon, 2 Sep 2024 09:58:59 +0530 Subject: [PATCH 102/215] removed unnecessary changes --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 1 - apps/meteor/client/lib/chats/uploads.ts | 2 +- .../meteor/client/views/room/composer/messageBox/MessageBox.tsx | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index b963bc8550bef..0fa55d4dbcc14 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -102,7 +102,7 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; send( file: File[] | File, - { msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 630b2a39b27bd..4b078c580bc39 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -174,7 +174,6 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi type: file.type, typeGroup: file.type.split('/')[0], name: fileName, - description, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index bf444690a1445..f95dd8f9640c4 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -147,7 +147,7 @@ const send = async ( }; await sdk.call('sendMessage', text, fileUrls, fileIds); updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error) { + } catch (error: unknown) { updateUploads((uploads) => uploads.map((upload) => { if (upload.id !== id) { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f1a075710effb..359cc91ae9da9 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -225,7 +225,7 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); - const handleSendMessage = useMutableCallback(async () => { + const handleSendMessage = useMutableCallback(() => { if (isUploading) { setIsUploading(!isUploading); return handleSendFiles(filesToUpload, chat, room, setFilesToUpload); From 8c91c8cdcc32c30769dea3b22ffe39069153fc5b Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 3 Sep 2024 16:04:36 +0530 Subject: [PATCH 103/215] added file which upload the file and return back the file IDs and fileURLs --- .../lib/chats/uploadFilesAndGetIdsandURL.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts diff --git a/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts b/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts new file mode 100644 index 0000000000000..eadf9072b058f --- /dev/null +++ b/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts @@ -0,0 +1,143 @@ +import type { IUpload } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; +import { Random } from '@rocket.chat/random'; + +import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { getErrorMessage } from '../errorHandling'; +import type { Upload } from './Upload'; + +let uploads: readonly Upload[] = []; + +const emitter = new Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }>(); + +const updateUploads = (update: (uploads: readonly Upload[]) => readonly Upload[]): void => { + uploads = update(uploads); + emitter.emit('update'); +}; +export const uploadAndGetIds = async ( + file: File, + { + rid, + tmid, + fileContent, + }: { + rid: string; + tmid?: string; + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }; + }, +): Promise<{ fileId: string; fileUrl: string }> => { + const id = Random.id(); + + updateUploads((uploads) => [ + ...uploads, + { + id, + name: fileContent?.raw.name || file.name, + percentage: 0, + }, + ]); + + try { + const result = await new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { + const xhr = sdk.rest.upload( + `/v1/rooms.media/${rid}`, + { + file, + ...(fileContent && { + content: JSON.stringify(fileContent.encrypted), + }), + }, + { + load: (event) => { + console.log('from uploadfiles event', event); + }, + progress: (event) => { + if (!event.lengthComputable) { + return; + } + const progress = (event.loaded / event.total) * 100; + if (progress === 100) { + return; + } + + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: Math.round(progress) || 0, + }; + }), + ); + }, + error: (event) => { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(xhr.responseText), + }; + }), + ); + reject(event); + }, + }, + ); + + xhr.onload = () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + + resolve({ + fileId: response.file._id, + fileUrl: response.file.url, + }); + } else { + reject(new Error('File upload failed.')); + } + }; + + if (uploads.length) { + UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + + emitter.once(`cancelling-${id}`, () => { + xhr.abort(); + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + reject(new Error('Upload cancelled.')); + }); + }); + + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + + return result; + } catch (error: unknown) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + throw error; + } finally { + if (!uploads.length) { + UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + } +}; From 7e5081b3ad6bf58b15b50f968870b1d1abb85b50 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 6 Sep 2024 00:59:01 +0530 Subject: [PATCH 104/215] fix: added condition so it will remove extra space --- .../room/composer/messageBox/MessageBox.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 359cc91ae9da9..9eb53f03a354f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -486,25 +486,29 @@ const MessageBox = ({ aria-activedescendant={ariaActiveDescendant} />
- - {filesToUpload.map((file, index) => ( -
+ - -
- ))} -
+ {filesToUpload.map((file, index) => ( +
+ +
+ ))} + + + )} Date: Sun, 8 Sep 2024 17:06:40 +0530 Subject: [PATCH 105/215] feat: Add drag-and-drop file upload to message box --- .../views/room/body/DropTargetOverlay.tsx | 7 +++---- apps/meteor/client/views/room/body/RoomBody.tsx | 7 +++++-- .../client/views/room/body/RoomBodyV2.tsx | 7 +++++-- .../views/room/composer/ComposerMessage.tsx | 3 ++- .../composer/messageBox/HandleFileUploads.ts | 17 ++++++++--------- .../room/composer/messageBox/MessageBox.tsx | 16 ++++++++++++---- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index 8a9b1ae77dbd6..67b8ab436b910 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -9,13 +9,13 @@ import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; type DropTargetOverlayProps = { enabled: boolean; + setFilesToUplaod: any; reason?: ReactNode; - onFileDrop?: (files: File[]) => void; visible?: boolean; onDismiss?: () => void; }; -function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { +function DropTargetOverlay({ enabled, setFilesToUplaod, reason, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { const t = useTranslation(); const handleDragLeave = useMutableCallback((event: DragEvent) => { @@ -55,8 +55,7 @@ function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDism } } } - - onFileDrop?.(files); + setFilesToUplaod(files); }); if (!visible) { diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index a592bb1fa2c0d..0a418b6e0ffec 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement, UIEvent } from 'react'; -import React, { memo, useCallback, useMemo, useRef } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { RoomRoles } from '../../../../app/models/client'; import { isTruthy } from '../../../../lib/isTruthy'; @@ -89,6 +89,7 @@ const RoomBody = (): ReactElement => { const useRealName = useSetting('UI_Use_Real_Name') as boolean; const innerBoxRef = useRef(null); + const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -225,7 +226,7 @@ const RoomBody = (): ReactElement => { >
- + {roomLeader ? ( { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index cfd6cb94cb518..e6a274179d83a 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement } from 'react'; -import React, { memo, useCallback, useMemo, useRef } from 'react'; +import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; @@ -84,6 +84,7 @@ const RoomBody = (): ReactElement => { }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); const innerBoxRef = useRef(null); + const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -207,7 +208,7 @@ const RoomBody = (): ReactElement => { >
- +
{uploads.map((upload) => ( @@ -285,6 +286,8 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 790b1739bde78..e1666a7967027 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -11,6 +11,8 @@ import ComposerSkeleton from './ComposerSkeleton'; import MessageBox from './messageBox/MessageBox'; export type ComposerMessageProps = { + filesToUpload: File[]; + setFilesToUpload: any; tmid?: IMessage['_id']; children?: ReactNode; subscription?: ISubscription; @@ -84,7 +86,6 @@ const ComposerMessage = ({ tmid, readOnly, onSend, ...props }: ComposerMessagePr const publicationReady = useReactiveValue( useCallback(() => LegacyRoomManager.getOpenedRoomByRid(room._id)?.streamActive ?? false, [room._id]), ); - if (!publicationReady) { return ; } diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts index ef7bfc7f8c93d..e6d3dc92060cc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -2,6 +2,7 @@ import type { IMessage, IUpload, IE2EEMessage, FileAttachmentProps } from '@rock import { e2e } from '../../../../../app/e2e/client'; import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; +import { prependReplies } from '../../../../lib/utils/prependReplies'; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { return new Promise((resolve) => { @@ -68,6 +69,7 @@ const handleEncryptedFilesShared = async ( title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], + description: i === 0 ? msg : undefined, title_link_download: true, encryption: { key: encryptedFilesarray[i].key, @@ -131,7 +133,6 @@ const handleEncryptedFilesShared = async ( type: filesToUpload[0].type, typeGroup: filesToUpload[0].type.split('/')[0], name: filesToUpload[0].name, - msg: msg || '', encryption: { key: encryptedFilesarray[0].key, iv: encryptedFilesarray[0].iv, @@ -141,11 +142,9 @@ const handleEncryptedFilesShared = async ( }, }; - const fileContent = await e2eRoom.encryptMessageContent(fileContentData); - - const uploadFileData = { - raw: {}, - encrypted: fileContent, + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), }; uploadFile( filesarray, @@ -154,7 +153,7 @@ const handleEncryptedFilesShared = async ( t: 'e2e', }, getContent, - uploadFileData, + fileContent, setFilesToUpload, ); } @@ -163,8 +162,8 @@ export const handleSendFiles = async (filesToUpload: File[], chat: any, room: an if (!chat || !room) { return; } - - const msg = chat.composer?.text ?? ''; + const replies = chat.composer?.quotedMessages.get() ?? []; + const msg = await prependReplies(chat.composer?.text || '', replies); filesToUpload.forEach((file) => { Object.defineProperty(file, 'name', { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 9eb53f03a354f..7db29a1b43296 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -17,7 +17,7 @@ import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessa import { useMutation } from '@tanstack/react-query'; import fileSize from 'filesize'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; -import React, { memo, useRef, useReducer, useCallback, useState } from 'react'; +import React, { memo, useRef, useReducer, useCallback, useState, useEffect } from 'react'; import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; @@ -82,6 +82,8 @@ const a: any[] = []; const getEmptyArray = () => a; type MessageBoxProps = { + filesToUpload: File[]; + setFilesToUpload: any; tmid?: IMessage['_id']; readOnly: boolean; onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise; @@ -102,6 +104,8 @@ type MessageBoxProps = { type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; const MessageBox = ({ + filesToUpload, + setFilesToUpload, tmid, onSend, onJoin, @@ -124,12 +128,16 @@ const MessageBox = ({ const [typing, setTyping] = useReducer(reducer, false); const [isUploading, setIsUploading] = useState(false); - const [filesToUpload, setFilesToUpload] = useState([]); + + useEffect(() => { + setIsUploading(filesToUpload.length > 0); + }, [filesToUpload]); + const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - const handleFilesToUpload: HandleFilesToUpload = (filesList, resetFileInput) => { - setFilesToUpload((prevFiles) => { + const handleFilesToUpload: HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => { + setFilesToUpload((prevFiles: File[]) => { let newFilesToUpload = [...prevFiles, ...filesList]; if (newFilesToUpload.length > 6) { newFilesToUpload = newFilesToUpload.slice(0, 6); From d21dd7f5c2b5a9cfcf1ee18fa03381de4077aab2 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 11 Sep 2024 11:01:24 +0530 Subject: [PATCH 106/215] fix: added encryption of message also --- .../client/views/room/composer/messageBox/HandleFileUploads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts index e6d3dc92060cc..5cff06239d33e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts +++ b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts @@ -69,7 +69,6 @@ const handleEncryptedFilesShared = async ( title: filesToUpload[i].name, type: 'file', title_link: fileUrl[i], - description: i === 0 ? msg : undefined, title_link_download: true, encryption: { key: encryptedFilesarray[i].key, @@ -126,6 +125,7 @@ const handleEncryptedFilesShared = async ( attachments, files: arrayoffiles, file: filesToUpload[0], + msg, }); }; From 8508954a66953b51c978f250ff2e2c625f5d5302 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 20 Sep 2024 11:59:48 +0530 Subject: [PATCH 107/215] fix: Issue inside the thread messages --- .../room/contextualBar/Threads/components/ThreadChat.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 7afe0b442bbc8..14b024014afa7 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -24,6 +24,7 @@ type ThreadChatProps = { const ThreadChat = ({ mainMessage }: ThreadChatProps) => { const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(); + const [filesToUpload, setFilesToUpload] = useState([]); const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel'); @@ -95,7 +96,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { return ( - + { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + setFilesToUpload={setFilesToUpload} + filesToUpload={filesToUpload} tshow={sendToChannel} > From ea689a23738e77eb199ee3499611d3ecfab08805 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Fri, 20 Sep 2024 12:01:49 +0530 Subject: [PATCH 108/215] fix: multiple dispatch messages --- .../room/composer/messageBox/MessageBox.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 7db29a1b43296..b8207a8670181 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -146,29 +146,39 @@ const MessageBox = ({ message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", }); } + let nameError = 0; + let sizeError = 0; const validFiles = newFilesToUpload.filter((queuedFile) => { const { name, size } = queuedFile; if (!name) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); + nameError = 1; return false; } if (maxFileSize > -1 && (size || 0) > maxFileSize) { - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - }); + sizeError = 1; return false; } return true; }); + if (nameError) { + dispatchToastMessage({ + type: 'error', + message: t('error-the-field-is-required', { field: t('Name') }), + }); + } + + if (sizeError) { + dispatchToastMessage({ + type: 'error', + message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, + }); + } + setIsUploading(validFiles.length > 0); return validFiles; }); From acdf3c1e4428c218251202db6ccf6f4774415900 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 22 Sep 2024 00:28:44 +0530 Subject: [PATCH 109/215] removed onFileDrop as using setFilesToUpload --- apps/meteor/client/views/room/body/DropTargetOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index be60a39db86f5..bcb57c407c804 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -19,7 +19,7 @@ function DropTargetOverlay({ enabled, setFilesToUplaod, reason, - onFileDrop, + // onFileDrop, // not using onFileDrop anymore as we use setFilesToUplaod visible = true, onDismiss, }: DropTargetOverlayProps): ReactElement | null { From f1ea0da8ac39f1b093d43cf5a82a5186937940e4 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 2 Oct 2024 23:58:02 +0530 Subject: [PATCH 110/215] testing test --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index ad98df1aaa534..0b951eb8bdc01 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -434,9 +434,10 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.sendMessage('any_description'); + // await poHomeChannel.content.descriptionInput.fill('any_description'); + // await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); + // await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); From 8ce9034f4da53390353471e35b95c70f86537bfa Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 3 Oct 2024 00:22:39 +0530 Subject: [PATCH 111/215] return to old test --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 0b951eb8bdc01..ad98df1aaa534 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -434,10 +434,9 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.sendMessage('any_description'); - // await poHomeChannel.content.descriptionInput.fill('any_description'); - // await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - // await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.descriptionInput.fill('any_description'); + await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); + await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); From 5f223159121ebcb380868bfbd1677443069cf409 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 6 Oct 2024 01:05:59 +0530 Subject: [PATCH 112/215] fix: remove selected files when in edit mode --- .../views/room/composer/messageBox/MessageBox.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index b8207a8670181..e007ff240d007 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -129,10 +129,6 @@ const MessageBox = ({ const [typing, setTyping] = useReducer(reducer, false); const [isUploading, setIsUploading] = useState(false); - useEffect(() => { - setIsUploading(filesToUpload.length > 0); - }, [filesToUpload]); - const dispatchToastMessage = useToastMessageDispatch(); const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; @@ -345,6 +341,13 @@ const MessageBox = ({ subscribe: chat.composer?.editing.subscribe ?? emptySubscribe, }); + useEffect(() => { + setIsUploading(filesToUpload.length > 0); + if (isEditing) { + setFilesToUpload([]); + } + }, [filesToUpload, isEditing, setFilesToUpload]); + const isRecordingAudio = useSubscription({ getCurrentValue: chat.composer?.recording.get ?? getEmptyFalse, subscribe: chat.composer?.recording.subscribe ?? emptySubscribe, From 19df253cbd0ac6e6b2d03eeb3c3cb0851c6e2af3 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 6 Oct 2024 21:05:58 +0530 Subject: [PATCH 113/215] fix: diabled file sharing while editing --- .../client/views/room/composer/messageBox/MessageBox.tsx | 1 + .../MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index e007ff240d007..c356d5d5e0bc5 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -555,6 +555,7 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} + isEditing={isEditing} handleFiles={handleFilesToUpload} /> diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 102be2a179454..253539930d5ae 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -28,6 +28,7 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; + isEditing: boolean; handleFiles: (filesList: File[], resetFileInput?: () => void) => void; }; @@ -46,6 +47,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + isEditing = false, handleFiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); @@ -59,7 +61,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || isRecording, handleFiles); + const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, handleFiles); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); From ec6a12feea64c49894d086fe4fce2aff322fb655 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Wed, 9 Oct 2024 17:53:54 +0530 Subject: [PATCH 114/215] fix:TS_error_resolved --- apps/meteor/client/views/room/body/DropTargetOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index ec16d8b615178..21b2c0af6609e 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -23,7 +23,7 @@ function DropTargetOverlay({ visible = true, onDismiss, }: DropTargetOverlayProps): ReactElement | null { - const { t } = useTranslation(); + const t = useTranslation(); const handleDragLeave = useMutableCallback((event: DragEvent) => { event.stopPropagation(); From 056bfed0f2a502403813c86bd2389c7b3516af27 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Tue, 15 Oct 2024 12:06:03 +0530 Subject: [PATCH 115/215] fix: Trans error --- apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index a4134dad7cc4b..cc80d31a53edc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -17,7 +17,6 @@ import { useMutation } from '@tanstack/react-query'; import fileSize from 'filesize'; import type { ReactElement, MouseEventHandler, FormEvent, ClipboardEventHandler, MouseEvent } from 'react'; import React, { memo, useRef, useReducer, useCallback, useState, useEffect } from 'react'; -import { Trans } from 'react-i18next'; import { useSubscription } from 'use-subscription'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; From 9fba829e50c14a61b7a4d1c299091b1b463741f5 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Sun, 27 Oct 2024 01:48:06 +0530 Subject: [PATCH 116/215] Restore deleted subscriptions.ts file --- apps/meteor/tests/mocks/data/subscriptions.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 apps/meteor/tests/mocks/data/subscriptions.ts diff --git a/apps/meteor/tests/mocks/data/subscriptions.ts b/apps/meteor/tests/mocks/data/subscriptions.ts new file mode 100644 index 0000000000000..535384773cccb --- /dev/null +++ b/apps/meteor/tests/mocks/data/subscriptions.ts @@ -0,0 +1,40 @@ +import { faker } from '@faker-js/faker'; + +export function mockSubscription() { + const data: Record = { + _id: faker.string.uuid(), + rid: faker.string.uuid(), + name: faker.person.firstName(), + t: 'd', + alert: true, + E2EKey: faker.datatype.boolean() ? faker.string.uuid() : undefined, + E2ESuggestedKey: faker.datatype.boolean() ? faker.string.uuid() : undefined, + oldRoomKeys: faker.datatype.boolean() ? generateOldKeys() : undefined, + u: { + _id: faker.person.firstName(), + }, + }; + + return data; +} + +function generateOldKeys() { + const amount = faker.number.int({ min: 1, max: 10 }); + const oldRoomKeys = []; + for (let i = 0; i < amount; i++) { + oldRoomKeys.push({ + E2EKey: faker.string.uuid(), + ts: new Date(), + e2eKeyId: faker.string.uuid().slice(12), + }); + } + return oldRoomKeys; +} + +export function generateMultipleSubs(amount: number) { + const subs = []; + for (let i = 0; i < amount; i++) { + subs.push(mockSubscription()); + } + return subs; +} From 08416f7846c67547cd9259aed36fa8952d950488 Mon Sep 17 00:00:00 2001 From: abhi patel Date: Thu, 28 Nov 2024 22:40:25 +0530 Subject: [PATCH 117/215] fix: merge conflict --- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 ++ .../views/room/composer/messageBox/FilePreview/FilePreview.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 98f675bf48e79..1df781c69884e 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -145,6 +145,8 @@ Meteor.methods({ federation: Match.Maybe(Object), groupable: Match.Maybe(Boolean), sentByEmail: Match.Maybe(Boolean), + u: Match.Maybe(Object), + _updatedAt: Match.Maybe(Date), }); const uid = Meteor.userId(); diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx index 959f30aaccc4e..07bfd6f15ff95 100644 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx @@ -1,9 +1,9 @@ import type { ReactElement } from 'react'; import React from 'react'; -import { isIE11 } from '../../../../../lib/utils/isIE11'; import GenericPreview from './GenericPreview'; import MediaPreview from './MediaPreview'; +import { isIE11 } from '../../../../../lib/utils/isIE11'; export enum FilePreviewType { IMAGE = 'image', From 8b176c76cd022934ccbd3fdc418bea540f5390e8 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 21 Jan 2025 11:42:03 -0300 Subject: [PATCH 118/215] feat: replace file preview --- .../messageBox/FilePreview/FilePreview.tsx | 63 --------- .../messageBox/FilePreview/GenericPreview.tsx | 77 ---------- .../messageBox/FilePreview/ImagePreview.tsx | 62 -------- .../messageBox/FilePreview/MediaPreview.tsx | 78 ---------- .../FilePreview/PreviewSkeleton.tsx | 7 - .../room/composer/messageBox/MessageBox.tsx | 45 +----- .../messageBox/MessageComposerFile.tsx | 133 ++++++++++++++++++ .../messageBox/MessageComposerFileArea.tsx | 22 +++ 8 files changed, 160 insertions(+), 327 deletions(-) delete mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx delete mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx delete mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx delete mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx delete mode 100644 apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx deleted file mode 100644 index 07bfd6f15ff95..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/FilePreview.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; - -import GenericPreview from './GenericPreview'; -import MediaPreview from './MediaPreview'; -import { isIE11 } from '../../../../../lib/utils/isIE11'; - -export enum FilePreviewType { - IMAGE = 'image', - AUDIO = 'audio', - // VIDEO = 'video', // currently showing it in simple generic view -} - -const getFileType = (fileType: File['type']): FilePreviewType | undefined => { - if (!fileType) { - return; - } - for (const type of Object.values(FilePreviewType)) { - if (fileType.indexOf(type) > -1) { - return type; - } - } -}; - -const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefined): boolean => { - if (!fileType) { - return false; - } - if (isIE11) { - return false; - } - // Avoid preview if file size bigger than 10mb - if (file.size > 10000000) { - return false; - } - if (!Object.values(FilePreviewType).includes(fileType)) { - return false; - } - return true; -}; - -type FilePreviewProps = { - file: File; - key: number; - index: number; - onRemove: (index: number) => void; -}; - -const FilePreview = ({ file, index, onRemove }: FilePreviewProps): ReactElement => { - const fileType = getFileType(file.type); - - const handleRemove = () => { - onRemove(index); - }; - - if (shouldShowMediaPreview(file, fileType)) { - return ; - } - - return ; -}; - -export default FilePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx deleted file mode 100644 index e8a4112a8bf20..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/GenericPreview.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React, { useState } from 'react'; - -import { formatBytes } from '../../../../../lib/utils/formatBytes'; - -type GenericPreviewProps = { - file: File; - index: number; - onRemove: (index: number) => void; -}; - -const GenericPreview = ({ file, index, onRemove }: GenericPreviewProps): ReactElement => { - const [isHovered, setIsHovered] = useState(false); - - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - const buttonStyle: React.CSSProperties = { - position: 'absolute' as const, - right: '-10px', - top: '-8px', - backgroundColor: '#6d6c6c', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - return ( - - {/* currently using div */} -
- {file.name.split('.')[1] === 'mp4' || file.name.split('.')[1] === 'webm' ? ( - - ) : ( - - )} -
- - {`${file.name.split('.')[0]} - ${formatBytes(file.size, 2)}`} - {`${file.name.split('.')[1]}`} - - {/*
*/} - onRemove(index)} /> - - ); -}; -export default GenericPreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx deleted file mode 100644 index 9c4e54c7df6f2..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/ImagePreview.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React, { useState } from 'react'; - -import GenericPreview from './GenericPreview'; -import PreviewSkeleton from './PreviewSkeleton'; - -type ImagePreviewProps = { - url: string; - file: File; - onRemove: (index: number) => void; - index: number; -}; - -const ImagePreview = ({ url, file, onRemove, index }: ImagePreviewProps): ReactElement => { - const [error, setError] = useState(false); - const [loading, setLoading] = useState(true); - const [isHovered, setIsHovered] = useState(false); - - const handleLoad = (): void => setLoading(false); - const handleError = (): void => { - setLoading(false); - setError(true); - }; - - const handleMouseEnter = (): void => setIsHovered(true); - const handleMouseLeave = (): void => setIsHovered(false); - - const buttonStyle: React.CSSProperties = { - position: 'absolute' as const, - right: '-10px', - top: '-8px', - backgroundColor: '#6d6c6c', - display: isHovered ? 'block' : 'none', - cursor: 'pointer', - zIndex: 1, - color: 'white', - borderRadius: '100%', - }; - - if (error) { - return ; - } - - return ( - - {loading && } - - onRemove(index)} /> - - ); -}; - -export default ImagePreview; diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx deleted file mode 100644 index a5eed5a03db30..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/MediaPreview.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { AudioPlayer, Box, Icon } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React, { useEffect, useState, memo } from 'react'; - -// import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; // as currently showing video in generic view -import { FilePreviewType } from './FilePreview'; -import ImagePreview from './ImagePreview'; -import PreviewSkeleton from './PreviewSkeleton'; - -type ReaderOnloadCallback = (url: FileReader['result']) => void; - -const readFileAsDataURL = (file: File, callback: ReaderOnloadCallback): void => { - const reader = new FileReader(); - reader.onload = (e): void => callback(e?.target?.result || null); - - return reader.readAsDataURL(file); -}; - -const useFileAsDataURL = (file: File): [loaded: boolean, url: null | FileReader['result']] => { - const [loaded, setLoaded] = useState(false); - const [url, setUrl] = useState(null); - - useEffect(() => { - setLoaded(false); - readFileAsDataURL(file, (url) => { - setUrl(url); - setLoaded(true); - }); - }, [file]); - return [loaded, url]; -}; - -type MediaPreviewProps = { - file: File; - fileType: FilePreviewType; - onRemove: (index: number) => void; - index: number; -}; - -const MediaPreview = ({ file, fileType, onRemove, index }: MediaPreviewProps): ReactElement => { - const [loaded, url] = useFileAsDataURL(file); - const t = useTranslation(); - - if (!loaded) { - return ; - } - - if (typeof url !== 'string') { - return ( - - - {t('FileUpload_Cannot_preview_file')} - - ); - } - - if (fileType === FilePreviewType.IMAGE) { - return ; - } - - // if (fileType === FilePreviewType.VIDEO) { - // return ( - // - // - // {t('Browser_does_not_support_video_element')} - // - // ); - // } - - if (fileType === FilePreviewType.AUDIO) { - return ; - } - - throw new Error('Wrong props provided for MediaPreview'); -}; - -export default memo(MediaPreview); diff --git a/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx b/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx deleted file mode 100644 index 72017e26e9f55..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/FilePreview/PreviewSkeleton.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Skeleton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import React from 'react'; - -const PreviewSkeleton = (): ReactElement => ; - -export default PreviewSkeleton; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f067ce7d2797b..78cdbfd96ef5f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,6 +1,5 @@ /* eslint-disable complexity */ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { MessageComposerAction, @@ -18,10 +17,12 @@ import fileSize from 'filesize'; import type { ReactElement, FormEvent, MouseEvent, ClipboardEvent } from 'react'; import { memo, useRef, useReducer, useCallback, useState, useEffect, useSyncExternalStore } from 'react'; +import { handleSendFiles } from './HandleFileUploads'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxHint from './MessageBoxHint'; import MessageBoxReplies from './MessageBoxReplies'; +import MessageComposerFileArea from './MessageComposerFileArea'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; @@ -43,8 +44,6 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; -import FilePreview from './FilePreview/FilePreview'; -import { handleSendFiles } from './HandleFileUploads'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; @@ -179,20 +178,8 @@ const MessageBox = ({ }; const handleRemoveFile = (indexToRemove: number) => { const updatedFiles = [...filesToUpload]; - - const element = document.getElementById(`file-preview-${indexToRemove}`); - if (element) { - element.style.transition = 'opacity 0.3s ease-in-out'; - element.style.opacity = '0'; - } - - setTimeout(() => { - updatedFiles.splice(indexToRemove, 1); - setFilesToUpload(updatedFiles); - if (element) { - element.style.opacity = '1'; - } - }, 300); + updatedFiles.splice(indexToRemove, 1); + setFilesToUpload(updatedFiles); }; const { isMobile } = useLayout(); @@ -497,29 +484,7 @@ const MessageBox = ({ aria-activedescendant={ariaActiveDescendant} />
- {isUploading && ( - <> - - {filesToUpload.map((file, index) => ( -
- -
- ))} -
- - )} + {isUploading && } { +// if (!fileType) { +// return; +// } +// for (const type of Object.values(FilePreviewType)) { +// if (fileType.indexOf(type) > -1) { +// return type; +// } +// } +// }; + +// const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefined): boolean => { +// if (!fileType) { +// return false; +// } +// if (isIE11) { +// return false; +// } +// // Avoid preview if file size bigger than 10mb +// if (file.size > 10000000) { +// return false; +// } +// if (!Object.values(FilePreviewType).includes(fileType)) { +// return false; +// } +// return true; +// }; + +type MessageComposerFileProps = { + file: File; + key: number; + index: number; + onRemove: (index: number) => void; +}; + +const MessageComposerFile = ({ file, index, onRemove }: MessageComposerFileProps): ReactElement => { + // if (shouldShowMediaPreview(file, fileType)) { + // return ; + // } + + const [fileName, setFileName] = useState(file.name.split('.')[0]); + const setModal = useSetModal(); + + const previewWrapperStyle = css` + background-color: 'surface-tint'; + + &:hover { + cursor: pointer; + background-color: ${Palette.surface['surface-hover']}; + } + `; + + const closeWrapperStyle = css` + position: absolute; + right: 0.25rem; + top: 0.25rem; + `; + + const fileExtension = file.name.split('.')[1]; + const fileSize = formatBytes(file.size, 2); + + return ( + + setModal( + setFileName(name)} + fileName={fileName} + file={file} + onClose={() => setModal(null)} + invalidContentType={!fileUploadIsValidContentType(file?.type)} + />, + ) + } + > + + + {fileName} + + {`${fileSize} - ${fileExtension}`} + + { + // chat.composer?.dismissQuotedMessage(reply._id); + // }} + onClick={(e) => { + e.stopPropagation(); + onRemove(index); + }} + > + + + {/* onRemove(index)} /> */} + + ); + + // return ; +}; + +export default MessageComposerFile; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx new file mode 100644 index 0000000000000..3dd43b918d576 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx @@ -0,0 +1,22 @@ +import { Box } from '@rocket.chat/fuselage'; + +import MessageComposerFile from './MessageComposerFile'; + +type MessageComposerFileAreaProps = { + filesToUpload: File[]; + handleRemoveFile: (indexToRemove: number) => void; +}; + +const MessageComposerFileArea = ({ filesToUpload, handleRemoveFile }: MessageComposerFileAreaProps) => { + return ( + + {filesToUpload.map((file, index) => ( +
+ +
+ ))} +
+ ); +}; + +export default MessageComposerFileArea; From 007a5cc4f51b797b724099f12c4e659aeeefdc14 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 11:23:22 -0300 Subject: [PATCH 119/215] fix: remove excessive state --- .../views/room/body/DropTargetOverlay.tsx | 13 +-- .../client/views/room/body/RoomBody.tsx | 10 +- .../views/room/composer/ComposerMessage.tsx | 5 +- .../room/composer/messageBox/MessageBox.tsx | 103 ++++-------------- .../MessageBoxActionsToolbar.tsx | 4 +- .../hooks/useFileUploadAction.ts | 11 +- 6 files changed, 37 insertions(+), 109 deletions(-) diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index 36bfe313b3cda..d429b46f4f45c 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -9,20 +9,13 @@ import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; type DropTargetOverlayProps = { enabled: boolean; - setFilesToUplaod: any; reason?: ReactNode; + onFileDrop?: (files: File[]) => void; visible?: boolean; onDismiss?: () => void; }; -function DropTargetOverlay({ - enabled, - setFilesToUplaod, - reason, - // onFileDrop, // not using onFileDrop anymore as we use setFilesToUplaod - visible = true, - onDismiss, -}: DropTargetOverlayProps): ReactElement | null { +function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { const { t } = useTranslation(); const handleDragLeave = useEffectEvent((event: DragEvent) => { @@ -62,7 +55,7 @@ function DropTargetOverlay({ } } } - setFilesToUplaod(files); + onFileDrop?.(files); }); if (!visible) { diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 4ef480e64a9e5..7f14751807c15 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement, UIEvent } from 'react'; -import { memo, useCallback, useMemo, useRef, useState } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; @@ -90,7 +90,6 @@ const RoomBody = (): ReactElement => { const useRealName = useSetting('UI_Use_Real_Name', false); const innerBoxRef = useRef(null); - const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -110,6 +109,7 @@ const RoomBody = (): ReactElement => { const { uploads, + isUploading, handleUploadFiles, handleUploadProgressClose, targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], @@ -228,7 +228,7 @@ const RoomBody = (): ReactElement => { >
- + {roomLeader ? ( { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - setFilesToUpload={setFilesToUpload} - filesToUpload={filesToUpload} + uploads={uploads} + isUploading={isUploading} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 8c0d754d2d99b..e16f57c2c5b08 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -9,10 +9,11 @@ import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { useChat } from '../contexts/ChatContext'; import { useRoom } from '../contexts/RoomContext'; import MessageBox from './messageBox/MessageBox'; +import type { Upload } from '../../../lib/chats/Upload'; export type ComposerMessageProps = { - filesToUpload: File[]; - setFilesToUpload: any; + uploads: readonly Upload[]; + isUploading: boolean; tmid?: IMessage['_id']; children?: ReactNode; subscription?: ISubscription; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index b5cea415ca744..69cfe6f88176e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -11,18 +11,19 @@ import { MessageComposerToolbarSubmit, MessageComposerButton, } from '@rocket.chat/ui-composer'; -import { useTranslation, useUserPreference, useLayout, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; -import fileSize from 'filesize'; import type { ReactElement, FormEvent, MouseEvent, ClipboardEvent } from 'react'; -import { memo, useRef, useReducer, useCallback, useState, useEffect, useSyncExternalStore } from 'react'; +import { memo, useRef, useReducer, useCallback, useSyncExternalStore } from 'react'; -import { handleSendFiles } from './HandleFileUploads'; +// import { handleSendFiles } from './HandleFileUploads'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxHint from './MessageBoxHint'; import MessageBoxReplies from './MessageBoxReplies'; import MessageComposerFileArea from './MessageComposerFileArea'; +import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; +import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; @@ -30,6 +31,7 @@ import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionF import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; +import type { Upload } from '../../../../lib/chats/Upload'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { keyCodes } from '../../../../lib/utils/keyCodes'; import AudioMessageRecorder from '../../../composer/AudioMessageRecorder'; @@ -44,8 +46,6 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup'; import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; -import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; -import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; @@ -79,8 +79,6 @@ const a: any[] = []; const getEmptyArray = () => a; type MessageBoxProps = { - filesToUpload: File[]; - setFilesToUpload: any; tmid?: IMessage['_id']; onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise; onJoin?: () => Promise; @@ -89,19 +87,17 @@ type MessageBoxProps = { onEscape?: () => void; onNavigateToPreviousMessage?: () => void; onNavigateToNextMessage?: () => void; - onUploadFiles?: (files: readonly File[]) => void; + onUploadFiles: (files: readonly File[]) => void; tshow?: IMessage['tshow']; previewUrls?: string[]; subscription?: ISubscription; showFormattingTips: boolean; isEmbedded?: boolean; + uploads: readonly Upload[]; + isUploading: boolean; }; -type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; - const MessageBox = ({ - filesToUpload, - setFilesToUpload, tmid, onSend, onJoin, @@ -112,6 +108,8 @@ const MessageBox = ({ onTyping, tshow, previewUrls, + uploads, + isUploading, }: MessageBoxProps): ReactElement => { const chat = useChat(); const room = useRoom(); @@ -122,65 +120,6 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const [typing, setTyping] = useReducer(reducer, false); - const [isUploading, setIsUploading] = useState(false); - - const dispatchToastMessage = useToastMessageDispatch(); - const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - - const handleFilesToUpload: HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => { - setFilesToUpload((prevFiles: File[]) => { - let newFilesToUpload = [...prevFiles, ...filesList]; - if (newFilesToUpload.length > 6) { - newFilesToUpload = newFilesToUpload.slice(0, 6); - dispatchToastMessage({ - type: 'error', - message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", - }); - } - let nameError = 0; - let sizeError = 0; - - const validFiles = newFilesToUpload.filter((queuedFile) => { - const { name, size } = queuedFile; - - if (!name) { - nameError = 1; - return false; - } - - if (maxFileSize > -1 && (size || 0) > maxFileSize) { - sizeError = 1; - return false; - } - - return true; - }); - - if (nameError) { - dispatchToastMessage({ - type: 'error', - message: t('error-the-field-is-required', { field: t('Name') }), - }); - } - - if (sizeError) { - dispatchToastMessage({ - type: 'error', - message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, - }); - } - - setIsUploading(validFiles.length > 0); - return validFiles; - }); - - resetFileInput?.(); - }; - const handleRemoveFile = (indexToRemove: number) => { - const updatedFiles = [...filesToUpload]; - updatedFiles.splice(indexToRemove, 1); - setFilesToUpload(updatedFiles); - }; const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; @@ -226,10 +165,10 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); + // TODO: change to something like `hasUploads` const handleSendMessage = useEffectEvent(() => { if (isUploading) { - setIsUploading(!isUploading); - return handleSendFiles(filesToUpload, chat, room, setFilesToUpload); + return chat?.flows.confirmFiles(); } const text = chat.composer?.text ?? ''; chat.composer?.clear(); @@ -325,13 +264,6 @@ const MessageBox = ({ const isEditing = useSyncExternalStore(chat.composer?.editing.subscribe ?? emptySubscribe, chat.composer?.editing.get ?? getEmptyFalse); - useEffect(() => { - setIsUploading(filesToUpload.length > 0); - if (isEditing) { - setFilesToUpload([]); - } - }, [filesToUpload, isEditing, setFilesToUpload]); - const isRecordingAudio = useSyncExternalStore( chat.composer?.recording.subscribe ?? emptySubscribe, chat.composer?.recording.get ?? getEmptyFalse, @@ -477,7 +409,13 @@ const MessageBox = ({ aria-activedescendant={popup.focused ? `popup-item-${popup.focused._id}` : undefined} />
- {isUploading && } + {isUploading && ( + + )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 23e1bb3ff3682..b074a91feae4d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -29,7 +29,6 @@ type MessageBoxActionsToolbarProps = { rid: IRoom['_id']; tmid?: IMessage['_id']; isEditing: boolean; - handleFiles: (filesList: File[], resetFileInput?: () => void) => void; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -48,7 +47,6 @@ const MessageBoxActionsToolbar = ({ variant = 'large', isMicrophoneDenied, isEditing = false, - handleFiles, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -61,7 +59,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, handleFiles); + const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 6f1fe36cba52b..8367ed210de67 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -3,16 +3,15 @@ import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useFileInput } from '../../../../../../hooks/useFileInput'; +import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = ( - disabled: boolean, - handleFiles: (filesList: File[], resetFileInput?: () => void) => void, -): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled', true); const fileInputRef = useFileInput(fileInputProps); + const chat = useChat(); useEffect(() => { const resetFileInput = () => { @@ -31,12 +30,12 @@ export const useFileUploadAction = ( }); return file; }); - handleFiles(filesToUpload, resetFileInput); + chat?.flows.uploadFiles(filesToUpload, resetFileInput); }; fileInputRef.current?.addEventListener('change', handleUploadChange); return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); - }, [fileInputRef]); + }, [chat, fileInputRef]); const handleUpload = () => { fileInputRef?.current?.click(); From 94da5807a04b79cfe87172f33827610266193601 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 11:24:50 -0300 Subject: [PATCH 120/215] feat: `MessageComposerFile` --- .../messageBox/MessageComposerFile.tsx | 108 ++++++------------ .../messageBox/MessageComposerFileArea.tsx | 12 +- .../messageBox/MessageComposerFileLoader.tsx | 34 ++++++ 3 files changed, 76 insertions(+), 78 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageComposerFileLoader.tsx diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx index 05925726a08ba..c82bbafce494a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx @@ -1,61 +1,21 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, IconButton, Palette } from '@rocket.chat/fuselage'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import { useState } from 'react'; +import type { AllHTMLAttributes, ReactElement } from 'react'; -import { fileUploadIsValidContentType } from '../../../../../app/utils/client'; +import MessageComposerFileLoader from './MessageComposerFileLoader'; +import { getMimeType } from '../../../../../app/utils/lib/mimeTypes'; +import type { Upload } from '../../../../lib/chats/Upload'; import { formatBytes } from '../../../../lib/utils/formatBytes'; -// import { isIE11 } from '../../../../../lib/utils/isIE11'; import FileUploadModal from '../../modals/FileUploadModal'; -// export enum FilePreviewType { -// IMAGE = 'image', -// AUDIO = 'audio', -// VIDEO = 'video', -// } - -// const getFileType = (fileType: File['type']): FilePreviewType | undefined => { -// if (!fileType) { -// return; -// } -// for (const type of Object.values(FilePreviewType)) { -// if (fileType.indexOf(type) > -1) { -// return type; -// } -// } -// }; - -// const shouldShowMediaPreview = (file: File, fileType: FilePreviewType | undefined): boolean => { -// if (!fileType) { -// return false; -// } -// if (isIE11) { -// return false; -// } -// // Avoid preview if file size bigger than 10mb -// if (file.size > 10000000) { -// return false; -// } -// if (!Object.values(FilePreviewType).includes(fileType)) { -// return false; -// } -// return true; -// }; - type MessageComposerFileProps = { - file: File; - key: number; - index: number; - onRemove: (index: number) => void; -}; + upload: Upload; + onRemove: (id: string) => void; + onEdit: (id: Upload['id'], fileName: string) => void; +} & Omit, 'is'>; -const MessageComposerFile = ({ file, index, onRemove }: MessageComposerFileProps): ReactElement => { - // if (shouldShowMediaPreview(file, fileType)) { - // return ; - // } - - const [fileName, setFileName] = useState(file.name.split('.')[0]); +const MessageComposerFile = ({ upload, onRemove, onEdit, ...props }: MessageComposerFileProps): ReactElement => { const setModal = useSetModal(); const previewWrapperStyle = css` @@ -73,12 +33,28 @@ const MessageComposerFile = ({ file, index, onRemove }: MessageComposerFileProps top: 0.25rem; `; - const fileExtension = file.name.split('.')[1]; - const fileSize = formatBytes(file.size, 2); + const fileSize = formatBytes(upload.file.size, 2); + const fileExtension = getMimeType(upload.file.type, upload.file.name); + const isLoading = upload.percentage > 0 && upload.percentage !== 100; + + const handleOpenFilePreview = () => { + setModal( + { + onEdit(upload.id, name); + setModal(null); + }} + fileName={upload.file.name} + file={upload.file} + onClose={() => setModal(null)} + />, + ); + }; return ( - setModal( - setFileName(name)} - fileName={fileName} - file={file} - onClose={() => setModal(null)} - invalidContentType={!fileUploadIsValidContentType(file?.type)} - />, - ) - } + onClick={handleOpenFilePreview} + onKeyDown={(e) => ['Enter', 'Space'].includes(e.code) && handleOpenFilePreview()} + {...props} > - {fileName} + {upload.file.name} {`${fileSize} - ${fileExtension}`} { - // chat.composer?.dismissQuotedMessage(reply._id); - // }} onClick={(e) => { e.stopPropagation(); - onRemove(index); + onRemove(upload.id); }} > - + {isLoading && } + {!isLoading && } - {/* onRemove(index)} /> */} ); - - // return ; }; export default MessageComposerFile; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx index 3dd43b918d576..3d1b1aa9fdf9b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx @@ -1,18 +1,20 @@ import { Box } from '@rocket.chat/fuselage'; import MessageComposerFile from './MessageComposerFile'; +import type { Upload } from '../../../../lib/chats/Upload'; type MessageComposerFileAreaProps = { - filesToUpload: File[]; - handleRemoveFile: (indexToRemove: number) => void; + uploads?: readonly Upload[]; + handleRemoveUpload: (id: Upload['id']) => void; + handleEditFileName: (id: Upload['id'], fileName: string) => void; }; -const MessageComposerFileArea = ({ filesToUpload, handleRemoveFile }: MessageComposerFileAreaProps) => { +const MessageComposerFileArea = ({ uploads, handleRemoveUpload, handleEditFileName }: MessageComposerFileAreaProps) => { return ( - {filesToUpload.map((file, index) => ( + {uploads?.map((upload, index) => (
- +
))}
diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileLoader.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileLoader.tsx new file mode 100644 index 0000000000000..670802e472d59 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileLoader.tsx @@ -0,0 +1,34 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +// TODO: This component should be moved to fuselage +const MessageComposerFileLoader = (props: ComponentProps) => { + const customCSS = css` + animation: spin-animation 0.8s linear infinite; + + @keyframes spin-animation { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + `; + + return ( + + + + + ); +}; + +export default MessageComposerFileLoader; From c565f4e56ca6e29a7d83b2dc0f592133f51b8053 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 11:26:40 -0300 Subject: [PATCH 121/215] chore: upload API tweaks --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 7 + apps/meteor/client/lib/chats/ChatAPI.ts | 4 + apps/meteor/client/lib/chats/Upload.ts | 2 + .../client/lib/chats/flows/confirmFiles.ts | 22 + .../client/lib/chats/flows/uploadFiles.ts | 417 ++++++++++++------ apps/meteor/client/lib/chats/uploads.ts | 82 ++-- .../views/room/body/hooks/useFileUpload.ts | 58 +++ .../FileUploadModal/FileUploadModal.tsx | 98 ++-- 8 files changed, 447 insertions(+), 243 deletions(-) create mode 100644 apps/meteor/client/lib/chats/flows/confirmFiles.ts diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 3745864061f42..9298e8724b3f4 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -5,6 +5,7 @@ import type { IActionManager } from '@rocket.chat/ui-contexts'; import { UserAction } from './UserAction'; import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI'; import { createDataAPI } from '../../../../client/lib/chats/data'; +import { confirmFiles } from '../../../../client/lib/chats/flows/confirmFiles'; import { processMessageEditing } from '../../../../client/lib/chats/flows/processMessageEditing'; import { processSetReaction } from '../../../../client/lib/chats/flows/processSetReaction'; import { processSlashCommand } from '../../../../client/lib/chats/flows/processSlashCommand'; @@ -175,6 +176,7 @@ export class ChatMessages implements ChatAPI { this.flows = { uploadFiles: uploadFiles.bind(null, this), + confirmFiles: confirmFiles.bind(null, this), sendMessage: sendMessage.bind(this, this), processSlashCommand: processSlashCommand.bind(null, this), processTooLongMessage: processTooLongMessage.bind(null, this), @@ -242,5 +244,10 @@ export class ChatMessages implements ChatAPI { } this.composer?.clear(); } + + // reset uploads when release composer + if (this.uploads.get().length > 0) { + this.uploads.clear(); + } } } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 268f43f60aec5..2969954b337a9 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -99,7 +99,10 @@ export type UploadsAPI = { get(): readonly Upload[]; subscribe(callback: () => void): () => void; wipeFailedOnes(): void; + clear(): void; cancel(id: Upload['id']): void; + removeUpload(id: Upload['id']): void; + editUploadFileName: (id: Upload['id'], fileName: string) => void; send( file: File[] | File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, @@ -145,6 +148,7 @@ export type ChatAPI = { readonly flows: { readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; + readonly confirmFiles: () => Promise; readonly sendMessage: ({ text, tshow, diff --git a/apps/meteor/client/lib/chats/Upload.ts b/apps/meteor/client/lib/chats/Upload.ts index a2d6bf18cd3ce..edffc6b5f3bf7 100644 --- a/apps/meteor/client/lib/chats/Upload.ts +++ b/apps/meteor/client/lib/chats/Upload.ts @@ -1,6 +1,8 @@ export type Upload = { readonly id: string; readonly name: string; + readonly file: File; + readonly url: string; readonly percentage: number; readonly error?: Error; }; diff --git a/apps/meteor/client/lib/chats/flows/confirmFiles.ts b/apps/meteor/client/lib/chats/flows/confirmFiles.ts new file mode 100644 index 0000000000000..45d1a1bc229dc --- /dev/null +++ b/apps/meteor/client/lib/chats/flows/confirmFiles.ts @@ -0,0 +1,22 @@ +import { sdk } from '../../../../app/utils/client/lib/SDKClient'; +import type { ChatAPI } from '../ChatAPI'; + +export const confirmFiles = async (chat: ChatAPI): Promise => { + const replies = chat.composer?.quotedMessages.get() ?? []; + const msg = chat.composer?.text || ''; + + const fileUrls = chat.uploads.get().map((upload) => upload.url); + const fileIds = chat.uploads.get().map((upload) => upload.id); + + const message = await chat.data.composeMessage(msg, { + quotedMessages: replies, + }); + + try { + await sdk.call('sendMessage', message, fileUrls, fileIds); + chat.composer?.clear(); + chat?.uploads.clear(); + } catch (error: unknown) { + console.error(error); + } +}; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 0c7f5eeac321e..830329c1280a5 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -48,10 +48,139 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi fileContent, ); chat.composer?.clear(); - imperativeModal.close(); + // imperativeModal.close(); uploadNextFile(); }; + const handleSubmitFile = async (file: File, fileName: string, description?: string): Promise => { + Object.defineProperty(file, 'name', { + writable: true, + value: fileName, + }); + + // encrypt attachment description + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(file, { description }); + return; + } + + if (!settings.get('E2E_Enable_Encrypt_Files')) { + uploadFile(file, { description }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(file, { description }); + return; + } + + const encryptedFile = await e2eRoom.encryptFile(file); + + if (encryptedFile) { + const getContent = async (filesId: string[], filesUrl: string[]): Promise => { + const attachments = []; + const _id = filesId[0]; + const fileUrl = filesUrl[0]; + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + format: getFileExtension(file.name), + }); + } + + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ] as IMessage['files']; + + return e2eRoom.encryptMessageContent({ + attachments, + files, + file: files?.[0], + }); + }; + + const fileContentData = { + type: file.type, + typeGroup: file.type.split('/')[0], + name: fileName, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), + }; + + uploadFile( + encryptedFile.file, + { + t: 'e2e', + }, + getContent, + fileContent, + ); + } + }; + const uploadNextFile = (): void => { const file = queue.pop(); if (!file) { @@ -59,150 +188,152 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi return; } - imperativeModal.open({ - component: FileUploadModal, - props: { - file, - fileName: file.name, - fileDescription: chat.composer?.text ?? '', - showDescription: room && !isRoomFederated(room), - onClose: (): void => { - imperativeModal.close(); - uploadNextFile(); - }, - onSubmit: async (fileName: string, description?: string): Promise => { - Object.defineProperty(file, 'name', { - writable: true, - value: fileName, - }); + handleSubmitFile(file, file.name, chat.composer?.text ?? ''); - // encrypt attachment description - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(file, { description }); - return; - } - - if (!settings.get('E2E_Enable_Encrypt_Files')) { - uploadFile(file, { description }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(file, { description }); - return; - } - - const encryptedFile = await e2eRoom.encryptFile(file); - - if (encryptedFile) { - const getContent = async (filesId: string[], filesUrl: string[]): Promise => { - const attachments = []; - const _id = filesId[0]; - const fileUrl = filesUrl[0]; - - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description, - title_link: fileUrl, - title_link_download: true, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - hashes: { - sha256: encryptedFile.hash, - }, - }; - - if (/^image\/.+/.test(file.type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - - attachments.push({ - ...attachment, - image_url: fileUrl, - image_type: file.type, - image_size: file.size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl, - audio_type: file.type, - audio_size: file.size, - }); - } else if (/^video\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - video_url: fileUrl, - video_type: file.type, - video_size: file.size, - }); - } else { - attachments.push({ - ...attachment, - size: file.size, - format: getFileExtension(file.name), - }); - } - - const files = [ - { - _id, - name: file.name, - type: file.type, - size: file.size, - // "format": "png" - }, - ] as IMessage['files']; - - return e2eRoom.encryptMessageContent({ - attachments, - files, - file: files?.[0], - }); - }; - - const fileContentData = { - type: file.type, - typeGroup: file.type.split('/')[0], - name: fileName, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - hashes: { - sha256: encryptedFile.hash, - }, - }; - - const fileContent = { - raw: fileContentData, - encrypted: await e2eRoom.encryptMessageContent(fileContentData), - }; - - uploadFile( - encryptedFile.file, - { - t: 'e2e', - }, - getContent, - fileContent, - ); - } - }, - invalidContentType: !fileUploadIsValidContentType(file?.type), - }, - }); + // imperativeModal.open({ + // component: FileUploadModal, + // props: { + // file, + // fileName: file.name, + // fileDescription: chat.composer?.text ?? '', + // showDescription: room && !isRoomFederated(room), + // onClose: (): void => { + // imperativeModal.close(); + // uploadNextFile(); + // }, + // onSubmit: async (fileName: string, description?: string): Promise => { + // Object.defineProperty(file, 'name', { + // writable: true, + // value: fileName, + // }); + + // // encrypt attachment description + // const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + // if (!e2eRoom) { + // uploadFile(file, { description }); + // return; + // } + + // if (!settings.get('E2E_Enable_Encrypt_Files')) { + // uploadFile(file, { description }); + // return; + // } + + // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + // if (!shouldConvertSentMessages) { + // uploadFile(file, { description }); + // return; + // } + + // const encryptedFile = await e2eRoom.encryptFile(file); + + // if (encryptedFile) { + // const getContent = async (filesId: string[], filesUrl: string[]): Promise => { + // const attachments = []; + // const _id = filesId[0]; + // const fileUrl = filesUrl[0]; + + // const attachment: FileAttachmentProps = { + // title: file.name, + // type: 'file', + // description, + // title_link: fileUrl, + // title_link_download: true, + // encryption: { + // key: encryptedFile.key, + // iv: encryptedFile.iv, + // }, + // hashes: { + // sha256: encryptedFile.hash, + // }, + // }; + + // if (/^image\/.+/.test(file.type)) { + // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + + // attachments.push({ + // ...attachment, + // image_url: fileUrl, + // image_type: file.type, + // image_size: file.size, + // ...(dimensions && { + // image_dimensions: dimensions, + // }), + // }); + // } else if (/^audio\/.+/.test(file.type)) { + // attachments.push({ + // ...attachment, + // audio_url: fileUrl, + // audio_type: file.type, + // audio_size: file.size, + // }); + // } else if (/^video\/.+/.test(file.type)) { + // attachments.push({ + // ...attachment, + // video_url: fileUrl, + // video_type: file.type, + // video_size: file.size, + // }); + // } else { + // attachments.push({ + // ...attachment, + // size: file.size, + // format: getFileExtension(file.name), + // }); + // } + + // const files = [ + // { + // _id, + // name: file.name, + // type: file.type, + // size: file.size, + // // "format": "png" + // }, + // ]; + + // return e2eRoom.encryptMessageContent({ + // attachments, + // files, + // file: files[0], + // }); + // }; + + // const fileContentData = { + // type: file.type, + // typeGroup: file.type.split('/')[0], + // name: fileName, + // encryption: { + // key: encryptedFile.key, + // iv: encryptedFile.iv, + // }, + // hashes: { + // sha256: encryptedFile.hash, + // }, + // }; + + // const fileContent = { + // raw: fileContentData, + // encrypted: await e2eRoom.encryptMessageContent(fileContentData), + // }; + + // uploadFile( + // encryptedFile.file, + // { + // t: 'e2e', + // }, + // getContent, + // fileContent, + // ); + // } + // }, + // invalidContentType: !fileUploadIsValidContentType(file?.type), + // }, + // }); }; uploadNextFile(); - resetFileInput?.(); + // resetFileInput?.(); }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index f95dd8f9640c4..c2549bae6a637 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -3,10 +3,10 @@ import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI } from './ChatAPI'; import type { Upload } from './Upload'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; let uploads: readonly Upload[] = []; @@ -29,8 +29,24 @@ const wipeFailedOnes = (): void => { updateUploads((uploads) => uploads.filter((upload) => !upload.error)); }; +const removeUpload = (id: Upload['id']): void => { + updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); +}; + +const editUploadFileName = (id: Upload['id'], fileName: Upload['name']): void => { + updateUploads((uploads) => + uploads.map((upload) => + upload.id === id ? { ...upload, file: new File([upload.file], fileName, { type: upload.file.type }) } : upload, + ), + ); +}; + +const clear = (): void => { + updateUploads(() => []); +}; + const send = async ( - file: File[] | File, + file: File, { description, msg, @@ -47,28 +63,32 @@ const send = async ( getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { - const files = Array.isArray(file) ? file : [file]; const id = Random.id(); updateUploads((uploads) => [ ...uploads, { id, - name: files[0].name || fileContent?.raw.name || 'unknown', + name: fileContent?.raw.name || file.name, + file, percentage: 0, + url: URL.createObjectURL(file), }, ]); - const uploadPromises = files.map((f) => { - return new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { + try { + await new Promise((resolve, reject) => { const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { - file: f, + file, ...(fileContent && { content: JSON.stringify(fileContent.encrypted), }), }, { + load: (event) => { + resolve(event); + }, progress: (event) => { if (!event.lengthComputable) { return; @@ -109,7 +129,19 @@ const send = async ( xhr.onload = () => { if (xhr.readyState === xhr.DONE && xhr.status === 200) { const result = JSON.parse(xhr.responseText); - resolve({ fileId: result.file._id, fileUrl: result.file.url }); + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + id: result.file._id, + url: result.file.url, + }; + }), + ); } }; @@ -119,34 +151,7 @@ const send = async ( reject(new Error('Upload cancelled')); }); }); - }); - - try { - const results = await Promise.all(uploadPromises); - const fileIds = results.map((result) => result.fileId); - const fileUrls = results.map((result) => result.fileUrl); - - if (msg === undefined) { - msg = ''; - } - - let content; - if (getContent) { - content = await getContent(fileIds, fileUrls); - } - const text: IMessage = { - rid, - _id: id, - msg: msg || description || '', - ts: new Date(), - u: { _id: id, username: id }, - _updatedAt: new Date(), - tmid, - t, - content, - }; - await sdk.call('sendMessage', text, fileUrls, fileIds); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + // updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error: unknown) { updateUploads((uploads) => uploads.map((upload) => { @@ -173,8 +178,11 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes subscribe, wipeFailedOnes, cancel, + clear, + removeUpload, + editUploadFileName, send: ( - file: File[] | File, + file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index e9803eb98a7dd..e6e20a4703204 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,10 +1,17 @@ +import fileSize from 'filesize'; import { useCallback, useEffect, useSyncExternalStore } from 'react'; import { useFileUploadDropTarget } from './useFileUploadDropTarget'; import type { Upload } from '../../../../lib/chats/Upload'; import { useChat } from '../../contexts/ChatContext'; +type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; + export const useFileUpload = () => { + // const dispatchToastMessage = useToastMessageDispatch(); + // const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; + // const [isUploading, setIsUploading] = useState(false); + const chat = useChat(); if (!chat) { throw new Error('No ChatContext provided'); @@ -32,8 +39,59 @@ export const useFileUpload = () => { return { uploads, + isUploading: uploads.length > 0, handleUploadProgressClose, handleUploadFiles, targeDrop: useFileUploadDropTarget(), }; }; + +// const handleFilesToUpload: HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => { +// setFilesToUpload((prevFiles: File[]) => { +// let newFilesToUpload = [...prevFiles, ...filesList]; +// if (newFilesToUpload.length > 6) { +// newFilesToUpload = newFilesToUpload.slice(0, 6); +// dispatchToastMessage({ +// type: 'error', +// message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", +// }); +// } +// let nameError = 0; +// let sizeError = 0; + +// const validFiles = newFilesToUpload.filter((queuedFile) => { +// const { name, size } = queuedFile; + +// if (!name) { +// nameError = 1; +// return false; +// } + +// if (maxFileSize > -1 && (size || 0) > maxFileSize) { +// sizeError = 1; +// return false; +// } + +// return true; +// }); + +// if (nameError) { +// dispatchToastMessage({ +// type: 'error', +// message: t('error-the-field-is-required', { field: t('Name') }), +// }); +// } + +// if (sizeError) { +// dispatchToastMessage({ +// type: 'error', +// message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, +// }); +// } + +// setIsUploading(validFiles.length > 0); +// return validFiles; +// }); + +// resetFileInput?.(); +// }; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 5be3948fa7a86..036bbb810c5a0 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -9,23 +9,12 @@ import FilePreview from './FilePreview'; type FileUploadModalProps = { onClose: () => void; - onSubmit: (name: string, description?: string) => void; + onSubmit: (name: string) => void; file: File; fileName: string; - fileDescription?: string; - invalidContentType: boolean; - showDescription?: boolean; }; -const FileUploadModal = ({ - onClose, - file, - fileName, - fileDescription, - onSubmit, - invalidContentType, - showDescription = true, -}: FileUploadModalProps): ReactElement => { +const FileUploadModal = ({ onClose, file, fileName, fileDescription, onSubmit }: FileUploadModalProps): ReactElement => { const { register, handleSubmit, @@ -33,54 +22,54 @@ const FileUploadModal = ({ } = useForm({ mode: 'onBlur', defaultValues: { name: fileName, description: fileDescription } }); const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); + // const dispatchToastMessage = useToastMessageDispatch(); const maxMsgSize = useSetting('Message_MaxAllowedSize', 5000); const maxFileSize = useSetting('FileUpload_MaxFileSize', 104857600); const isDescriptionValid = (description: string) => description.length >= maxMsgSize ? t('Cannot_upload_file_character_limit', { count: maxMsgSize }) : true; - const submit = ({ name, description }: { name: string; description?: string }): void => { - // -1 maxFileSize means there is no limit - if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - onClose(); - return dispatchToastMessage({ - type: 'error', - message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), - }); - } + // MOVE TO UPLOAD PROCESS + // -1 maxFileSize means there is no limit + // const submit = ({ name, description }: { name: string; description?: string }): void => { + // // if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { + // // onClose(); + // // return dispatchToastMessage({ + // // type: 'error', + // // message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), + // // }); + // // } + // // onSubmit(name, description); + // }; - onSubmit(name, description); - }; + // MOVE TO THE UPLOAD PROCESS + // useEffect(() => { + // if (invalidContentType) { + // dispatchToastMessage({ + // type: 'error', + // message: t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }), + // }); + // onClose(); + // return; + // } - useEffect(() => { - if (invalidContentType) { - dispatchToastMessage({ - type: 'error', - message: t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }), - }); - onClose(); - return; - } - - if (file.size === 0) { - dispatchToastMessage({ - type: 'error', - message: t('FileUpload_File_Empty'), - }); - onClose(); - } - }, [file, dispatchToastMessage, invalidContentType, t, onClose]); + // if (file.size === 0) { + // dispatchToastMessage({ + // type: 'error', + // message: t('FileUpload_File_Empty'), + // }); + // onClose(); + // } + // }, [file, dispatchToastMessage, invalidContentType, t, onClose]); const fileUploadFormId = useId(); const fileNameField = useId(); - const fileDescriptionField = useId(); return ( ) => ( - + onSubmit(name))} {...props} /> )} > @@ -109,23 +98,6 @@ const FileUploadModal = ({ {errors.name && {errors.name.message}} - {showDescription && ( - - {t('Upload_file_description')} - - isDescriptionValid(value || ''), - })} - error={errors.description?.message} - aria-invalid={errors.description ? 'true' : 'false'} - aria-describedby={`${fileDescriptionField}-error`} - /> - - {errors.description && {errors.description.message}} - - )} @@ -134,7 +106,7 @@ const FileUploadModal = ({ {t('Cancel')} From 7bf3b59b67e07e49f5bf05c9f3513da1dbfb9f3a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 15:19:13 -0300 Subject: [PATCH 122/215] feat: `MessageComposerFile` loading and error --- .../room/composer/messageBox/MessageBox.tsx | 5 +- .../messageBox/MessageComposerFile.tsx | 80 +++++++------------ .../messageBox/MessageComposerFileArea.tsx | 9 ++- .../MessageComposerFileComponent.tsx | 60 ++++++++++++++ .../messageBox/MessageComposerFileError.tsx | 26 ++++++ packages/i18n/src/locales/en.i18n.json | 1 + 6 files changed, 123 insertions(+), 58 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 69cfe6f88176e..526b5bc300dcf 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -412,8 +412,9 @@ const MessageBox = ({ {isUploading && ( )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx index c82bbafce494a..848f8854b09bd 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx @@ -1,10 +1,13 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, IconButton, Palette } from '@rocket.chat/fuselage'; +import { IconButton } from '@rocket.chat/fuselage'; import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useState } from 'react'; import type { AllHTMLAttributes, ReactElement } from 'react'; +import MessageComposerFileComponent from './MessageComposerFileComponent'; +import MessageComposerFileError from './MessageComposerFileError'; import MessageComposerFileLoader from './MessageComposerFileLoader'; import { getMimeType } from '../../../../../app/utils/lib/mimeTypes'; +import { usePreventPropagation } from '../../../../hooks/usePreventPropagation'; import type { Upload } from '../../../../lib/chats/Upload'; import { formatBytes } from '../../../../lib/utils/formatBytes'; import FileUploadModal from '../../modals/FileUploadModal'; @@ -13,26 +16,13 @@ type MessageComposerFileProps = { upload: Upload; onRemove: (id: string) => void; onEdit: (id: Upload['id'], fileName: string) => void; + onCancel: (id: Upload['id']) => void; } & Omit, 'is'>; -const MessageComposerFile = ({ upload, onRemove, onEdit, ...props }: MessageComposerFileProps): ReactElement => { +const MessageComposerFile = ({ upload, onRemove, onEdit, onCancel, ...props }: MessageComposerFileProps): ReactElement => { + const [isHover, setIsHover] = useState(false); const setModal = useSetModal(); - const previewWrapperStyle = css` - background-color: 'surface-tint'; - - &:hover { - cursor: pointer; - background-color: ${Palette.surface['surface-hover']}; - } - `; - - const closeWrapperStyle = css` - position: absolute; - right: 0.25rem; - top: 0.25rem; - `; - const fileSize = formatBytes(upload.file.size, 2); const fileExtension = getMimeType(upload.file.type, upload.file.name); const isLoading = upload.percentage > 0 && upload.percentage !== 100; @@ -51,44 +41,30 @@ const MessageComposerFile = ({ upload, onRemove, onEdit, ...props }: MessageComp ); }; + const dismissAction = isLoading ? () => onCancel(upload.id) : () => onRemove(upload.id); + const handleDismiss = usePreventPropagation(dismissAction); + + if (upload.error) { + return ( + : } + /> + ); + } + return ( - ['Enter', 'Space'].includes(e.code) && handleOpenFilePreview()} + onMouseLeave={() => setIsHover(false)} + onMouseEnter={() => setIsHover(true)} + fileTitle={upload.file.name} + fileSubtitle={`${fileSize} - ${fileExtension}`} + actionIcon={isLoading && !isHover ? : } {...props} - > - - - {upload.file.name} - - {`${fileSize} - ${fileExtension}`} - - { - e.stopPropagation(); - onRemove(upload.id); - }} - > - {isLoading && } - {!isLoading && } - - + /> ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx index 3d1b1aa9fdf9b..f0683114161fb 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx @@ -5,16 +5,17 @@ import type { Upload } from '../../../../lib/chats/Upload'; type MessageComposerFileAreaProps = { uploads?: readonly Upload[]; - handleRemoveUpload: (id: Upload['id']) => void; - handleEditFileName: (id: Upload['id'], fileName: string) => void; + onRemove: (id: Upload['id']) => void; + onEdit: (id: Upload['id'], fileName: string) => void; + onCancel: (id: Upload['id']) => void; }; -const MessageComposerFileArea = ({ uploads, handleRemoveUpload, handleEditFileName }: MessageComposerFileAreaProps) => { +const MessageComposerFileArea = ({ uploads, onRemove, onEdit, onCancel }: MessageComposerFileAreaProps) => { return ( {uploads?.map((upload, index) => (
- +
))}
diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx new file mode 100644 index 0000000000000..a42fbf44ac1ca --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx @@ -0,0 +1,60 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { AllHTMLAttributes, ReactElement } from 'react'; + +type MessageComposerFileComponentProps = { + fileTitle: string; + fileSubtitle: string; + actionIcon: ReactElement; + error?: boolean; +} & Omit, 'is'>; + +// TODO: This component will live in `ui-composer` +const MessageComposerFileComponent = ({ fileTitle, fileSubtitle, actionIcon, error, ...props }: MessageComposerFileComponentProps) => { + const closeWrapperStyle = css` + position: absolute; + right: 0.5rem; + top: 0.5rem; + `; + + const previewWrapperStyle = css` + background-color: 'surface-tint'; + + &:hover { + cursor: ${error ? 'unset' : 'pointer'}; + background-color: ${Palette.surface['surface-hover']}; + } + `; + + return ( + + + + {fileTitle} + + + {fileSubtitle} + + + {actionIcon} + + ); +}; + +export default MessageComposerFileComponent; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx new file mode 100644 index 0000000000000..c92c6c69d3f5c --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx @@ -0,0 +1,26 @@ +import type { AllHTMLAttributes, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import MessageComposerFileComponent from './MessageComposerFileComponent'; + +type MessageComposerFileComponentProps = { + fileTitle: string; + error: Error; + actionIcon: ReactElement; +} & AllHTMLAttributes; + +const MessageComposerFileError = ({ fileTitle, error, actionIcon, ...props }: MessageComposerFileComponentProps) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default MessageComposerFileError; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 6478106c15d7e..061f9a83da42c 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5053,6 +5053,7 @@ "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", "Upload_app": "Upload App", + "Upload_failed": "Upload failed", "Set_randomly_and_send_by_email": "Set randomly and send by email", "Set_random_password_and_send_by_email": "Set random password and send by email", "set-leader": "Set Leader", From 60b2262051da9e8f758c59cb9f3cd05068a674cd Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 15:28:18 -0300 Subject: [PATCH 123/215] chore: `RoomV2` not rendering attachments in composer --- apps/meteor/client/views/room/body/RoomBodyV2.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 586fab7c18ab5..a4f87b7b1a7f1 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; -import { memo, useCallback, useMemo, useRef, useState } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; @@ -85,7 +85,6 @@ const RoomBody = (): ReactElement => { }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); const innerBoxRef = useRef(null); - const [filesToUpload, setFilesToUpload] = useState([]); const { wrapperRef: unreadBarWrapperRef, @@ -105,6 +104,7 @@ const RoomBody = (): ReactElement => { const { uploads, + isUploading, handleUploadFiles, handleUploadProgressClose, targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], @@ -210,7 +210,7 @@ const RoomBody = (): ReactElement => { >
- + {uploads.length > 0 && ( @@ -290,8 +290,8 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - setFilesToUpload={setFilesToUpload} - filesToUpload={filesToUpload} + uploads={uploads} + isUploading={isUploading} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} From e6752687fe70cc8078b9b0cc604d578b7438bc1a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 15:33:02 -0300 Subject: [PATCH 124/215] chore: remove `UploadProgressIndicator` --- .../client/views/room/body/RoomBody.tsx | 18 +---- .../client/views/room/body/RoomBodyV2.tsx | 18 +---- .../UploadProgressContainer.tsx | 8 --- .../UploadProgressIndicator.tsx | 65 ------------------- .../views/room/body/UploadProgress/index.ts | 2 - 5 files changed, 2 insertions(+), 109 deletions(-) delete mode 100644 apps/meteor/client/views/room/body/UploadProgress/UploadProgressContainer.tsx delete mode 100644 apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx delete mode 100644 apps/meteor/client/views/room/body/UploadProgress/index.ts diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 7f14751807c15..d28007d3c3944 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -30,7 +30,6 @@ import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; -import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; import { useFileUpload } from './hooks/useFileUpload'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; @@ -111,7 +110,7 @@ const RoomBody = (): ReactElement => { uploads, isUploading, handleUploadFiles, - handleUploadProgressClose, + // handleUploadProgressClose, targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); @@ -240,20 +239,6 @@ const RoomBody = (): ReactElement => { triggerProps={triggerProps} /> ) : null} - {uploads.length > 0 && ( - - {uploads.map((upload) => ( - - ))} - - )} {Boolean(unread) && ( { onMarkAsReadButtonClick={handleMarkAsReadButtonClick} /> )} - diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index a4f87b7b1a7f1..61eaa88c151d1 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -27,7 +27,6 @@ import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; import { RoomTopic } from './RoomTopic'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; -import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; import { useBannerSection } from './hooks/useBannerSection'; import { useFileUpload } from './hooks/useFileUpload'; import { useGetMore } from './hooks/useGetMore'; @@ -106,7 +105,7 @@ const RoomBody = (): ReactElement => { uploads, isUploading, handleUploadFiles, - handleUploadProgressClose, + // handleUploadProgressClose, targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); @@ -212,20 +211,6 @@ const RoomBody = (): ReactElement => {
- {uploads.length > 0 && ( - - {uploads.map((upload) => ( - - ))} - - )} {Boolean(unread) && ( { onMarkAsReadButtonClick={handleMarkAsReadButtonClick} /> )} - diff --git a/apps/meteor/client/views/room/body/UploadProgress/UploadProgressContainer.tsx b/apps/meteor/client/views/room/body/UploadProgress/UploadProgressContainer.tsx deleted file mode 100644 index 03d36898b8839..0000000000000 --- a/apps/meteor/client/views/room/body/UploadProgress/UploadProgressContainer.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ComponentProps } from 'react'; - -const UploadProgressContainer = (props: ComponentProps) => { - return ; -}; - -export default UploadProgressContainer; diff --git a/apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx b/apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx deleted file mode 100644 index 9a781fe520534..0000000000000 --- a/apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Button, Palette } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import type { Upload } from '../../../../lib/chats/Upload'; - -type UploadProgressIndicatorProps = { - id: Upload['id']; - name: string; - percentage: number; - error?: string; - onClose?: (id: Upload['id']) => void; -}; - -const UploadProgressIndicator = ({ id, name, percentage, error, onClose }: UploadProgressIndicatorProps): ReactElement | null => { - const { t } = useTranslation(); - - const customClass = css` - &::after { - content: ''; - position: absolute; - z-index: 1; - left: 0; - width: ${percentage}%; - height: 100%; - transition: width, 1s, ease-out; - background-color: ${Palette.surface['surface-neutral']}; - } - `; - - const handleCloseClick = useCallback(() => { - onClose?.(id); - }, [id, onClose]); - - return ( - - - [{percentage}%] {name} - - - - ); -}; - -export default UploadProgressIndicator; diff --git a/apps/meteor/client/views/room/body/UploadProgress/index.ts b/apps/meteor/client/views/room/body/UploadProgress/index.ts deleted file mode 100644 index b4bfb8f16b0f1..0000000000000 --- a/apps/meteor/client/views/room/body/UploadProgress/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as UploadProgressIndicator } from './UploadProgressIndicator'; -export { default as UploadProgressContainer } from './UploadProgressContainer'; From 22b981c852bcea2de046e8983b2b656c1c703f1e Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 11 Feb 2025 19:10:55 -0300 Subject: [PATCH 125/215] feat: limit number of files --- .../client/lib/chats/flows/uploadFiles.ts | 13 ++- .../client/views/room/body/RoomBody.tsx | 4 +- .../client/views/room/body/RoomBodyV2.tsx | 4 +- .../views/room/body/hooks/useFileUpload.ts | 79 ++++--------------- .../views/room/composer/ComposerMessage.tsx | 1 + .../room/composer/messageBox/MessageBox.tsx | 14 ++-- packages/i18n/src/locales/en.i18n.json | 1 + 7 files changed, 43 insertions(+), 73 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 830329c1280a5..3dd5be1ede662 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -4,9 +4,11 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; import { settings } from '../../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; +import { t } from '../../../../app/utils/lib/i18n'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; import { imperativeModal } from '../../imperativeModal'; +import { dispatchToastMessage } from '../../toast'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; @@ -24,6 +26,13 @@ const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number }; export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { + if (chat?.uploads.get().length > 10) { + return dispatchToastMessage({ + type: 'error', + message: t('You_cant_upload_more_than__count__files', { count: 10 }), + }); + } + const replies = chat.composer?.quotedMessages.get() ?? []; const msg = await prependReplies('', replies); @@ -47,7 +56,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi getContent, fileContent, ); - chat.composer?.clear(); + // chat.composer?.clear(); // imperativeModal.close(); uploadNextFile(); }; @@ -335,5 +344,5 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi }; uploadNextFile(); - // resetFileInput?.(); + resetFileInput?.(); }; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index d28007d3c3944..393f8eea2d4cf 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -109,9 +109,10 @@ const RoomBody = (): ReactElement => { const { uploads, isUploading, + hasUploads, handleUploadFiles, // handleUploadProgressClose, - targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], + targetDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); @@ -299,6 +300,7 @@ const RoomBody = (): ReactElement => { onUploadFiles={handleUploadFiles} uploads={uploads} isUploading={isUploading} + hasUploads={hasUploads} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 61eaa88c151d1..0ebdca2fe0f98 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -104,9 +104,10 @@ const RoomBody = (): ReactElement => { const { uploads, isUploading, + hasUploads, handleUploadFiles, // handleUploadProgressClose, - targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], + targetDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); @@ -276,6 +277,7 @@ const RoomBody = (): ReactElement => { onUploadFiles={handleUploadFiles} uploads={uploads} isUploading={isUploading} + hasUploads={hasUploads} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index e6e20a4703204..643fde0f73006 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,16 +1,13 @@ -import fileSize from 'filesize'; -import { useCallback, useEffect, useSyncExternalStore } from 'react'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'; import { useFileUploadDropTarget } from './useFileUploadDropTarget'; import type { Upload } from '../../../../lib/chats/Upload'; import { useChat } from '../../contexts/ChatContext'; -type HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => void; - export const useFileUpload = () => { - // const dispatchToastMessage = useToastMessageDispatch(); + const targetDrop = useFileUploadDropTarget(); // const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; - // const [isUploading, setIsUploading] = useState(false); const chat = useChat(); if (!chat) { @@ -37,61 +34,17 @@ export const useFileUpload = () => { [chat], ); - return { - uploads, - isUploading: uploads.length > 0, - handleUploadProgressClose, - handleUploadFiles, - targeDrop: useFileUploadDropTarget(), - }; + const isUploading = uploads.some((upload) => upload.percentage < 100); + + return useMemo( + () => ({ + uploads, + hasUploads: uploads.length > 0, + isUploading, + handleUploadProgressClose, + handleUploadFiles, + targetDrop, + }), + [uploads, isUploading, handleUploadProgressClose, handleUploadFiles, targetDrop], + ); }; - -// const handleFilesToUpload: HandleFilesToUpload = (filesList: File[], resetFileInput?: () => void) => { -// setFilesToUpload((prevFiles: File[]) => { -// let newFilesToUpload = [...prevFiles, ...filesList]; -// if (newFilesToUpload.length > 6) { -// newFilesToUpload = newFilesToUpload.slice(0, 6); -// dispatchToastMessage({ -// type: 'error', -// message: "You can't upload more than 6 files at once. Only the first 6 files will be uploaded.", -// }); -// } -// let nameError = 0; -// let sizeError = 0; - -// const validFiles = newFilesToUpload.filter((queuedFile) => { -// const { name, size } = queuedFile; - -// if (!name) { -// nameError = 1; -// return false; -// } - -// if (maxFileSize > -1 && (size || 0) > maxFileSize) { -// sizeError = 1; -// return false; -// } - -// return true; -// }); - -// if (nameError) { -// dispatchToastMessage({ -// type: 'error', -// message: t('error-the-field-is-required', { field: t('Name') }), -// }); -// } - -// if (sizeError) { -// dispatchToastMessage({ -// type: 'error', -// message: `${t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })}`, -// }); -// } - -// setIsUploading(validFiles.length > 0); -// return validFiles; -// }); - -// resetFileInput?.(); -// }; diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index e16f57c2c5b08..cc3f202cef852 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -14,6 +14,7 @@ import type { Upload } from '../../../lib/chats/Upload'; export type ComposerMessageProps = { uploads: readonly Upload[]; isUploading: boolean; + hasUploads: boolean; tmid?: IMessage['_id']; children?: ReactNode; subscription?: ISubscription; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 526b5bc300dcf..b72ee46ace34f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -95,6 +95,7 @@ type MessageBoxProps = { isEmbedded?: boolean; uploads: readonly Upload[]; isUploading: boolean; + hasUploads: boolean; }; const MessageBox = ({ @@ -110,6 +111,7 @@ const MessageBox = ({ previewUrls, uploads, isUploading, + hasUploads, }: MessageBoxProps): ReactElement => { const chat = useChat(); const room = useRoom(); @@ -165,9 +167,8 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); - // TODO: change to something like `hasUploads` const handleSendMessage = useEffectEvent(() => { - if (isUploading) { + if (hasUploads) { return chat?.flows.confirmFiles(); } const text = chat.composer?.text ?? ''; @@ -356,6 +357,7 @@ const MessageBox = ({ const mergedRefs = useMessageComposerMergedRefs(popup.callbackRef, textareaRef, callbackRef, autofocusRef, keyDownHandlerCallbackRef); const shouldPopupPreview = useEnablePopupPreview(popup.filter, popup.option); + const shouldDisableDueUploads = !hasUploads || isUploading; return ( <> @@ -409,7 +411,7 @@ const MessageBox = ({ aria-activedescendant={popup.focused ? `popup-item-${popup.focused._id}` : undefined} />
- {isUploading && ( + {hasUploads && ( )} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 061f9a83da42c..231272eb1ad52 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6247,6 +6247,7 @@ "You_can_do_from_account_preferences": "You can do this later from your account preferences", "You_can_search_using_RegExp_eg": "You can search using Regular Expression. e.g. /^text$/i", "You_can_try_to": "You can try to", + "You_cant_upload_more_than__count__files": "You can't upload more than {{count}} files at once. Only the first {{count}} files will be uploaded.", "You_can_use_an_emoji_as_avatar": "You can also use an emoji as an avatar.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate Omnichannel with your CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "You can't leave a omnichannel room. Please, use the close button.", From 6340dbfc8a4ce575fda2e399c9574ae259ce2de3 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 13 Feb 2025 16:31:23 -0300 Subject: [PATCH 126/215] feat: move file error validation to uploads.send --- .../client/lib/chats/flows/uploadFiles.ts | 5 +- apps/meteor/client/lib/chats/uploads.ts | 20 +++++++ .../views/room/body/hooks/useFileUpload.ts | 2 - .../FileUploadModal/FileUploadModal.spec.tsx | 1 - .../FileUploadModal/FileUploadModal.tsx | 57 +++---------------- packages/i18n/src/locales/en.i18n.json | 1 - 6 files changed, 29 insertions(+), 57 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 3dd5be1ede662..e751094f8ef8a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,13 +1,10 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; +// import { isRoomFederated } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; import { settings } from '../../../../app/settings/client'; -import { fileUploadIsValidContentType } from '../../../../app/utils/client'; import { t } from '../../../../app/utils/lib/i18n'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; -import FileUploadModal from '../../../views/room/modals/FileUploadModal'; -import { imperativeModal } from '../../imperativeModal'; import { dispatchToastMessage } from '../../toast'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index c2549bae6a637..94a936b698ea8 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,12 +1,16 @@ import type { IMessage, IRoom, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; +import fileSize from 'filesize'; import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI } from './ChatAPI'; import type { Upload } from './Upload'; +import { settings } from '../../../app/settings/client'; +import { fileUploadIsValidContentType } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { i18n } from '../../../app/utils/lib/i18n'; let uploads: readonly Upload[] = []; @@ -63,7 +67,10 @@ const send = async ( getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, ): Promise => { + const maxFileSize = settings.get('FileUpload_MaxFileSize'); + const invalidContentType = !fileUploadIsValidContentType(file.type); const id = Random.id(); + updateUploads((uploads) => [ ...uploads, { @@ -77,6 +84,19 @@ const send = async ( try { await new Promise((resolve, reject) => { + if (file.size === 0) { + return reject(new Error(i18n.t('FileUpload_File_Empty'))); + } + + // -1 maxFileSize means there is no limit + if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { + return reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }))); + } + + if (invalidContentType) { + return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); + } + const xhr = sdk.rest.upload( `/v1/rooms.media/${rid}`, { diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index 643fde0f73006..7731ae2f8497a 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,4 +1,3 @@ -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'; import { useFileUploadDropTarget } from './useFileUploadDropTarget'; @@ -7,7 +6,6 @@ import { useChat } from '../../contexts/ChatContext'; export const useFileUpload = () => { const targetDrop = useFileUploadDropTarget(); - // const maxFileSize = useSetting('FileUpload_MaxFileSize') as number; const chat = useChat(); if (!chat) { diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx index 222ced1701d7c..94bc4601ffa18 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx @@ -18,7 +18,6 @@ it('should show Undo request button when roomOpen is true and transcriptRequest render(, { wrapper: mockAppRoot() .withTranslations('en', 'core', { - Cannot_upload_file_character_limit: 'Cannot upload file, description is over the {{count}} character limit', Send: 'Send', Upload_file_description: 'File description', }) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 036bbb810c5a0..ed392c9c14e41 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -1,8 +1,7 @@ import { Modal, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldError, TextInput, Button } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; -import fileSize from 'filesize'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; -import { memo, useEffect, useId } from 'react'; +import { memo, useId } from 'react'; import { useForm } from 'react-hook-form'; import FilePreview from './FilePreview'; @@ -14,56 +13,16 @@ type FileUploadModalProps = { fileName: string; }; -const FileUploadModal = ({ onClose, file, fileName, fileDescription, onSubmit }: FileUploadModalProps): ReactElement => { +const FileUploadModal = ({ onClose, file, fileName, onSubmit }: FileUploadModalProps): ReactElement => { + const t = useTranslation(); + const fileUploadFormId = useId(); + const fileNameField = useId(); + const { register, handleSubmit, formState: { errors, isSubmitting }, - } = useForm({ mode: 'onBlur', defaultValues: { name: fileName, description: fileDescription } }); - - const t = useTranslation(); - // const dispatchToastMessage = useToastMessageDispatch(); - const maxMsgSize = useSetting('Message_MaxAllowedSize', 5000); - const maxFileSize = useSetting('FileUpload_MaxFileSize', 104857600); - - const isDescriptionValid = (description: string) => - description.length >= maxMsgSize ? t('Cannot_upload_file_character_limit', { count: maxMsgSize }) : true; - - // MOVE TO UPLOAD PROCESS - // -1 maxFileSize means there is no limit - // const submit = ({ name, description }: { name: string; description?: string }): void => { - // // if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - // // onClose(); - // // return dispatchToastMessage({ - // // type: 'error', - // // message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }), - // // }); - // // } - // // onSubmit(name, description); - // }; - - // MOVE TO THE UPLOAD PROCESS - // useEffect(() => { - // if (invalidContentType) { - // dispatchToastMessage({ - // type: 'error', - // message: t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }), - // }); - // onClose(); - // return; - // } - - // if (file.size === 0) { - // dispatchToastMessage({ - // type: 'error', - // message: t('FileUpload_File_Empty'), - // }); - // onClose(); - // } - // }, [file, dispatchToastMessage, invalidContentType, t, onClose]); - - const fileUploadFormId = useId(); - const fileNameField = useId(); + } = useForm({ mode: 'onBlur', defaultValues: { name: fileName } }); return ( Date: Fri, 14 Feb 2025 11:03:52 -0300 Subject: [PATCH 127/215] feat: new endpoint to edit file name properly --- apps/meteor/app/api/server/v1/rooms.ts | 26 +++++++++++ apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- apps/meteor/client/lib/chats/Upload.ts | 1 - .../client/lib/chats/flows/uploadFiles.ts | 1 + apps/meteor/client/lib/chats/uploads.ts | 45 ++++++++++++++----- packages/rest-typings/src/v1/rooms.ts | 4 ++ 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 589b7af3d8838..cbc69fa558de7 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -331,6 +331,32 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.mediaEdit/:rid/:fileId', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.forbidden(); + } + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { + throw new Meteor.Error('invalid-file'); + } + + if (!this.bodyParams.fileName) { + throw new Meteor.Error('invalid-file-name'); + } + + await Uploads.updateFileNameById(this.urlParams.fileId, this.bodyParams.fileName); + + return API.v1.success(); + }, + }, +); + API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 2969954b337a9..9d15fecd6c56d 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -104,7 +104,7 @@ export type UploadsAPI = { removeUpload(id: Upload['id']): void; editUploadFileName: (id: Upload['id'], fileName: string) => void; send( - file: File[] | File, + file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, diff --git a/apps/meteor/client/lib/chats/Upload.ts b/apps/meteor/client/lib/chats/Upload.ts index edffc6b5f3bf7..b32f0b662b1cf 100644 --- a/apps/meteor/client/lib/chats/Upload.ts +++ b/apps/meteor/client/lib/chats/Upload.ts @@ -1,6 +1,5 @@ export type Upload = { readonly id: string; - readonly name: string; readonly file: File; readonly url: string; readonly percentage: number; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index e751094f8ef8a..66440eee7f93a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -23,6 +23,7 @@ const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number }; export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { + // TODO: calculate max files based on the new array and the files in the queue if (chat?.uploads.get().length > 10) { return dispatchToastMessage({ type: 'error', diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 94a936b698ea8..b127b54ef35fe 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -37,18 +37,40 @@ const removeUpload = (id: Upload['id']): void => { updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); }; -const editUploadFileName = (id: Upload['id'], fileName: Upload['name']): void => { - updateUploads((uploads) => - uploads.map((upload) => - upload.id === id ? { ...upload, file: new File([upload.file], fileName, { type: upload.file.type }) } : upload, - ), - ); -}; +const editUploadFileName = async (rid: IRoom['_id'], uploadId: Upload['id'], fileName: Upload['file']['name']): Promise => { + try { + await sdk.rest.post(`/v1/rooms.mediaEdit/${rid}/${uploadId}`, { + fileName, + }); + + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; + } -const clear = (): void => { - updateUploads(() => []); + return { ...upload, file: new File([upload.file], fileName, upload.file) }; + }), + ); + } catch (error) { + updateUploads((uploads) => + uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error('Could not updated file name'), + }; + }), + ); + } }; +const clear = () => updateUploads(() => []); + const send = async ( file: File, { @@ -75,8 +97,7 @@ const send = async ( ...uploads, { id, - name: fileContent?.raw.name || file.name, - file, + file: new File([file], fileContent?.raw.name || file.name, file), percentage: 0, url: URL.createObjectURL(file), }, @@ -200,7 +221,7 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes cancel, clear, removeUpload, - editUploadFileName, + editUploadFileName: (id, fileName) => editUploadFileName(rid, id, fileName), send: ( file: File, { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 902f20aec394e..186f43b3d68e7 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -774,6 +774,10 @@ export type RoomsEndpoints = { }) => { message: IMessage | null }; }; + '/v1/rooms.mediaEdit/:rid/:fileId': { + POST: (params: { fileName?: string }) => void; + }; + '/v1/rooms.saveNotification': { POST: (params: { roomId: string; notifications: Notifications }) => void; }; From a11bfc014df6c1922392bb0f30575374ce299533 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 14 Feb 2025 16:43:32 -0300 Subject: [PATCH 128/215] refactor: UploadsStore --- apps/meteor/client/lib/chats/uploads.ts | 372 ++++++++++++------------ 1 file changed, 185 insertions(+), 187 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index b127b54ef35fe..3f02a1d8fd4f8 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -12,220 +12,218 @@ import { fileUploadIsValidContentType } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { i18n } from '../../../app/utils/lib/i18n'; -let uploads: readonly Upload[] = []; +class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI { + private rid: string; -const emitter = new Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }>(); + private tmid?: string; -const updateUploads = (update: (uploads: readonly Upload[]) => readonly Upload[]): void => { - uploads = update(uploads); - emitter.emit('update'); -}; + constructor({ rid, tmid }: { rid: string; tmid?: IMessage['_id'] }) { + super(); -const get = (): readonly Upload[] => uploads; + this.rid = rid; + this.tmid = tmid; + } -const subscribe = (callback: () => void): (() => void) => emitter.on('update', callback); + uploads: readonly Upload[] = []; -const cancel = (id: Upload['id']): void => { - emitter.emit(`cancelling-${id}`); -}; + set = (uploads: Upload[]): void => { + this.uploads = uploads; + this.emit('update'); + }; -const wipeFailedOnes = (): void => { - updateUploads((uploads) => uploads.filter((upload) => !upload.error)); -}; + get = (): readonly Upload[] => this.uploads; -const removeUpload = (id: Upload['id']): void => { - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); -}; + subscribe = (callback: () => void): (() => void) => this.on('update', callback); -const editUploadFileName = async (rid: IRoom['_id'], uploadId: Upload['id'], fileName: Upload['file']['name']): Promise => { - try { - await sdk.rest.post(`/v1/rooms.mediaEdit/${rid}/${uploadId}`, { - fileName, - }); + cancel = (id: Upload['id']): void => { + this.emit(`cancelling-${id}`); + }; - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== uploadId) { - return upload; - } + wipeFailedOnes = (): void => { + this.set(this.uploads.filter((upload) => !upload.error)); + }; - return { ...upload, file: new File([upload.file], fileName, upload.file) }; - }), - ); - } catch (error) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== uploadId) { - return upload; - } + removeUpload = (id: Upload['id']): void => { + this.set(this.uploads.filter((upload) => upload.id !== id)); + }; - return { - ...upload, - percentage: 0, - error: new Error('Could not updated file name'), - }; - }), - ); - } -}; - -const clear = () => updateUploads(() => []); - -const send = async ( - file: File, - { - description, - msg, - rid, - tmid, - t, - }: { - description?: string; - msg?: string; - rid: string; - tmid?: string; - t?: IMessage['t']; - }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, -): Promise => { - const maxFileSize = settings.get('FileUpload_MaxFileSize'); - const invalidContentType = !fileUploadIsValidContentType(file.type); - const id = Random.id(); - - updateUploads((uploads) => [ - ...uploads, + editUploadFileName = async (uploadId: Upload['id'], fileName: Upload['file']['name']): Promise => { + try { + await sdk.rest.post(`/v1/rooms.mediaEdit/${this.rid}/${uploadId}`, { + fileName, + }); + + this.set( + this.uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; + } + + return { ...upload, file: new File([upload.file], fileName, upload.file) }; + }), + ); + } catch (error) { + this.set( + this.uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error('Could not updated file name'), + }; + }), + ); + } + }; + + clear = () => this.set([]); + + send = async ( + file: File, { - id, - file: new File([file], fileContent?.raw.name || file.name, file), - percentage: 0, - url: URL.createObjectURL(file), + description, + msg, + // rid, + // tmid, + t, + }: { + description?: string; + msg?: string; + rid: string; + tmid?: string; + t?: IMessage['t']; }, - ]); - - try { - await new Promise((resolve, reject) => { - if (file.size === 0) { - return reject(new Error(i18n.t('FileUpload_File_Empty'))); - } + getContent?: (fileId: string[], fileUrl: string[]) => Promise, + fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + ): Promise => { + const maxFileSize = settings.get('FileUpload_MaxFileSize'); + const invalidContentType = !fileUploadIsValidContentType(file.type); + const id = Random.id(); + + this.set([ + ...this.uploads, + { + id, + file: new File([file], fileContent?.raw.name || file.name, file), + percentage: 0, + url: URL.createObjectURL(file), + }, + ]); + + try { + await new Promise((resolve, reject) => { + if (file.size === 0) { + return reject(new Error(i18n.t('FileUpload_File_Empty'))); + } - // -1 maxFileSize means there is no limit - if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - return reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }))); - } + // -1 maxFileSize means there is no limit + if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { + return reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }))); + } - if (invalidContentType) { - return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); - } + if (invalidContentType) { + return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); + } - const xhr = sdk.rest.upload( - `/v1/rooms.media/${rid}`, - { - file, - ...(fileContent && { - content: JSON.stringify(fileContent.encrypted), - }), - }, - { - load: (event) => { - resolve(event); + const xhr = sdk.rest.upload( + `/v1/rooms.media/${this.rid}`, + { + file, + ...(fileContent && { + content: JSON.stringify(fileContent.encrypted), + }), }, - progress: (event) => { - if (!event.lengthComputable) { - return; - } - const progress = (event.loaded / event.total) * 100; - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: Math.round(progress) || 0, - }; - }), - ); + { + load: (event) => { + resolve(event); + }, + progress: (event) => { + if (!event.lengthComputable) { + return; + } + const progress = (event.loaded / event.total) * 100; + this.set( + this.uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: Math.round(progress) || 0, + }; + }), + ); + }, + error: (event) => { + this.set( + this.uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(xhr.responseText), + }; + }), + ); + reject(event); + }, }, - error: (event) => { - updateUploads((uploads) => - uploads.map((upload) => { + ); + + xhr.onload = () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + this.set( + this.uploads.map((upload) => { if (upload.id !== id) { return upload; } return { ...upload, - percentage: 0, - error: new Error(xhr.responseText), + id: result.file._id, + url: result.file.url, }; }), ); - reject(event); - }, - }, - ); - - xhr.onload = () => { - if (xhr.readyState === xhr.DONE && xhr.status === 200) { - const result = JSON.parse(xhr.responseText); - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - id: result.file._id, - url: result.file.url, - }; - }), - ); - } - }; + } + }; - emitter.once(`cancelling-${id}`, () => { - xhr.abort(); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - reject(new Error('Upload cancelled')); + this.once(`cancelling-${id}`, () => { + xhr.abort(); + this.set(this.uploads.filter((upload) => upload.id !== id)); + reject(new Error('Upload cancelled')); + }); }); - }); - // updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error: unknown) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + // updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); + } catch (error: unknown) { + this.set( + this.uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + percentage: 0, + error: new Error(getErrorMessage(error)), + }; + }), + ); + } finally { + if (!this.uploads.length) { + UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); + } } - } -}; - -export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ - get, - subscribe, - wipeFailedOnes, - cancel, - clear, - removeUpload, - editUploadFileName: (id, fileName) => editUploadFileName(rid, id, fileName), - send: ( - file: File, - { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), -}); + }; +} + +export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => + new UploadsStore({ rid, tmid }); From 15237a28b50e8f2da2bf093027dcef27996cb78c Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 14 Feb 2025 16:54:08 -0300 Subject: [PATCH 129/215] feat: introduce threadUploads --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 5 ++- apps/meteor/client/lib/chats/ChatAPI.ts | 11 ++++++- .../client/lib/chats/flows/confirmFiles.ts | 10 +++--- .../lib/chats/flows/processTooLongMessage.ts | 4 +-- .../client/lib/chats/flows/uploadFiles.ts | 11 ++++--- .../AudioMessageRecorder.tsx | 10 +++--- .../VideoMessageRecorder.tsx | 10 +++--- .../client/views/room/body/RoomBody.tsx | 20 ++++------- .../views/room/body/hooks/useFileUpload.ts | 28 ++++++++-------- .../body/hooks/useFileUploadDropTarget.ts | 7 ++-- .../views/room/composer/ComposerMessage.tsx | 10 ++---- .../room/composer/messageBox/MessageBox.tsx | 33 ++++++++++--------- .../MessageBoxActionsToolbar.tsx | 3 +- .../hooks/useFileUploadAction.ts | 7 ++-- .../Threads/components/ThreadChat.tsx | 17 +++++----- 15 files changed, 101 insertions(+), 85 deletions(-) diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 9298e8724b3f4..16f6319f86c2f 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -43,6 +43,8 @@ export class ChatMessages implements ChatAPI { public uploads: UploadsAPI; + public threadUploads: UploadsAPI; + public ActionManager: any; public emojiPicker: { @@ -148,7 +150,8 @@ export class ChatMessages implements ChatAPI { const { rid, tmid } = params; this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); - this.uploads = createUploadsAPI({ rid, tmid }); + this.uploads = createUploadsAPI({ rid }); + this.threadUploads = createUploadsAPI({ rid, tmid }); this.ActionManager = params.actionManager; const unimplemented = () => { diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 9d15fecd6c56d..93d6893ed3472 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -117,6 +117,7 @@ export type ChatAPI = { readonly setComposerAPI: (composer?: ComposerAPI) => void; readonly data: DataAPI; readonly uploads: UploadsAPI; + readonly threadUploads: UploadsAPI; readonly readStateManager: ReadStateManager; readonly messageEditing: { toPreviousMessage(): Promise; @@ -147,7 +148,15 @@ export type ChatAPI = { ActionManager: IActionManager; readonly flows: { - readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; + readonly uploadFiles: ({ + files, + uploadsStore, + resetFileInput, + }: { + files: readonly File[]; + uploadsStore: UploadsAPI; + resetFileInput?: () => void; + }) => Promise; readonly confirmFiles: () => Promise; readonly sendMessage: ({ text, diff --git a/apps/meteor/client/lib/chats/flows/confirmFiles.ts b/apps/meteor/client/lib/chats/flows/confirmFiles.ts index 45d1a1bc229dc..57d9c6831bd3c 100644 --- a/apps/meteor/client/lib/chats/flows/confirmFiles.ts +++ b/apps/meteor/client/lib/chats/flows/confirmFiles.ts @@ -5,17 +5,19 @@ export const confirmFiles = async (chat: ChatAPI): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; const msg = chat.composer?.text || ''; - const fileUrls = chat.uploads.get().map((upload) => upload.url); - const fileIds = chat.uploads.get().map((upload) => upload.id); - const message = await chat.data.composeMessage(msg, { quotedMessages: replies, }); + const store = message?.tmid ? chat.threadUploads : chat.uploads; + + const fileUrls = store.get().map((upload) => upload.url); + const fileIds = store.get().map((upload) => upload.id); + try { await sdk.call('sendMessage', message, fileUrls, fileIds); chat.composer?.clear(); - chat?.uploads.clear(); + store.clear(); } catch (error: unknown) { console.error(error); } diff --git a/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts b/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts index 11781348978e5..8b2a0c2b30168 100644 --- a/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts +++ b/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts @@ -7,7 +7,7 @@ import { imperativeModal } from '../../imperativeModal'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; -export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick): Promise => { +export const processTooLongMessage = async (chat: ChatAPI, { msg, tmid }: Pick): Promise => { const maxAllowedSize = settings.get('Message_MaxAllowedSize'); if (msg.length <= maxAllowedSize) { @@ -33,7 +33,7 @@ export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick => { return new Promise((resolve) => { @@ -22,9 +22,12 @@ const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number }); }; -export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { +export const uploadFiles = async ( + chat: ChatAPI, + { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, +): Promise => { // TODO: calculate max files based on the new array and the files in the queue - if (chat?.uploads.get().length > 10) { + if (uploadsStore.get().length > 10) { return dispatchToastMessage({ type: 'error', message: t('You_cant_upload_more_than__count__files', { count: 10 }), @@ -45,7 +48,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi getContent?: (fileId: string[], fileUrl: string[]) => Promise, fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ) => { - chat.uploads.send( + uploadsStore.send( file, { msg, diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx index 3a57324c70c13..62a2b2ec56276 100644 --- a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -7,18 +7,18 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AudioRecorder } from '../../../../app/ui/client/lib/recorderjs/AudioRecorder'; -import type { ChatAPI } from '../../../lib/chats/ChatAPI'; +import type { UploadsAPI } from '../../../lib/chats/ChatAPI'; import { useChat } from '../../room/contexts/ChatContext'; const audioRecorder = new AudioRecorder(); type AudioMessageRecorderProps = { rid: IRoom['_id']; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React + uploadsStore: UploadsAPI; isMicrophoneDenied?: boolean; }; -const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => { +const AudioMessageRecorder = ({ rid, uploadsStore, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => { const { t } = useTranslation(); const [state, setState] = useState<'loading' | 'recording'>('recording'); @@ -81,7 +81,7 @@ const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMes await stopRecording(); }); - const chat = useChat() ?? chatContext; + const chat = useChat(); const handleDoneButtonClick = useEffectEvent(async () => { setState('loading'); @@ -91,7 +91,7 @@ const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMes const fileName = `${t('Audio_record')}.mp3`; const file = new File([blob], fileName, { type: 'audio/mpeg' }); - await chat?.flows.uploadFiles([file]); + await chat?.flows.uploadFiles({ files: [file], uploadsStore }); }); useEffect(() => { diff --git a/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx b/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx index 0c50a6013ae60..685fe624703ab 100644 --- a/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx +++ b/apps/meteor/client/views/composer/VideoMessageRecorder/VideoMessageRecorder.tsx @@ -8,13 +8,13 @@ import { useRef, useEffect, useState } from 'react'; import { UserAction, USER_ACTIVITIES } from '../../../../app/ui/client/lib/UserAction'; import { VideoRecorder } from '../../../../app/ui/client/lib/recorderjs/videoRecorder'; -import type { ChatAPI } from '../../../lib/chats/ChatAPI'; +import type { UploadsAPI } from '../../../lib/chats/ChatAPI'; import { useChat } from '../../room/contexts/ChatContext'; type VideoMessageRecorderProps = { rid: IRoom['_id']; tmid?: IMessage['_id']; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React + uploadsStore: UploadsAPI; reference: RefObject; } & Omit, 'is'>; @@ -38,7 +38,7 @@ const getVideoRecordingExtension = () => { return 'mp4'; }; -const VideoMessageRecorder = ({ rid, tmid, chatContext, reference }: VideoMessageRecorderProps) => { +const VideoMessageRecorder = ({ rid, tmid, uploadsStore, reference }: VideoMessageRecorderProps) => { const t = useTranslation(); const videoRef = useRef(null); const dispatchToastMessage = useToastMessageDispatch(); @@ -49,7 +49,7 @@ const VideoMessageRecorder = ({ rid, tmid, chatContext, reference }: VideoMessag const isRecording = recordingState === 'recording'; const sendButtonDisabled = !(VideoRecorder.cameraStarted.get() && !(recordingState === 'recording')); - const chat = useChat() ?? chatContext; + const chat = useChat(); const stopVideoRecording = async (rid: IRoom['_id'], tmid?: IMessage['_id']) => { if (recordingInterval) { @@ -86,7 +86,7 @@ const VideoMessageRecorder = ({ rid, tmid, chatContext, reference }: VideoMessag const cb = async (blob: Blob) => { const fileName = `${t('Video_record')}.${getVideoRecordingExtension()}`; const file = new File([blob], fileName, { type: VideoRecorder.getSupportedMimeTypes().split(';')[0] }); - await chat?.flows.uploadFiles([file]); + await chat?.flows.uploadFiles({ files: [file], uploadsStore }); chat?.composer?.setRecordingVideo(false); }; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 393f8eea2d4cf..c2ef36c17f400 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -30,7 +30,8 @@ import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; -import { useFileUpload } from './hooks/useFileUpload'; +// import { useFileUpload } from './hooks/useFileUpload'; +import { useFileUploadDropTarget } from './hooks/useFileUploadDropTarget'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; @@ -106,14 +107,7 @@ const RoomBody = (): ReactElement => { const { wrapperRef: leaderBannerWrapperRef, hideLeaderHeader, innerRef: leaderBannerInnerRef } = useLeaderBanner(); - const { - uploads, - isUploading, - hasUploads, - handleUploadFiles, - // handleUploadProgressClose, - targetDrop: [fileUploadTriggerProps, fileUploadOverlayProps], - } = useFileUpload(); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.uploads); const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); @@ -297,10 +291,10 @@ const RoomBody = (): ReactElement => { onResize={handleComposerResize} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - onUploadFiles={handleUploadFiles} - uploads={uploads} - isUploading={isUploading} - hasUploads={hasUploads} + // onUploadFiles={handleUploadFiles} + // uploads={uploads} + // isUploading={isUploading} + // hasUploads={hasUploads} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index 7731ae2f8497a..443fd229b6233 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,35 +1,35 @@ import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'; -import { useFileUploadDropTarget } from './useFileUploadDropTarget'; +// import { useFileUploadDropTarget } from './useFileUploadDropTarget'; +import type { UploadsAPI } from '../../../../lib/chats/ChatAPI'; import type { Upload } from '../../../../lib/chats/Upload'; import { useChat } from '../../contexts/ChatContext'; -export const useFileUpload = () => { - const targetDrop = useFileUploadDropTarget(); - +export const useFileUpload = (store: UploadsAPI) => { const chat = useChat(); - if (!chat) { + + if (!chat || !store) { throw new Error('No ChatContext provided'); } useEffect(() => { - chat.uploads.wipeFailedOnes(); - }, [chat]); + store.wipeFailedOnes(); + }, [store]); - const uploads = useSyncExternalStore(chat.uploads.subscribe, chat.uploads.get); + const uploads = useSyncExternalStore(store.subscribe, store.get); const handleUploadProgressClose = useCallback( (id: Upload['id']) => { - chat.uploads.cancel(id); + store.cancel(id); }, - [chat], + [store], ); const handleUploadFiles = useCallback( (files: readonly File[]): void => { - chat.flows.uploadFiles(files); + chat?.flows.uploadFiles({ files, uploadsStore: store }); }, - [chat], + [chat, store], ); const isUploading = uploads.some((upload) => upload.percentage < 100); @@ -41,8 +41,8 @@ export const useFileUpload = () => { isUploading, handleUploadProgressClose, handleUploadFiles, - targetDrop, + // targetDrop, }), - [uploads, isUploading, handleUploadProgressClose, handleUploadFiles, targetDrop], + [uploads, isUploading, handleUploadProgressClose, handleUploadFiles], ); }; diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index c247822f160e1..11f1e8f3c9f31 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -6,11 +6,14 @@ import { useCallback, useMemo } from 'react'; import { useDropTarget } from './useDropTarget'; import { useIsRoomOverMacLimit } from '../../../../hooks/omnichannel/useIsRoomOverMacLimit'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import type { UploadsAPI } from '../../../../lib/chats/ChatAPI'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { useChat } from '../../contexts/ChatContext'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; -export const useFileUploadDropTarget = (): readonly [ +export const useFileUploadDropTarget = ( + uploadsStore: UploadsAPI, +): readonly [ fileUploadTriggerProps: { onDragEnter: (event: DragEvent) => void; }, @@ -58,7 +61,7 @@ export const useFileUploadDropTarget = (): readonly [ return file; }); - chat?.flows.uploadFiles(uploads); + chat?.flows.uploadFiles({ files: uploads, uploadsStore }); }); const allOverlayProps = useMemo(() => { diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index cc3f202cef852..3c21479dba868 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -9,12 +9,8 @@ import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { useChat } from '../contexts/ChatContext'; import { useRoom } from '../contexts/RoomContext'; import MessageBox from './messageBox/MessageBox'; -import type { Upload } from '../../../lib/chats/Upload'; export type ComposerMessageProps = { - uploads: readonly Upload[]; - isUploading: boolean; - hasUploads: boolean; tmid?: IMessage['_id']; children?: ReactNode; subscription?: ISubscription; @@ -78,9 +74,9 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac }, onNavigateToPreviousMessage: () => chat?.messageEditing.toPreviousMessage(), onNavigateToNextMessage: () => chat?.messageEditing.toNextMessage(), - onUploadFiles: (files: readonly File[]) => { - return chat?.flows.uploadFiles(files); - }, + // onUploadFiles: (files: readonly File[]) => { + // return chat?.flows.uploadFiles({ files, uploadsStore: chat.uploads }); + // }, }), [chat?.data, chat?.flows, chat?.action, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, onSend], ); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index b72ee46ace34f..88dfd3ea87850 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -31,11 +31,11 @@ import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionF import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; -import type { Upload } from '../../../../lib/chats/Upload'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { keyCodes } from '../../../../lib/utils/keyCodes'; import AudioMessageRecorder from '../../../composer/AudioMessageRecorder'; import VideoMessageRecorder from '../../../composer/VideoMessageRecorder'; +import { useFileUpload } from '../../body/hooks/useFileUpload'; import { useChat } from '../../contexts/ChatContext'; import { useComposerPopupOptions } from '../../contexts/ComposerPopupContext'; import { useRoom } from '../../contexts/RoomContext'; @@ -87,15 +87,15 @@ type MessageBoxProps = { onEscape?: () => void; onNavigateToPreviousMessage?: () => void; onNavigateToNextMessage?: () => void; - onUploadFiles: (files: readonly File[]) => void; + // onUploadFiles: (files: readonly File[]) => void; tshow?: IMessage['tshow']; previewUrls?: string[]; subscription?: ISubscription; showFormattingTips: boolean; isEmbedded?: boolean; - uploads: readonly Upload[]; - isUploading: boolean; - hasUploads: boolean; + // uploads: readonly Upload[]; + // isUploading: boolean; + // hasUploads: boolean; }; const MessageBox = ({ @@ -104,14 +104,14 @@ const MessageBox = ({ onJoin, onNavigateToNextMessage, onNavigateToPreviousMessage, - onUploadFiles, + // onUploadFiles, onEscape, onTyping, tshow, previewUrls, - uploads, - isUploading, - hasUploads, + // uploads, + // isUploading, + // hasUploads, }: MessageBoxProps): ReactElement => { const chat = useChat(); const room = useRoom(); @@ -167,6 +167,9 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); + const uploadsStore = tmid ? chat.threadUploads : chat.uploads; + const { uploads, hasUploads, handleUploadFiles, isUploading } = useFileUpload(uploadsStore); + const handleSendMessage = useEffectEvent(() => { if (hasUploads) { return chat?.flows.confirmFiles(); @@ -335,7 +338,7 @@ const MessageBox = ({ if (files.length) { event.preventDefault(); - onUploadFiles?.(files); + handleUploadFiles?.(files); } }); @@ -396,9 +399,9 @@ const MessageBox = ({ unencryptedMessagesAllowed={unencryptedMessagesAllowed} isMobile={isMobile} /> - {isRecordingVideo && } + {isRecordingVideo && } - {isRecordingAudio && } + {isRecordingAudio && } )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index b074a91feae4d..be7c6c1e2470f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -56,10 +56,11 @@ const MessageBoxActionsToolbar = ({ } const room = useRoom(); + const uploadsStore = tmid ? chatContext.threadUploads : chatContext.uploads; const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing); + const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, uploadsStore); const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 8367ed210de67..d84adfbb8bbb5 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -3,11 +3,12 @@ import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useFileInput } from '../../../../../../hooks/useFileInput'; +import type { UploadsAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { +export const useFileUploadAction = (disabled: boolean, uploadsStore: UploadsAPI): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled', true); const fileInputRef = useFileInput(fileInputProps); @@ -30,12 +31,12 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => }); return file; }); - chat?.flows.uploadFiles(filesToUpload, resetFileInput); + chat?.flows.uploadFiles({ files: filesToUpload, uploadsStore, resetFileInput }); }; fileInputRef.current?.addEventListener('change', handleUploadChange); return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); - }, [chat, fileInputRef]); + }, [chat, fileInputRef, uploadsStore]); const handleUpload = () => { fileInputRef?.current?.click(); 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 f44586068667e..3556b9b5af76b 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -22,8 +22,11 @@ type ThreadChatProps = { }; const ThreadChat = ({ mainMessage }: ThreadChatProps) => { - const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(); - const [filesToUpload, setFilesToUpload] = useState([]); + const chat = useChat(); + + if (!chat) { + throw new Error('No ChatContext provided'); + } const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel'); @@ -50,7 +53,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { closeTab(); }, [closeTab]); - const chat = useChat(); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat?.threadUploads); const handleNavigateToPreviousMessage = useCallback((): void => { chat?.messageEditing.toPreviousMessage(); @@ -62,9 +65,9 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { const handleUploadFiles = useCallback( (files: readonly File[]): void => { - chat?.flows.uploadFiles(files); + chat?.flows.uploadFiles({ files, uploadsStore: chat.threadUploads }); }, - [chat?.flows], + [chat?.flows, chat.threadUploads], ); const room = useRoom(); @@ -95,7 +98,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { return ( - + { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - setFilesToUpload={setFilesToUpload} - filesToUpload={filesToUpload} tshow={sendToChannel} > From 2d7453ef960eacc04a9e2f261cd97e60d2c6513b Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 12:01:43 -0300 Subject: [PATCH 130/215] chore: code cleanup --- .../client/lib/chats/flows/uploadFiles.ts | 151 +----------------- .../lib/chats/uploadFilesAndGetIdsandURL.ts | 143 ----------------- apps/meteor/client/lib/chats/uploads.ts | 1 - .../client/views/room/body/RoomBody.tsx | 5 - .../client/views/room/body/RoomBodyV2.tsx | 15 +- .../views/room/body/hooks/useFileUpload.ts | 2 - .../room/composer/messageBox/MessageBox.tsx | 9 -- 7 files changed, 5 insertions(+), 321 deletions(-) delete mode 100644 apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 0346a2d91b41d..b6a5d165129f3 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,5 +1,4 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; -// import { isRoomFederated } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; import { settings } from '../../../../app/settings/client'; @@ -57,12 +56,11 @@ export const uploadFiles = async ( getContent, fileContent, ); - // chat.composer?.clear(); - // imperativeModal.close(); + uploadNextFile(); }; - const handleSubmitFile = async (file: File, fileName: string, description?: string): Promise => { + const submitToUpload = async (file: File, fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, @@ -198,150 +196,7 @@ export const uploadFiles = async ( return; } - handleSubmitFile(file, file.name, chat.composer?.text ?? ''); - - // imperativeModal.open({ - // component: FileUploadModal, - // props: { - // file, - // fileName: file.name, - // fileDescription: chat.composer?.text ?? '', - // showDescription: room && !isRoomFederated(room), - // onClose: (): void => { - // imperativeModal.close(); - // uploadNextFile(); - // }, - // onSubmit: async (fileName: string, description?: string): Promise => { - // Object.defineProperty(file, 'name', { - // writable: true, - // value: fileName, - // }); - - // // encrypt attachment description - // const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - // if (!e2eRoom) { - // uploadFile(file, { description }); - // return; - // } - - // if (!settings.get('E2E_Enable_Encrypt_Files')) { - // uploadFile(file, { description }); - // return; - // } - - // const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - // if (!shouldConvertSentMessages) { - // uploadFile(file, { description }); - // return; - // } - - // const encryptedFile = await e2eRoom.encryptFile(file); - - // if (encryptedFile) { - // const getContent = async (filesId: string[], filesUrl: string[]): Promise => { - // const attachments = []; - // const _id = filesId[0]; - // const fileUrl = filesUrl[0]; - - // const attachment: FileAttachmentProps = { - // title: file.name, - // type: 'file', - // description, - // title_link: fileUrl, - // title_link_download: true, - // encryption: { - // key: encryptedFile.key, - // iv: encryptedFile.iv, - // }, - // hashes: { - // sha256: encryptedFile.hash, - // }, - // }; - - // if (/^image\/.+/.test(file.type)) { - // const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - - // attachments.push({ - // ...attachment, - // image_url: fileUrl, - // image_type: file.type, - // image_size: file.size, - // ...(dimensions && { - // image_dimensions: dimensions, - // }), - // }); - // } else if (/^audio\/.+/.test(file.type)) { - // attachments.push({ - // ...attachment, - // audio_url: fileUrl, - // audio_type: file.type, - // audio_size: file.size, - // }); - // } else if (/^video\/.+/.test(file.type)) { - // attachments.push({ - // ...attachment, - // video_url: fileUrl, - // video_type: file.type, - // video_size: file.size, - // }); - // } else { - // attachments.push({ - // ...attachment, - // size: file.size, - // format: getFileExtension(file.name), - // }); - // } - - // const files = [ - // { - // _id, - // name: file.name, - // type: file.type, - // size: file.size, - // // "format": "png" - // }, - // ]; - - // return e2eRoom.encryptMessageContent({ - // attachments, - // files, - // file: files[0], - // }); - // }; - - // const fileContentData = { - // type: file.type, - // typeGroup: file.type.split('/')[0], - // name: fileName, - // encryption: { - // key: encryptedFile.key, - // iv: encryptedFile.iv, - // }, - // hashes: { - // sha256: encryptedFile.hash, - // }, - // }; - - // const fileContent = { - // raw: fileContentData, - // encrypted: await e2eRoom.encryptMessageContent(fileContentData), - // }; - - // uploadFile( - // encryptedFile.file, - // { - // t: 'e2e', - // }, - // getContent, - // fileContent, - // ); - // } - // }, - // invalidContentType: !fileUploadIsValidContentType(file?.type), - // }, - // }); + submitToUpload(file, file.name, chat.composer?.text ?? ''); }; uploadNextFile(); diff --git a/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts b/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts deleted file mode 100644 index eadf9072b058f..0000000000000 --- a/apps/meteor/client/lib/chats/uploadFilesAndGetIdsandURL.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { IUpload } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; - -import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { getErrorMessage } from '../errorHandling'; -import type { Upload } from './Upload'; - -let uploads: readonly Upload[] = []; - -const emitter = new Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }>(); - -const updateUploads = (update: (uploads: readonly Upload[]) => readonly Upload[]): void => { - uploads = update(uploads); - emitter.emit('update'); -}; -export const uploadAndGetIds = async ( - file: File, - { - rid, - tmid, - fileContent, - }: { - rid: string; - tmid?: string; - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }; - }, -): Promise<{ fileId: string; fileUrl: string }> => { - const id = Random.id(); - - updateUploads((uploads) => [ - ...uploads, - { - id, - name: fileContent?.raw.name || file.name, - percentage: 0, - }, - ]); - - try { - const result = await new Promise<{ fileId: string; fileUrl: string }>((resolve, reject) => { - const xhr = sdk.rest.upload( - `/v1/rooms.media/${rid}`, - { - file, - ...(fileContent && { - content: JSON.stringify(fileContent.encrypted), - }), - }, - { - load: (event) => { - console.log('from uploadfiles event', event); - }, - progress: (event) => { - if (!event.lengthComputable) { - return; - } - const progress = (event.loaded / event.total) * 100; - if (progress === 100) { - return; - } - - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: Math.round(progress) || 0, - }; - }), - ); - }, - error: (event) => { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(xhr.responseText), - }; - }), - ); - reject(event); - }, - }, - ); - - xhr.onload = () => { - if (xhr.readyState === xhr.DONE && xhr.status === 200) { - const response = JSON.parse(xhr.responseText); - - resolve({ - fileId: response.file._id, - fileUrl: response.file.url, - }); - } else { - reject(new Error('File upload failed.')); - } - }; - - if (uploads.length) { - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - - emitter.once(`cancelling-${id}`, () => { - xhr.abort(); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - reject(new Error('Upload cancelled.')); - }); - }); - - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - - return result; - } catch (error: unknown) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), - }; - }), - ); - throw error; - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } - } -}; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 3f02a1d8fd4f8..8fb5ce299ca9c 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -202,7 +202,6 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' reject(new Error('Upload cancelled')); }); }); - // updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); } catch (error: unknown) { this.set( this.uploads.map((upload) => { diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index c2ef36c17f400..1563de57598d5 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -30,7 +30,6 @@ import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; -// import { useFileUpload } from './hooks/useFileUpload'; import { useFileUploadDropTarget } from './hooks/useFileUploadDropTarget'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; @@ -291,10 +290,6 @@ const RoomBody = (): ReactElement => { onResize={handleComposerResize} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - // onUploadFiles={handleUploadFiles} - // uploads={uploads} - // isUploading={isUploading} - // hasUploads={hasUploads} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 0ebdca2fe0f98..ff283444ab4ab 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -28,7 +28,7 @@ import RoomForeword from './RoomForeword/RoomForeword'; import { RoomTopic } from './RoomTopic'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import { useBannerSection } from './hooks/useBannerSection'; -import { useFileUpload } from './hooks/useFileUpload'; +import { useFileUploadDropTarget } from './hooks/useFileUploadDropTarget'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; @@ -101,14 +101,7 @@ const RoomBody = (): ReactElement => { const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); - const { - uploads, - isUploading, - hasUploads, - handleUploadFiles, - // handleUploadProgressClose, - targetDrop: [fileUploadTriggerProps, fileUploadOverlayProps], - } = useFileUpload(); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.uploads); const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); @@ -274,10 +267,6 @@ const RoomBody = (): ReactElement => { onResize={handleComposerResize} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - onUploadFiles={handleUploadFiles} - uploads={uploads} - isUploading={isUploading} - hasUploads={hasUploads} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index 443fd229b6233..6c6a01832f0b2 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'; -// import { useFileUploadDropTarget } from './useFileUploadDropTarget'; import type { UploadsAPI } from '../../../../lib/chats/ChatAPI'; import type { Upload } from '../../../../lib/chats/Upload'; import { useChat } from '../../contexts/ChatContext'; @@ -41,7 +40,6 @@ export const useFileUpload = (store: UploadsAPI) => { isUploading, handleUploadProgressClose, handleUploadFiles, - // targetDrop, }), [uploads, isUploading, handleUploadProgressClose, handleUploadFiles], ); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 88dfd3ea87850..c7981bfc5aa59 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -16,7 +16,6 @@ import { useMutation } from '@tanstack/react-query'; import type { ReactElement, FormEvent, MouseEvent, ClipboardEvent } from 'react'; import { memo, useRef, useReducer, useCallback, useSyncExternalStore } from 'react'; -// import { handleSendFiles } from './HandleFileUploads'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxHint from './MessageBoxHint'; @@ -87,15 +86,11 @@ type MessageBoxProps = { onEscape?: () => void; onNavigateToPreviousMessage?: () => void; onNavigateToNextMessage?: () => void; - // onUploadFiles: (files: readonly File[]) => void; tshow?: IMessage['tshow']; previewUrls?: string[]; subscription?: ISubscription; showFormattingTips: boolean; isEmbedded?: boolean; - // uploads: readonly Upload[]; - // isUploading: boolean; - // hasUploads: boolean; }; const MessageBox = ({ @@ -104,14 +99,10 @@ const MessageBox = ({ onJoin, onNavigateToNextMessage, onNavigateToPreviousMessage, - // onUploadFiles, onEscape, onTyping, tshow, previewUrls, - // uploads, - // isUploading, - // hasUploads, }: MessageBoxProps): ReactElement => { const chat = useChat(); const room = useRoom(); From eca7363d2cf903372d0717916ad2276803d51710 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 12:15:08 -0300 Subject: [PATCH 131/215] fix: remove `handleUploadFiles` from ThreadChat` --- .../contextualBar/Threads/components/ThreadChat.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) 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 3556b9b5af76b..6b0f40caf5726 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -53,7 +53,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { closeTab(); }, [closeTab]); - const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat?.threadUploads); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.threadUploads); const handleNavigateToPreviousMessage = useCallback((): void => { chat?.messageEditing.toPreviousMessage(); @@ -63,13 +63,6 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { chat?.messageEditing.toNextMessage(); }, [chat?.messageEditing]); - const handleUploadFiles = useCallback( - (files: readonly File[]): void => { - chat?.flows.uploadFiles({ files, uploadsStore: chat.threadUploads }); - }, - [chat?.flows, chat.threadUploads], - ); - const room = useRoom(); const readThreads = useMethod('readThreads'); useEffect(() => { @@ -121,7 +114,6 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { onEscape={handleComposerEscape} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - onUploadFiles={handleUploadFiles} tshow={sendToChannel} > From 6f69c6cedfada03282261c70874d7580b2a9ff35 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 14:46:35 -0300 Subject: [PATCH 132/215] fix: webdav upload flow --- .../MessageBoxActionsToolbar.tsx | 2 +- .../hooks/useWebdavActions.tsx | 8 +++----- .../FileUploadModal/FileUploadModal.stories.tsx | 2 -- .../WebdavFilePickerModal.tsx | 15 ++------------- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index be7c6c1e2470f..da91f27a75610 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -61,7 +61,7 @@ const MessageBoxActionsToolbar = ({ const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, uploadsStore); - const webdavActions = useWebdavActions(); + const webdavActions = useWebdavActions(uploadsStore); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index 0df740e38afc6..419895be69965 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx @@ -4,11 +4,12 @@ import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; import { useWebDAVAccountIntegrationsQuery } from '../../../../../../hooks/webdav/useWebDAVAccountIntegrationsQuery'; +import type { UploadsAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal'; import WebdavFilePickerModal from '../../../../webdav/WebdavFilePickerModal'; -export const useWebdavActions = (): GenericMenuItemProps[] => { +export const useWebdavActions = (uploadsStore: UploadsAPI): GenericMenuItemProps[] => { const enabled = useSetting('Webdav_Integration_Enabled', false); const { isSuccess, data } = useWebDAVAccountIntegrationsQuery({ enabled }); @@ -19,10 +20,7 @@ export const useWebdavActions = (): GenericMenuItemProps[] => { const setModal = useSetModal(); const handleAddWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); - const handleUpload = async (file: File, description?: string) => - chat?.uploads.send(file, { - description, - }); + const handleUpload = async (file: File) => chat?.flows.uploadFiles({ files: [file], uploadsStore }); const handleOpenWebdav = (account: IWebdavAccountIntegration) => setModal( setModal(null)} />); diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx index bb4eb6fa0df55..b3f9f7bd05bbf 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx @@ -12,8 +12,6 @@ export default { args: { file: new File(['The lazy brown fox jumped over the lazy brown fox.'], 'test.txt', { type: 'text/plain' }), fileName: 'test.txt', - fileDescription: '', - invalidContentType: false, }, } satisfies Meta; diff --git a/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx b/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx index 3d8cd4f38983b..dfcbb5470f9c5 100644 --- a/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx +++ b/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx @@ -2,7 +2,7 @@ import type { IWebdavNode, IWebdavAccountIntegration } from '@rocket.chat/core-t import type { SelectOption } from '@rocket.chat/fuselage'; import { Modal, Box, IconButton, Select } from '@rocket.chat/fuselage'; import { useEffectEvent, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useMethod, useToastMessageDispatch, useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; +import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, MouseEvent } from 'react'; import { useState, useEffect, useCallback } from 'react'; @@ -10,10 +10,8 @@ import FilePickerBreadcrumbs from './FilePickerBreadcrumbs'; import WebdavFilePickerGrid from './WebdavFilePickerGrid'; import WebdavFilePickerTable from './WebdavFilePickerTable'; import { sortWebdavNodes } from './lib/sortWebdavNodes'; -import { fileUploadIsValidContentType } from '../../../../../app/utils/client'; import FilterByText from '../../../../components/FilterByText'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; -import FileUploadModal from '../../modals/FileUploadModal'; export type WebdavSortOptions = 'name' | 'size' | 'dataModified'; @@ -25,7 +23,6 @@ type WebdavFilePickerModalProps = { const WebdavFilePickerModal = ({ onUpload, onClose, account }: WebdavFilePickerModalProps): ReactElement => { const t = useTranslation(); - const setModal = useSetModal(); const getWebdavFilePreview = useMethod('getWebdavFilePreview'); const getWebdavFileList = useMethod('getWebdavFileList'); const getFileFromWebdav = useMethod('getFileFromWebdav'); @@ -147,15 +144,7 @@ const WebdavFilePickerModal = ({ onUpload, onClose, account }: WebdavFilePickerM const blob = new Blob([data]); const file = new File([blob], webdavNode.basename, { type: webdavNode.mime }); - setModal( - => uploadFile(file, description)} - file={file} - onClose={(): void => setModal(null)} - invalidContentType={Boolean(file.type && !fileUploadIsValidContentType(file.type))} - />, - ); + uploadFile(file); } catch (error) { return dispatchToastMessage({ type: 'error', message: error }); } From dd2839a81b1adb71c0798753b556bd4283c9413e Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 15:59:17 -0300 Subject: [PATCH 133/215] fix: unnecessary store clear --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 5 ----- apps/meteor/client/views/room/body/DropTargetOverlay.tsx | 1 + apps/meteor/client/views/room/composer/ComposerMessage.tsx | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 16f6319f86c2f..61230999428b3 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -247,10 +247,5 @@ export class ChatMessages implements ChatAPI { } this.composer?.clear(); } - - // reset uploads when release composer - if (this.uploads.get().length > 0) { - this.uploads.clear(); - } } } diff --git a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx index d429b46f4f45c..761f3b0e3712a 100644 --- a/apps/meteor/client/views/room/body/DropTargetOverlay.tsx +++ b/apps/meteor/client/views/room/body/DropTargetOverlay.tsx @@ -55,6 +55,7 @@ function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDism } } } + onFileDrop?.(files); }); diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 3c21479dba868..3aa4fb2079cbf 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -84,6 +84,7 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac const publicationReady = useReactiveValue( useCallback(() => LegacyRoomManager.getOpenedRoomByRid(room._id)?.streamActive ?? false, [room._id]), ); + if (!publicationReady) { return ; } From 7f34a38850cb68022d6702304cf61bef90aeccd2 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 16:22:41 -0300 Subject: [PATCH 134/215] fix: `rooms.mediaEdit` review --- apps/meteor/app/api/server/v1/rooms.ts | 15 ++++++++++----- packages/rest-typings/src/v1/rooms.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index f264ac43f7f1c..fea13a6f4799c 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -13,6 +13,7 @@ import { isRoomsOpenProps, isRoomsMembersOrderedByRoleProps, isRoomsHideProps, + isMediaEditProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -335,24 +336,28 @@ API.v1.addRoute( API.v1.addRoute( 'rooms.mediaEdit/:rid/:fileId', - { authRequired: true }, + { authRequired: true, validateParams: isMediaEditProps }, { async post() { if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.forbidden(); } + if (!this.bodyParams.fileName) { + throw new Meteor.Error('invalid-file-name'); + } + const file = await Uploads.findOneById(this.urlParams.fileId); if (!file) { throw new Meteor.Error('invalid-file'); } - if (!this.bodyParams.fileName) { - throw new Meteor.Error('invalid-file-name'); - } + const { matchedCount } = await Uploads.updateFileNameById(this.urlParams.fileId, this.bodyParams.fileName); - await Uploads.updateFileNameById(this.urlParams.fileId, this.bodyParams.fileName); + if (matchedCount === 0) { + throw new Meteor.Error('invalid-file'); + } return API.v1.success(); }, diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index e3fbadb5fc84a..6cab17a356e74 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -688,6 +688,19 @@ const roomsHideSchema = { export const isRoomsHideProps = ajv.compile(roomsHideSchema); +type RoomsMediaEditProps = { + fileName: string; +}; + +const mediaEditSchema = { + type: 'object', + properties: { fileName: { type: 'string' } }, + required: ['fileName'], + additionalProperties: false, +}; + +export const isMediaEditProps = ajv.compile(mediaEditSchema); + export type RoomsEndpoints = { '/v1/rooms.autocomplete.channelAndPrivate': { GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => { From b1796d4492870a6c50de77ff91687dc5195c1174 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 16:28:25 -0300 Subject: [PATCH 135/215] chore: remove unnecessary file --- .../composer/messageBox/HandleFileUploads.ts | 192 ------------------ 1 file changed, 192 deletions(-) delete mode 100644 apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts diff --git a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts b/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts deleted file mode 100644 index 5cff06239d33e..0000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/HandleFileUploads.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { IMessage, IUpload, IE2EEMessage, FileAttachmentProps } from '@rocket.chat/core-typings'; - -import { e2e } from '../../../../../app/e2e/client'; -import { getFileExtension } from '../../../../../lib/utils/getFileExtension'; -import { prependReplies } from '../../../../lib/utils/prependReplies'; - -const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ - height: img.height, - width: img.width, - }); - }; - img.src = dataURL; - }); -}; -const uploadFile = ( - file: File[] | File, - chat: any, - extraData?: Pick & { msg?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - setFilesToUpload?: (files: File[]) => void, -) => { - if (!chat) { - console.error('Chat context not found'); - return; - } - chat.uploads.send( - file, - { - ...extraData, - }, - getContent, - fileContent, - ); - chat.composer?.clear(); - setFilesToUpload?.([]); -}; - -const handleEncryptedFilesShared = async ( - filesToUpload: File[], - chat: any, - msg: string, - e2eRoom: any, - setFilesToUpload?: (files: File[]) => void, -) => { - const encryptedFilesarray: any = await Promise.all(filesToUpload.map((file) => e2eRoom.encryptFile(file))); - - const filesarray = encryptedFilesarray.map((file: any) => file?.file); - - const imgDimensions = await Promise.all( - filesToUpload.map((file) => { - if (/^image\/.+/.test(file.type)) { - return getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - } - return null; - }), - ); - - if (encryptedFilesarray[0]) { - const getContent = async (_id: string[], fileUrl: string[]): Promise => { - const attachments = []; - const arrayoffiles = []; - for (let i = 0; i < _id.length; i++) { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].name, - type: 'file', - title_link: fileUrl[i], - title_link_download: true, - encryption: { - key: encryptedFilesarray[i].key, - iv: encryptedFilesarray[i].iv, - }, - hashes: { - sha256: encryptedFilesarray[i].hash, - }, - }; - - if (/^image\/.+/.test(filesToUpload[i].type)) { - const dimensions = imgDimensions[i]; - attachments.push({ - ...attachment, - image_url: fileUrl[i], - image_type: filesToUpload[i].type, - image_size: filesToUpload[i].size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl[i], - audio_type: filesToUpload[i].type, - audio_size: filesToUpload[i].size, - }); - } else if (/^video\/.+/.test(filesToUpload[i].type)) { - attachments.push({ - ...attachment, - video_url: fileUrl[i], - video_type: filesToUpload[i].type, - video_size: filesToUpload[i].size, - }); - } else { - attachments.push({ - ...attachment, - size: filesToUpload[i].size, - format: getFileExtension(filesToUpload[i].name), - }); - } - - const files = { - _id: _id[i], - name: filesToUpload[i].name, - type: filesToUpload[i].type, - size: filesToUpload[i].size, - }; - arrayoffiles.push(files); - } - - return e2eRoom.encryptMessageContent({ - attachments, - files: arrayoffiles, - file: filesToUpload[0], - msg, - }); - }; - - const fileContentData = { - type: filesToUpload[0].type, - typeGroup: filesToUpload[0].type.split('/')[0], - name: filesToUpload[0].name, - encryption: { - key: encryptedFilesarray[0].key, - iv: encryptedFilesarray[0].iv, - }, - hashes: { - sha256: encryptedFilesarray[0].hash, - }, - }; - - const fileContent = { - raw: fileContentData, - encrypted: await e2eRoom.encryptMessageContent(fileContentData), - }; - uploadFile( - filesarray, - chat, - { - t: 'e2e', - }, - getContent, - fileContent, - setFilesToUpload, - ); - } -}; -export const handleSendFiles = async (filesToUpload: File[], chat: any, room: any, setFilesToUpload?: (files: File[]) => void) => { - if (!chat || !room) { - return; - } - const replies = chat.composer?.quotedMessages.get() ?? []; - const msg = await prependReplies(chat.composer?.text || '', replies); - - filesToUpload.forEach((file) => { - Object.defineProperty(file, 'name', { - writable: true, - value: file.name, - }); - }); - - const e2eRoom = await e2e.getInstanceByRoomId(room._id); - - if (!e2eRoom) { - uploadFile(filesToUpload, chat, { msg }); - setFilesToUpload?.([]); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { - uploadFile(filesToUpload, chat, { msg }); - setFilesToUpload?.([]); - return; - } - handleEncryptedFilesShared(filesToUpload, chat, msg, e2eRoom, setFilesToUpload); - chat.composer?.clear(); -}; From 872f0c632a1db3d586595030115fcd026ef3e2bc Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 18 Feb 2025 16:37:39 -0300 Subject: [PATCH 136/215] Fix some of my reviews --- .../server/methods/sendFileMessage.ts | 37 +++++++++---------- .../app/lib/server/functions/sendMessage.ts | 21 ++++------- .../src/models/IBaseUploadsModel.ts | 4 ++ packages/models/src/models/BaseUploadModel.ts | 31 ++++++++++++++++ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 6863ab5220306..5ce1fc08b11ae 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -23,43 +23,40 @@ import { FileUpload } from '../lib/FileUpload'; function validateFileRequiredFields(file: Partial): asserts file is AtLeast { const requiredFields = ['_id', 'name', 'type', 'size']; - requiredFields.forEach((field) => { + for (const field of requiredFields) { if (!Object.keys(file).includes(field)) { throw new Meteor.Error('error-invalid-file', 'Invalid file'); } - }); + } } +type parseMultipleFilesIntoMessageAttachmentsResult = { files: FileProp[]; attachments: MessageAttachment[] }; export const parseMultipleFilesIntoMessageAttachments = async ( filesToConfirm: Partial[], roomId: string, user: IUser, -): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { - const messageFiles: FileProp[] = []; - const messageAttachments: MessageAttachment[] = []; +): Promise => { + const result: parseMultipleFilesIntoMessageAttachmentsResult = { files: [], attachments: [] }; + + await Promise.await( + filesToConfirm.reduce Promise>>((acc, file) => { + if (!file) return acc; - const filesAndAttachments = await Promise.all( - filesToConfirm - .filter((files: Partial) => !!files) - .map(async (file) => { + acc.push(async () => { try { const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - return { files, attachments }; + result.files.push(...files); + result.attachments.push(...attachments); } catch (error) { console.error('Error processing file:', file, error); - return { files: [], attachments: [] }; } - }), - ); + }); - filesAndAttachments - .filter(({ files, attachments }) => files.length || attachments.length) - .forEach(({ files, attachments }) => { - messageFiles.push(...files); - messageAttachments.push(...attachments); - }); + return acc; + }, []), + ); - return { files: messageFiles, attachments: messageAttachments }; + return result; }; export const parseFileIntoMessageAttachments = async ( diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 4772726b63d95..3305f5a99b0e1 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUpload } from '@rocket.chat/core-typings'; +import { isE2EEMessage, type IMessage, type IRoom, type IUpload } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -216,29 +216,22 @@ export function prepareMessageObject( /** * Validates and sends the message object. */ -export const sendMessage = async function ( +export const sendMessage = async ( user: any, message: any, room: any, upsert = false, previewUrls?: string[], uploadIdsToConfirm?: string[], -) { +) => { if (!user || !message || !room._id) { return false; } if (uploadIdsToConfirm !== undefined) { - const filesToConfirm: Partial[] = await Promise.all( - uploadIdsToConfirm.map(async (fileid) => { - const file = await Uploads.findOneById(fileid); - if (!file) { - throw new Meteor.Error('invalid-file'); - } - return file; - }), - ); - if (message?.t !== 'e2e') { + const filesToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm); + + if (isE2EEMessage(message)) { const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); message.files = files; message.attachments = attachments; @@ -318,7 +311,7 @@ export const sendMessage = async function ( await afterSaveMessage(message, room); if (uploadIdsToConfirm !== undefined) { - await Promise.all(uploadIdsToConfirm.map((fileid) => Uploads.confirmTemporaryFile(fileid, user._id))); + await Uploads.confirmTemporaryFiles(uploadIdsToConfirm, user._id); } void notifyOnMessageChange({ id: message._id }); diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 12db0ee25d6b6..d41cb9aa4bd11 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -10,6 +10,10 @@ export interface IBaseUploadsModel extends IBaseModel { confirmTemporaryFile(fileId: string, userId: string): Promise | undefined; + confirmTemporaryFiles(fileIds: string[], userId: string): Promise | undefined; + + findByIds(_id: string[], options?: FindOptions): FindCursor; + findOneByName(name: string): Promise; findOneByRoomId(rid: string): Promise; diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index 4037566272b9f..f4ef4ff3c415b 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -91,6 +91,37 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } + confirmTemporaryFiles(fileIds: string[], userId: string): Promise | undefined { + if (!fileIds.length) { + return; + } + + const filter = { + _id: { + $in: fileIds, + }, + userId, + }; + + const update: Filter = { + $unset: { + expiresAt: 1, + }, + }; + + return this.updateMany(filter, update); + } + + findByIds(_id: string[], options?: FindOptions): FindCursor { + const query = { + _id: { + $in: _id, + }, + }; + + return this.find(query, options); + } + async findOneByName(name: string): Promise { return this.findOne({ name }); } From da5dcf8023b826f21b5ff026a714533d66f7996b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 18 Feb 2025 17:03:57 -0300 Subject: [PATCH 137/215] Fix last changes --- .../server/methods/sendFileMessage.ts | 24 ++++++++++--------- .../app/lib/server/functions/sendMessage.ts | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5ce1fc08b11ae..c53a6692099a1 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -38,19 +38,21 @@ export const parseMultipleFilesIntoMessageAttachments = async ( ): Promise => { const result: parseMultipleFilesIntoMessageAttachmentsResult = { files: [], attachments: [] }; - await Promise.await( - filesToConfirm.reduce Promise>>((acc, file) => { + await Promise.all( + filesToConfirm.reduce>>((acc, file) => { if (!file) return acc; - acc.push(async () => { - try { - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - result.files.push(...files); - result.attachments.push(...attachments); - } catch (error) { - console.error('Error processing file:', file, error); - } - }); + acc.push( + (async () => { + try { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + result.files.push(...files); + result.attachments.push(...attachments); + } catch (error) { + console.error('Error processing file:', file, error); + } + })(), + ); return acc; }, []), diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 3305f5a99b0e1..d669ef647e09a 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -229,9 +229,9 @@ export const sendMessage = async ( } if (uploadIdsToConfirm !== undefined) { - const filesToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm); + const filesToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm).toArray(); - if (isE2EEMessage(message)) { + if (!isE2EEMessage(message)) { const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); message.files = files; message.attachments = attachments; From 4b98e47962e069cffcb76d537502bb642790ce38 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 19:58:08 -0300 Subject: [PATCH 138/215] fix: some `rooms.mediaEdit` suggestions --- apps/meteor/app/api/server/v1/rooms.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index fea13a6f4799c..8ba38b92a5d1c 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -339,24 +339,26 @@ API.v1.addRoute( { authRequired: true, validateParams: isMediaEditProps }, { async post() { - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { - return API.v1.forbidden(); + const { fileName } = this.bodyParams; + + if (fileName) { + throw new Error('invalid-file-name'); } - if (!this.bodyParams.fileName) { - throw new Meteor.Error('invalid-file-name'); + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.forbidden(); } const file = await Uploads.findOneById(this.urlParams.fileId); if (!file) { - throw new Meteor.Error('invalid-file'); + throw new Error('invalid-file'); } - const { matchedCount } = await Uploads.updateFileNameById(this.urlParams.fileId, this.bodyParams.fileName); + const { matchedCount } = await Uploads.updateFileNameById(this.urlParams.fileId, fileName); if (matchedCount === 0) { - throw new Meteor.Error('invalid-file'); + throw new Error('invalid-file'); } return API.v1.success(); From 6b8ab8efa22ffeae8d5e63ffe3726e19797a9f8d Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 18 Feb 2025 20:05:52 -0300 Subject: [PATCH 139/215] fix: remove props from sendMessage --- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 801e38499b447..e15ef7a87e557 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -145,8 +145,6 @@ Meteor.methods({ federation: Match.Maybe(Object), groupable: Match.Maybe(Boolean), sentByEmail: Match.Maybe(Boolean), - u: Match.Maybe(Object), - _updatedAt: Match.Maybe(Date), }); const uid = Meteor.userId(); From c21b0373a5a0c19afdcb3cb2c1d34f88ab4efd26 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 19 Feb 2025 11:20:15 -0300 Subject: [PATCH 140/215] fix: remove description param from `uploadFile` --- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 11 +++++------ apps/meteor/client/lib/chats/uploads.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 93d6893ed3472..b77b61afe49e1 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -107,7 +107,7 @@ export type UploadsAPI = { file: File, { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ): Promise; }; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index b6a5d165129f3..175eea1b3abfb 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -60,7 +60,7 @@ export const uploadFiles = async ( uploadNextFile(); }; - const submitToUpload = async (file: File, fileName: string, description?: string): Promise => { + const submitToUpload = async (file: File, fileName: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, @@ -70,19 +70,19 @@ export const uploadFiles = async ( const e2eRoom = await e2e.getInstanceByRoomId(room._id); if (!e2eRoom) { - uploadFile(file, { description }); + uploadFile(file); return; } if (!settings.get('E2E_Enable_Encrypt_Files')) { - uploadFile(file, { description }); + uploadFile(file); return; } const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); if (!shouldConvertSentMessages) { - uploadFile(file, { description }); + uploadFile(file); return; } @@ -97,7 +97,6 @@ export const uploadFiles = async ( const attachment: FileAttachmentProps = { title: file.name, type: 'file', - description, title_link: fileUrl, title_link_download: true, encryption: { @@ -196,7 +195,7 @@ export const uploadFiles = async ( return; } - submitToUpload(file, file.name, chat.composer?.text ?? ''); + submitToUpload(file, file.name); }; uploadNextFile(); diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 8fb5ce299ca9c..abad932b6d1da 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -84,10 +84,9 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' send = async ( file: File, { - description, msg, - // rid, - // tmid, + rid, + tmid, t, }: { description?: string; @@ -196,6 +195,10 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' } }; + if (this.uploads.length) { + UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + } + this.once(`cancelling-${id}`, () => { xhr.abort(); this.set(this.uploads.filter((upload) => upload.id !== id)); From a003a4f91e3f1c4f8d5b4be65369dc6895bbf4a4 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 19 Feb 2025 14:11:24 -0300 Subject: [PATCH 141/215] fix: `rooms.mediaEdit` fileName param --- apps/meteor/app/api/server/v1/rooms.ts | 2 +- packages/rest-typings/src/v1/rooms.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 8ba38b92a5d1c..f335f8ede56ff 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -341,7 +341,7 @@ API.v1.addRoute( async post() { const { fileName } = this.bodyParams; - if (fileName) { + if (!fileName) { throw new Error('invalid-file-name'); } diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 6cab17a356e74..c576af4c51b63 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -806,7 +806,7 @@ export type RoomsEndpoints = { }; '/v1/rooms.mediaEdit/:rid/:fileId': { - POST: (params: { fileName?: string }) => void; + POST: (params: { fileName: string }) => void; }; '/v1/rooms.saveNotification': { From 40189e279775ad7628ecf24e6950f67dfbeda311 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Feb 2025 18:29:23 -0300 Subject: [PATCH 142/215] Fix some reviews --- apps/meteor/app/api/server/v1/rooms.ts | 10 ---------- .../app/file-upload/server/methods/sendFileMessage.ts | 7 ++++++- apps/meteor/app/lib/server/functions/sendMessage.ts | 11 ++++------- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 ++ apps/meteor/client/lib/chats/flows/uploadFiles.ts | 5 +++-- apps/meteor/client/lib/constants.ts | 1 + apps/meteor/client/providers/ImageGalleryProvider.tsx | 4 ++-- .../model-typings/src/models/IBaseUploadsModel.ts | 2 +- packages/models/src/models/BaseUploadModel.ts | 4 ++-- 9 files changed, 21 insertions(+), 25 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index f335f8ede56ff..6f9a293f7dc8d 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -341,20 +341,10 @@ API.v1.addRoute( async post() { const { fileName } = this.bodyParams; - if (!fileName) { - throw new Error('invalid-file-name'); - } - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { return API.v1.forbidden(); } - const file = await Uploads.findOneById(this.urlParams.fileId); - - if (!file) { - throw new Error('invalid-file'); - } - const { matchedCount } = await Uploads.updateFileNameById(this.urlParams.fileId, fileName); if (matchedCount === 0) { diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index c53a6692099a1..48b42207d0fd0 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -9,6 +9,7 @@ import type { FileProp, } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { Logger } from '@rocket.chat/logger'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -30,6 +31,8 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL } } +const logger = new Logger('sendFileMessage'); + type parseMultipleFilesIntoMessageAttachmentsResult = { files: FileProp[]; attachments: MessageAttachment[] }; export const parseMultipleFilesIntoMessageAttachments = async ( filesToConfirm: Partial[], @@ -49,7 +52,9 @@ export const parseMultipleFilesIntoMessageAttachments = async ( result.files.push(...files); result.attachments.push(...attachments); } catch (error) { - console.error('Error processing file:', file, error); + // Not an important error, it should not happen and if it happens wil affect the attachment + // preview in the message object only + logger.warn('Error processing file:', file, error); } })(), ); diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index d669ef647e09a..3439b37c24a8c 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -228,14 +228,11 @@ export const sendMessage = async ( return false; } - if (uploadIdsToConfirm !== undefined) { + if (uploadIdsToConfirm !== undefined && !isE2EEMessage(message)) { const filesToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm).toArray(); - - if (!isE2EEMessage(message)) { - const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); - message.files = files; - message.attachments = attachments; - } + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + message.files = files; + message.attachments = attachments; } await validateMessage(message, room, user); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index e15ef7a87e557..4e60ff8a67ba6 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -147,6 +147,8 @@ Meteor.methods({ sentByEmail: Match.Maybe(Boolean), }); + check(uploadIdsToConfirm, Match.Maybe([String])); + const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 175eea1b3abfb..c5cbbbb5a3290 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -4,6 +4,7 @@ import { e2e } from '../../../../app/e2e/client'; import { settings } from '../../../../app/settings/client'; import { t } from '../../../../app/utils/lib/i18n'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; +import { MAX_MULTIPLE_UPLOADED_FILES } from '../../constants'; import { dispatchToastMessage } from '../../toast'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI, UploadsAPI } from '../ChatAPI'; @@ -26,7 +27,7 @@ export const uploadFiles = async ( { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, ): Promise => { // TODO: calculate max files based on the new array and the files in the queue - if (uploadsStore.get().length > 10) { + if (uploadsStore.get().length > MAX_MULTIPLE_UPLOADED_FILES) { return dispatchToastMessage({ type: 'error', message: t('You_cant_upload_more_than__count__files', { count: 10 }), @@ -148,7 +149,7 @@ export const uploadFiles = async ( name: file.name, type: file.type, size: file.size, - // "format": "png" + format: getFileExtension(file.name), }, ] as IMessage['files']; diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index 89052eea93acb..3b2f8c339d662 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -1,3 +1,4 @@ export const USER_STATUS_TEXT_MAX_LENGTH = 120; export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; +export const MAX_MULTIPLE_UPLOADED_FILES = 10; diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index eb163e4b20aaf..134405fae4aab 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -23,10 +23,10 @@ const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { return setSingleImageUrl(target.dataset.id); } if (target?.classList.contains('gallery-item')) { - const id1 = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; + const idFromSrc = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; - return setImageId(id1 || target.dataset.id || id); + return setImageId(idFromSrc || target.dataset.id || id); } if (target?.classList.contains('gallery-item-container')) { return setImageId(target.dataset.id); diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index d41cb9aa4bd11..dddf53b6bc3ab 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -12,7 +12,7 @@ export interface IBaseUploadsModel extends IBaseModel { confirmTemporaryFiles(fileIds: string[], userId: string): Promise | undefined; - findByIds(_id: string[], options?: FindOptions): FindCursor; + findByIds(_ids: string[], options?: FindOptions): FindCursor; findOneByName(name: string): Promise; diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index f4ef4ff3c415b..e8af63751dd3b 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -112,10 +112,10 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateMany(filter, update); } - findByIds(_id: string[], options?: FindOptions): FindCursor { + findByIds(_ids: string[], options?: FindOptions): FindCursor { const query = { _id: { - $in: _id, + $in: _ids, }, }; From 7ab58a22525f78ecf302ef6bb63eaef90afd7deb Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 20 Feb 2025 18:04:02 -0300 Subject: [PATCH 143/215] refactor: upload API Co-authored-by: gabriellsh --- .../app/e2e/client/rocketchat.e2e.room.ts | 13 +- apps/meteor/client/lib/chats/ChatAPI.ts | 16 +- apps/meteor/client/lib/chats/Upload.ts | 18 +- .../client/lib/chats/flows/confirmFiles.ts | 144 +++++++++++++++- .../client/lib/chats/flows/uploadFiles.ts | 158 +++--------------- apps/meteor/client/lib/chats/uploads.ts | 35 ++-- 6 files changed, 207 insertions(+), 177 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts index 830322505e0b1..423e479e13df1 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts @@ -27,6 +27,7 @@ import { import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; import { RoomManager } from '../../../client/lib/RoomManager'; +import type { EncryptedFile } from '../../../client/lib/chats/Upload'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; import { Rooms, Subscriptions, Messages } from '../../models/client'; @@ -187,7 +188,7 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.KEYS_RECEIVED); } - async shouldConvertSentMessages(message: { msg: string }) { + async readyToEncrypt() { if (!this.isReady() || this[PAUSED]) { return false; } @@ -198,6 +199,14 @@ export class E2ERoom extends Emitter { }); } + return true; + } + + async shouldConvertSentMessages(message: { msg: string }) { + if (!(await this.readyToEncrypt())) { + return false; + } + if (message.msg[0] === '/') { return false; } @@ -562,7 +571,7 @@ export class E2ERoom extends Emitter { } // Encrypts files before upload. I/O is in arraybuffers. - async encryptFile(file: File) { + async encryptFile(file: File): Promise { // if (!this.isSupportedRoomType(this.typeOfRoom)) { // return; // } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index b77b61afe49e1..de15760dbbf0f 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,7 +1,7 @@ import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; -import type { Upload } from './Upload'; +import type { Upload, EncryptedFile } from './Upload'; import type { ReadStateManager } from './readStateManager'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; import type { Subscribable } from '../../definitions/Subscribable'; @@ -95,6 +95,12 @@ export type DataAPI = { getSubscriptionFromMessage(message: IMessage): Promise; }; +export type EncryptedFileUploadContent = { + rawFile: File; + fileContent: { raw: Partial; encrypted?: IE2EEMessage['content'] | undefined }; + encryptedFile: EncryptedFile; +}; + export type UploadsAPI = { get(): readonly Upload[]; subscribe(callback: () => void): () => void; @@ -103,12 +109,8 @@ export type UploadsAPI = { cancel(id: Upload['id']): void; removeUpload(id: Upload['id']): void; editUploadFileName: (id: Upload['id'], fileName: string) => void; - send( - file: File, - { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, - ): Promise; + send(file: File, encrypted?: never): Promise; + send(file: File, encrypted: EncryptedFileUploadContent): Promise; }; export type ChatAPI = { diff --git a/apps/meteor/client/lib/chats/Upload.ts b/apps/meteor/client/lib/chats/Upload.ts index b32f0b662b1cf..720a9682dc1ac 100644 --- a/apps/meteor/client/lib/chats/Upload.ts +++ b/apps/meteor/client/lib/chats/Upload.ts @@ -1,7 +1,21 @@ -export type Upload = { +export type NonEncryptedUpload = { readonly id: string; readonly file: File; - readonly url: string; + readonly url?: string; readonly percentage: number; readonly error?: Error; }; + +export type EncryptedUpload = NonEncryptedUpload & { + readonly encryptedFile: EncryptedFile; +}; + +export type Upload = EncryptedUpload | NonEncryptedUpload; + +export type EncryptedFile = { + file: File; + key: JsonWebKey; + iv: string; + type: File['type']; + hash: string; +}; diff --git a/apps/meteor/client/lib/chats/flows/confirmFiles.ts b/apps/meteor/client/lib/chats/flows/confirmFiles.ts index 57d9c6831bd3c..4afd03cf6e7d5 100644 --- a/apps/meteor/client/lib/chats/flows/confirmFiles.ts +++ b/apps/meteor/client/lib/chats/flows/confirmFiles.ts @@ -1,18 +1,152 @@ +import type { AtLeast, FileAttachmentProps, IMessage } from '@rocket.chat/core-typings'; + +import { e2e } from '../../../../app/e2e/client'; +import type { E2ERoom } from '../../../../app/e2e/client/rocketchat.e2e.room'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; +import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import type { ChatAPI } from '../ChatAPI'; +import type { EncryptedUpload } from '../Upload'; + +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; + +const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2eRoom: E2ERoom, msg: string) => { + const attachments = []; + const arrayOfFiles = []; + + const imgDimensions = await Promise.all( + filesToUpload.map(({ file }) => { + if (/^image\/.+/.test(file.type)) { + return getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + } + return null; + }), + ); + + for (let i = 0; i < filesToUpload.length; i++) { + const attachment: FileAttachmentProps = { + title: filesToUpload[i].file.name, + type: 'file', + title_link: filesToUpload[i].url, + title_link_download: true, + encryption: { + key: filesToUpload[i].encryptedFile.key, + iv: filesToUpload[i].encryptedFile.iv, + }, + hashes: { + sha256: filesToUpload[i].encryptedFile.hash, + }, + }; + + if (/^image\/.+/.test(filesToUpload[i].file.type)) { + const dimensions = imgDimensions[i]; + attachments.push({ + ...attachment, + image_url: filesToUpload[i].url, + image_type: filesToUpload[i].file.type, + image_size: filesToUpload[i].file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(filesToUpload[i].file.type)) { + attachments.push({ + ...attachment, + audio_url: filesToUpload[i].url, + audio_type: filesToUpload[i].file.type, + audio_size: filesToUpload[i].file.size, + }); + } else if (/^video\/.+/.test(filesToUpload[i].file.type)) { + attachments.push({ + ...attachment, + video_url: filesToUpload[i].url, + video_type: filesToUpload[i].file.type, + video_size: filesToUpload[i].file.size, + }); + } else { + attachments.push({ + ...attachment, + size: filesToUpload[i].file.size, + format: getFileExtension(filesToUpload[i].file.name), + }); + } + + const files = { + _id: filesToUpload[i].id, + name: filesToUpload[i].file.name, + type: filesToUpload[i].file.type, + size: filesToUpload[i].file.size, + format: getFileExtension(filesToUpload[i].file.name), + }; + + arrayOfFiles.push(files); + } + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayOfFiles, + file: arrayOfFiles[0], + msg, + }); +}; export const confirmFiles = async (chat: ChatAPI): Promise => { + const room = await chat.data.getRoom(); + const e2eRoom = await e2e.getInstanceByRoomId(room._id); const replies = chat.composer?.quotedMessages.get() ?? []; - const msg = chat.composer?.text || ''; + const text = chat.composer?.text || ''; - const message = await chat.data.composeMessage(msg, { + const { msg, tmid, ...composedMessage } = await chat.data.composeMessage(text, { quotedMessages: replies, }); - const store = message?.tmid ? chat.threadUploads : chat.uploads; + const store = tmid ? chat.threadUploads : chat.uploads; + const filesToUpload = store.get(); + + if (filesToUpload.length === 0) { + return; + } + + const { fileUrls, fileIds } = filesToUpload.reduce<{ fileUrls: string[]; fileIds: string[] }>( + (acc, upload) => { + if (!upload.url || !upload.id) { + return acc; + } + + acc.fileIds.push(upload.id); + acc.fileUrls.push(upload.url); + + return acc; + }, + { fileUrls: [], fileIds: [] }, + ); + + const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg }); + + let content; + if (e2eRoom && shouldConvertSentMessages) { + content = await getEncryptedContent(filesToUpload as EncryptedUpload[], e2eRoom, msg); + } - const fileUrls = store.get().map((upload) => upload.url); - const fileIds = store.get().map((upload) => upload.id); + const message: AtLeast = { + ...composedMessage, + tmid, + msg, + content, + ...(e2eRoom && { + t: 'e2e', + }), + } as const; try { await sdk.call('sendMessage', message, fileUrls, fileIds); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index c5cbbbb5a3290..03eaaea3c42a7 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,26 +1,10 @@ -import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; - import { e2e } from '../../../../app/e2e/client'; import { settings } from '../../../../app/settings/client'; import { t } from '../../../../app/utils/lib/i18n'; -import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { MAX_MULTIPLE_UPLOADED_FILES } from '../../constants'; import { dispatchToastMessage } from '../../toast'; -import { prependReplies } from '../../utils/prependReplies'; -import type { ChatAPI, UploadsAPI } from '../ChatAPI'; - -const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ - height: img.height, - width: img.width, - }); - }; - img.src = dataURL; - }); -}; +// import { prependReplies } from '../../utils/prependReplies'; +import type { ChatAPI, UploadsAPI, EncryptedFileUploadContent } from '../ChatAPI'; export const uploadFiles = async ( chat: ChatAPI, @@ -34,55 +18,44 @@ export const uploadFiles = async ( }); } - const replies = chat.composer?.quotedMessages.get() ?? []; + // const replies = chat.composer?.quotedMessages.get() ?? []; - const msg = await prependReplies('', replies); + // const msg = await prependReplies('', replies); const room = await chat.data.getRoom(); const queue = [...files]; - const uploadFile = ( - file: File, - extraData?: Pick & { description?: string }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, - ) => { - uploadsStore.send( - file, - { - msg, - ...extraData, - }, - getContent, - fileContent, - ); + const uploadFile = (file: File, encrypted?: EncryptedFileUploadContent) => { + if (encrypted) { + uploadsStore.send(file, encrypted); + } else { + uploadsStore.send(file); + } uploadNextFile(); }; - const submitToUpload = async (file: File, fileName: string): Promise => { + const uploadNextFile = async (): Promise => { + const file = queue.pop(); + if (!file) { + chat.composer?.dismissAllQuotedMessages(); + return; + } + Object.defineProperty(file, 'name', { writable: true, - value: fileName, + value: file.name, }); - // encrypt attachment description const e2eRoom = await e2e.getInstanceByRoomId(room._id); - if (!e2eRoom) { + if (!e2eRoom || !settings.get('E2E_Enable_Encrypt_Files')) { uploadFile(file); return; } - if (!settings.get('E2E_Enable_Encrypt_Files')) { - uploadFile(file); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); - - if (!shouldConvertSentMessages) { + if (!(await e2eRoom.readyToEncrypt())) { uploadFile(file); return; } @@ -90,80 +63,10 @@ export const uploadFiles = async ( const encryptedFile = await e2eRoom.encryptFile(file); if (encryptedFile) { - const getContent = async (filesId: string[], filesUrl: string[]): Promise => { - const attachments = []; - const _id = filesId[0]; - const fileUrl = filesUrl[0]; - - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - title_link: fileUrl, - title_link_download: true, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - hashes: { - sha256: encryptedFile.hash, - }, - }; - - if (/^image\/.+/.test(file.type)) { - const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - - attachments.push({ - ...attachment, - image_url: fileUrl, - image_type: file.type, - image_size: file.size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - audio_url: fileUrl, - audio_type: file.type, - audio_size: file.size, - }); - } else if (/^video\/.+/.test(file.type)) { - attachments.push({ - ...attachment, - video_url: fileUrl, - video_type: file.type, - video_size: file.size, - }); - } else { - attachments.push({ - ...attachment, - size: file.size, - format: getFileExtension(file.name), - }); - } - - const files = [ - { - _id, - name: file.name, - type: file.type, - size: file.size, - format: getFileExtension(file.name), - }, - ] as IMessage['files']; - - return e2eRoom.encryptMessageContent({ - attachments, - files, - file: files?.[0], - }); - }; - const fileContentData = { type: file.type, typeGroup: file.type.split('/')[0], - name: fileName, + name: file.name, encryption: { key: encryptedFile.key, iv: encryptedFile.iv, @@ -178,25 +81,8 @@ export const uploadFiles = async ( encrypted: await e2eRoom.encryptMessageContent(fileContentData), }; - uploadFile( - encryptedFile.file, - { - t: 'e2e', - }, - getContent, - fileContent, - ); - } - }; - - const uploadNextFile = (): void => { - const file = queue.pop(); - if (!file) { - chat.composer?.dismissAllQuotedMessages(); - return; + uploadFile(encryptedFile.file, { rawFile: file, fileContent, encryptedFile }); } - - submitToUpload(file, file.name); }; uploadNextFile(); diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index abad932b6d1da..c012b9e3c1699 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,11 +1,11 @@ -import type { IMessage, IRoom, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; import fileSize from 'filesize'; import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; import { getErrorMessage } from '../errorHandling'; -import type { UploadsAPI } from './ChatAPI'; +import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI'; import type { Upload } from './Upload'; import { settings } from '../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../app/utils/client'; @@ -59,6 +59,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' return upload; } + // TODO reencrypt file return { ...upload, file: new File([upload.file], fileName, upload.file) }; }), ); @@ -81,23 +82,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' clear = () => this.set([]); - send = async ( - file: File, - { - msg, - rid, - tmid, - t, - }: { - description?: string; - msg?: string; - rid: string; - tmid?: string; - t?: IMessage['t']; - }, - getContent?: (fileId: string[], fileUrl: string[]) => Promise, - fileContent?: { raw: Partial; encrypted?: { algorithm: string; ciphertext: string } | undefined }, - ): Promise => { + async send(file: File, encrypted?: EncryptedFileUploadContent): Promise { const maxFileSize = settings.get('FileUpload_MaxFileSize'); const invalidContentType = !fileUploadIsValidContentType(file.type); const id = Random.id(); @@ -106,9 +91,9 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' ...this.uploads, { id, - file: new File([file], fileContent?.raw.name || file.name, file), + file: encrypted ? encrypted.rawFile : file, percentage: 0, - url: URL.createObjectURL(file), + encryptedFile: encrypted?.encryptedFile, }, ]); @@ -131,8 +116,8 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' `/v1/rooms.media/${this.rid}`, { file, - ...(fileContent && { - content: JSON.stringify(fileContent.encrypted), + ...(encrypted && { + content: JSON.stringify(encrypted.fileContent.encrypted), }), }, { @@ -196,7 +181,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' }; if (this.uploads.length) { - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + UserAction.performContinuously(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); } this.once(`cancelling-${id}`, () => { @@ -224,7 +209,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); } } - }; + } } export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => From 70ba3deba64c04e51fcf1bc2d8dfdaa5e9a9118b Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 26 Feb 2025 10:31:16 -0300 Subject: [PATCH 144/215] chore: handle `confirmFiles` inside `sendMessage` --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 4 ++-- apps/meteor/client/lib/chats/ChatAPI.ts | 3 ++- ...nfirmFiles.ts => processMessageUploads.ts} | 21 +++++++++---------- .../client/lib/chats/flows/sendMessage.ts | 15 ++++++++++--- .../views/room/composer/ComposerMessage.tsx | 6 +++--- .../room/composer/messageBox/MessageBox.tsx | 3 --- 6 files changed, 29 insertions(+), 23 deletions(-) rename apps/meteor/client/lib/chats/flows/{confirmFiles.ts => processMessageUploads.ts} (89%) diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 61230999428b3..cf484cf7cb54d 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -5,8 +5,8 @@ import type { IActionManager } from '@rocket.chat/ui-contexts'; import { UserAction } from './UserAction'; import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI'; import { createDataAPI } from '../../../../client/lib/chats/data'; -import { confirmFiles } from '../../../../client/lib/chats/flows/confirmFiles'; import { processMessageEditing } from '../../../../client/lib/chats/flows/processMessageEditing'; +import { processMessageUploads } from '../../../../client/lib/chats/flows/processMessageUploads'; import { processSetReaction } from '../../../../client/lib/chats/flows/processSetReaction'; import { processSlashCommand } from '../../../../client/lib/chats/flows/processSlashCommand'; import { processTooLongMessage } from '../../../../client/lib/chats/flows/processTooLongMessage'; @@ -179,11 +179,11 @@ export class ChatMessages implements ChatAPI { this.flows = { uploadFiles: uploadFiles.bind(null, this), - confirmFiles: confirmFiles.bind(null, this), sendMessage: sendMessage.bind(this, this), processSlashCommand: processSlashCommand.bind(null, this), processTooLongMessage: processTooLongMessage.bind(null, this), processMessageEditing: processMessageEditing.bind(null, this), + processMessageUploads: processMessageUploads.bind(null, this), processSetReaction: processSetReaction.bind(null, this), requestMessageDeletion: requestMessageDeletion.bind(this, this), replyBroadcast: replyBroadcast.bind(null, this), diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index de15760dbbf0f..913163f093127 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -159,7 +159,6 @@ export type ChatAPI = { uploadsStore: UploadsAPI; resetFileInput?: () => void; }) => Promise; - readonly confirmFiles: () => Promise; readonly sendMessage: ({ text, tshow, @@ -168,6 +167,7 @@ export type ChatAPI = { tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; + tmid?: IMessage['tmid']; }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; @@ -175,6 +175,7 @@ export type ChatAPI = { message: Pick & Partial>, previewUrls?: string[], ) => Promise; + readonly processMessageUploads: (message: IMessage) => Promise; readonly processSetReaction: (message: Pick) => Promise; readonly requestMessageDeletion: (message: IMessage) => Promise; readonly replyBroadcast: (message: IMessage) => Promise; diff --git a/apps/meteor/client/lib/chats/flows/confirmFiles.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts similarity index 89% rename from apps/meteor/client/lib/chats/flows/confirmFiles.ts rename to apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 4afd03cf6e7d5..ff38d639da9c0 100644 --- a/apps/meteor/client/lib/chats/flows/confirmFiles.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -4,6 +4,7 @@ import { e2e } from '../../../../app/e2e/client'; import type { E2ERoom } from '../../../../app/e2e/client/rocketchat.e2e.room'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; +import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import type { EncryptedUpload } from '../Upload'; @@ -100,21 +101,16 @@ const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2 }); }; -export const confirmFiles = async (chat: ChatAPI): Promise => { +export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => { + const { tmid, msg } = message; const room = await chat.data.getRoom(); const e2eRoom = await e2e.getInstanceByRoomId(room._id); - const replies = chat.composer?.quotedMessages.get() ?? []; - const text = chat.composer?.text || ''; - - const { msg, tmid, ...composedMessage } = await chat.data.composeMessage(text, { - quotedMessages: replies, - }); const store = tmid ? chat.threadUploads : chat.uploads; const filesToUpload = store.get(); if (filesToUpload.length === 0) { - return; + return false; } const { fileUrls, fileIds } = filesToUpload.reduce<{ fileUrls: string[]; fileIds: string[] }>( @@ -138,8 +134,8 @@ export const confirmFiles = async (chat: ChatAPI): Promise => { content = await getEncryptedContent(filesToUpload as EncryptedUpload[], e2eRoom, msg); } - const message: AtLeast = { - ...composedMessage, + const composedMessage: AtLeast = { + ...message, tmid, msg, content, @@ -149,10 +145,13 @@ export const confirmFiles = async (chat: ChatAPI): Promise => { } as const; try { - await sdk.call('sendMessage', message, fileUrls, fileIds); + await sdk.call('sendMessage', composedMessage, fileUrls, fileIds); chat.composer?.clear(); store.clear(); } catch (error: unknown) { console.error(error); + dispatchToastMessage({ type: 'error', message: error }); } + + return true; }; diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 3d372900fb299..c7983c70185c7 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -6,6 +6,7 @@ import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { processMessageEditing } from './processMessageEditing'; +import { processMessageUploads } from './processMessageUploads'; import { processSetReaction } from './processSetReaction'; import { processSlashCommand } from './processSlashCommand'; import { processTooLongMessage } from './processTooLongMessage'; @@ -23,6 +24,10 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], return; } + if (await processMessageUploads(chat, message)) { + return; + } + message = (await onClientBeforeSendMessage(message)) as IMessage; // e2e should be a client property only @@ -42,7 +47,8 @@ export const sendMessage = async ( tshow, previewUrls, isSlashCommandAllowed, - }: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }, + tmid, + }: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; tmid?: IMessage['tmid'] }, ): Promise => { if (!(await chat.data.isSubscribedToRoom())) { try { @@ -55,14 +61,17 @@ export const sendMessage = async ( chat.readStateManager.clearUnreadMark(); + const uploadsStore = tmid ? chat.threadUploads : chat.uploads; + const hasFiles = uploadsStore.get().length > 0; + text = text.trim(); - if (!text && !chat.currentEditing) { + if (!text && !chat.currentEditing && !hasFiles) { // Nothing to do return false; } - if (text) { + if (text || hasFiles) { const message = await chat.data.composeMessage(text, { sendToChannel: tshow, quotedMessages: chat.composer?.quotedMessages.get() ?? [], diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 3aa4fb2079cbf..5dab30922eea9 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -46,11 +46,13 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, + tmid, }: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; + tmid?: IMessage['_id']; }): Promise => { try { await chat?.action.stop('typing'); @@ -59,6 +61,7 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, + tmid, }); if (newMessageSent) onSend?.(); } catch (error) { @@ -74,9 +77,6 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac }, onNavigateToPreviousMessage: () => chat?.messageEditing.toPreviousMessage(), onNavigateToNextMessage: () => chat?.messageEditing.toNextMessage(), - // onUploadFiles: (files: readonly File[]) => { - // return chat?.flows.uploadFiles({ files, uploadsStore: chat.uploads }); - // }, }), [chat?.data, chat?.flows, chat?.action, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, onSend], ); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index c7981bfc5aa59..b90c319b810b7 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -162,9 +162,6 @@ const MessageBox = ({ const { uploads, hasUploads, handleUploadFiles, isUploading } = useFileUpload(uploadsStore); const handleSendMessage = useEffectEvent(() => { - if (hasUploads) { - return chat?.flows.confirmFiles(); - } const text = chat.composer?.text ?? ''; chat.composer?.clear(); popup.clear(); From 4c0be8e7f5ff66b89859dff1b114b9b51a6d6092 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 24 Feb 2025 16:36:39 -0300 Subject: [PATCH 145/215] chore: remove outdated unit test --- .../FileUploadModal/FileUploadModal.spec.tsx | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx deleted file mode 100644 index 94bc4601ffa18..0000000000000 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { mockAppRoot } from '@rocket.chat/mock-providers'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import FileUploadModal from './FileUploadModal'; - -const defaultProps = { - onClose: () => undefined, - file: new File([], 'testing.png'), - fileName: 'Testing', - fileDescription: '', - onSubmit: () => undefined, - invalidContentType: false, - showDescription: true, -}; - -it('should show Undo request button when roomOpen is true and transcriptRequest exist', async () => { - render(, { - wrapper: mockAppRoot() - .withTranslations('en', 'core', { - Send: 'Send', - Upload_file_description: 'File description', - }) - .withSetting('Message_MaxAllowedSize', 10) - .build(), - }); - - const input = await screen.findByRole('textbox', { name: 'File description' }); - expect(input).toBeInTheDocument(); - await userEvent.type(input, '12345678910'); - await userEvent.tab(); - - expect(screen.getByText('Cannot upload file, description is over the 10 character limit')).toBeInTheDocument(); -}); From be927ddc029382c8db51a61b1d562204e350a9a5 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 24 Feb 2025 16:36:47 -0300 Subject: [PATCH 146/215] chore: remove outdated translation --- packages/i18n/src/locales/af.i18n.json | 3 +-- packages/i18n/src/locales/ar.i18n.json | 1 - packages/i18n/src/locales/az.i18n.json | 3 +-- packages/i18n/src/locales/be-BY.i18n.json | 3 +-- packages/i18n/src/locales/bg.i18n.json | 3 +-- packages/i18n/src/locales/bs.i18n.json | 3 +-- packages/i18n/src/locales/ca.i18n.json | 1 - packages/i18n/src/locales/cs.i18n.json | 1 - packages/i18n/src/locales/cy.i18n.json | 3 +-- packages/i18n/src/locales/da.i18n.json | 1 - packages/i18n/src/locales/de-AT.i18n.json | 3 +-- packages/i18n/src/locales/de-IN.i18n.json | 1 - packages/i18n/src/locales/de.i18n.json | 3 +-- packages/i18n/src/locales/el.i18n.json | 3 +-- packages/i18n/src/locales/en.i18n.json | 1 - packages/i18n/src/locales/eo.i18n.json | 3 +-- packages/i18n/src/locales/es.i18n.json | 1 - packages/i18n/src/locales/fa.i18n.json | 3 +-- packages/i18n/src/locales/fi.i18n.json | 3 +-- packages/i18n/src/locales/fr.i18n.json | 1 - packages/i18n/src/locales/he.i18n.json | 5 ++--- packages/i18n/src/locales/hi-IN.i18n.json | 3 +-- packages/i18n/src/locales/hr.i18n.json | 3 +-- packages/i18n/src/locales/hu.i18n.json | 3 +-- packages/i18n/src/locales/id.i18n.json | 3 +-- packages/i18n/src/locales/it.i18n.json | 1 - packages/i18n/src/locales/ja.i18n.json | 1 - packages/i18n/src/locales/ka-GE.i18n.json | 1 - packages/i18n/src/locales/km.i18n.json | 3 +-- packages/i18n/src/locales/ko.i18n.json | 1 - packages/i18n/src/locales/ku.i18n.json | 3 +-- packages/i18n/src/locales/lo.i18n.json | 3 +-- packages/i18n/src/locales/lt.i18n.json | 3 +-- packages/i18n/src/locales/lv.i18n.json | 3 +-- packages/i18n/src/locales/mn.i18n.json | 3 +-- packages/i18n/src/locales/ms-MY.i18n.json | 3 +-- packages/i18n/src/locales/nl.i18n.json | 1 - packages/i18n/src/locales/nn.i18n.json | 3 +-- packages/i18n/src/locales/no.i18n.json | 3 +-- packages/i18n/src/locales/pl.i18n.json | 3 +-- packages/i18n/src/locales/pt-BR.i18n.json | 1 - packages/i18n/src/locales/pt.i18n.json | 1 - packages/i18n/src/locales/ro.i18n.json | 3 +-- packages/i18n/src/locales/ru.i18n.json | 1 - packages/i18n/src/locales/se.i18n.json | 3 +-- packages/i18n/src/locales/sk-SK.i18n.json | 3 +-- packages/i18n/src/locales/sl-SI.i18n.json | 3 +-- packages/i18n/src/locales/sq.i18n.json | 3 +-- packages/i18n/src/locales/sr.i18n.json | 1 - packages/i18n/src/locales/sv.i18n.json | 3 +-- packages/i18n/src/locales/ta-IN.i18n.json | 3 +-- packages/i18n/src/locales/th-TH.i18n.json | 3 +-- packages/i18n/src/locales/tr.i18n.json | 3 +-- packages/i18n/src/locales/uk.i18n.json | 1 - packages/i18n/src/locales/vi-VN.i18n.json | 3 +-- packages/i18n/src/locales/zh-HK.i18n.json | 3 +-- packages/i18n/src/locales/zh-TW.i18n.json | 1 - packages/i18n/src/locales/zh.i18n.json | 1 - 58 files changed, 39 insertions(+), 97 deletions(-) diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index 3db46a1316986..6f87f42ed1976 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -2496,7 +2496,6 @@ "Unstar_Message": "Verwyder ster", "Update_your_RocketChat": "Dateer jou Rocket.Chat op", "Updated_at": "Opgedateer op", - "Upload_file_description": "Lêer beskrywing", "Upload_file_name": "Lêernaam", "Upload_file_question": "Laai leêr op?", "Upload_Folder_Path": "Laai mappad op", @@ -2751,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Stuur bevestiging e-pos", "Enterprise": "onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 86858e90855ad..2b8daaf642204 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -4365,7 +4365,6 @@ "Upgrade_tab_upgrade_your_plan": "قم بترقية اشتراكك", "Upload": "تحميل", "Uploads": "تحميلات", - "Upload_file_description": "وصف الملف", "Upload_file_name": "اسم الملف", "Upload_file_question": "تحميل الملف؟", "Upload_Folder_Path": "تحميل مسار المجلد", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index 5fbc0d7420eac..f0dbe61b6572c 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -2496,7 +2496,6 @@ "Unstar_Message": "Star'ı sil", "Update_your_RocketChat": "Sizin Rocket.Chat yeniləyin", "Updated_at": "Yenilənib", - "Upload_file_description": "Fayl təsviri", "Upload_file_name": "Fayl adı", "Upload_file_question": "Fayl yükləməyiniz?", "Upload_Folder_Path": "Qovluq yolunu yükləyin", @@ -2751,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Təsdiq e-poçt göndər", "Enterprise": "Müəssisə", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index 4185d6080b6cd..c8c85a9f1482c 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -2514,7 +2514,6 @@ "Unstar_Message": "выдаліць пазнаку", "Update_your_RocketChat": "абнаўленне Rocket.Chat", "Updated_at": "абноўлена", - "Upload_file_description": "апісанне файла", "Upload_file_name": "Імя файла", "Upload_file_question": "Загрузіць файл?", "Upload_Folder_Path": "Загрузіць шлях да тэчцы", @@ -2769,4 +2768,4 @@ "registration.component.form.sendConfirmationEmail": "Адправіць па электроннай пошце пацвярджэнне", "Enterprise": "прадпрыемства", "UpgradeToGetMore_engagement-dashboard_Title": "аналітыка" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index 7370664b3c394..c3dc6a0ea31f4 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -2493,7 +2493,6 @@ "Unstar_Message": "Премахване на звезда", "Update_your_RocketChat": "Актуализирайте вашето Rocket.Chat", "Updated_at": "Актуализиран на", - "Upload_file_description": "Описание на файла", "Upload_file_name": "Име на файл", "Upload_file_question": "Качи фаил?", "Upload_Folder_Path": "Качване на пътя на папките", @@ -2744,4 +2743,4 @@ "registration.component.form.sendConfirmationEmail": "Изпратете имейл за потвърждение", "Enterprise": "начинание", "UpgradeToGetMore_engagement-dashboard_Title": "анализ" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index c6c9813a752d7..9854549045b2d 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -2490,7 +2490,6 @@ "Unstar_Message": "Ukloni zvjezdicu", "Update_your_RocketChat": "Ažurirajte Rocket.Chat", "Updated_at": "Ažurirano u", - "Upload_file_description": "Opis fajla", "Upload_file_name": "Ime fajla", "Upload_file_question": "Prenesi datoteku?", "Upload_Folder_Path": "Prijenos puta mape", @@ -2741,4 +2740,4 @@ "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "Enterprise": "Poduzeće", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 1e4ae4e16c950..7c67dbc70804b 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -4281,7 +4281,6 @@ "Updated_at": "Actualitzat a", "Upload": "Pujar", "Uploads": "Càrregues", - "Upload_file_description": "Descripció de l'arxiu", "Upload_file_name": "Nom de l'arxiu", "Upload_file_question": "Pujar l'arxiu?", "Upload_Folder_Path": "Carregar ruta de la carpeta", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 73d89df40b53b..32b4b3f1f8ee1 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -3623,7 +3623,6 @@ "Update_your_RocketChat": "Aktualizujte svůj Rocket.Chat", "Updated_at": "Poslední aktualizace", "Upload": "Nahrát", - "Upload_file_description": "Popis souboru", "Upload_file_name": "Název souboru", "Upload_file_question": "Nahrát soubor?", "Upload_Folder_Path": "Cesta složky pro nahrávání souborů", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index dee7d8273247a..b0c3a67ddc5fc 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -2491,7 +2491,6 @@ "Unstar_Message": "Dileu Seren", "Update_your_RocketChat": "Diweddarwch eich Rocket.Chat", "Updated_at": "Wedi'i ddiweddaru yn", - "Upload_file_description": "Disgrifiad o'r ffeil", "Upload_file_name": "Ffeil enw", "Upload_file_question": "Llwytho ffeil?", "Upload_Folder_Path": "Llwytho Llwybr Ffolder", @@ -2743,4 +2742,4 @@ "registration.component.form.sendConfirmationEmail": "Anfon ebost cadarnhad", "Enterprise": "Menter", "UpgradeToGetMore_engagement-dashboard_Title": "Dadansoddiadau" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 718295d4bf23f..8a5cff0027c5a 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -3729,7 +3729,6 @@ "Update_your_RocketChat": "Opdater din Rocket.Chat", "Updated_at": "Opdateret kl", "Upload": "Upload", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Upload fil?", "Upload_Folder_Path": "Upload mappepath", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index 00717193c8f33..7f2f00ac6ffae 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -2499,7 +2499,6 @@ "Unstar_Message": "Markierung entfernen", "Update_your_RocketChat": "Aktualisieren Sie Ihr Rocket.Chat", "Updated_at": "Aktualisiert am", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Möchten Sie eine Datei hochladen?", "Upload_Folder_Path": "Ordnerpfad hochladen", @@ -2751,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Bestätigungsmail versenden", "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 93bacebaf2cf0..a1ed99bd97ef5 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -2821,7 +2821,6 @@ "Update_to_version": "Auf Version {{version}} aktualisieren", "Update_your_RocketChat": "Aktualisiere Dein Rocket.Chat", "Updated_at": "Aktualisiert am", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Datei hochladen?", "Upload_Folder_Path": "Pfad des Uploads", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 38c56d02861c1..39f812712f577 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -4877,7 +4877,6 @@ "Upgrade_tab_upgrade_your_plan": "Aktualisieren Sie Ihren Plan", "Upload": "Hochladen", "Uploads": "Uploads", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Datei hochladen?", "Upload_Folder_Path": "Pfad des Uploads", @@ -5500,4 +5499,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index caf851c5f7d3b..d05fe587bd40f 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -2503,7 +2503,6 @@ "Unstar_Message": "Κατάργηση αστεριών", "Update_your_RocketChat": "Ενημερώστε το Rocket.Chat", "Updated_at": "Ενημερώθηκε στο", - "Upload_file_description": "Περιγραφή Αρχείου", "Upload_file_name": "Ονομα αρχείου", "Upload_file_question": "Να ανέβει το αρχείο;", "Upload_Folder_Path": "Μεταφόρτωση διαδρομής φακέλου", @@ -2758,4 +2757,4 @@ "registration.component.form.sendConfirmationEmail": "Αποστολή email επιβεβαίωσης", "Enterprise": "Επιχείρηση", "UpgradeToGetMore_engagement-dashboard_Title": "Αναλυτικά στοιχεία" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 768c684aeb2c3..d59f5404fbe0a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5761,7 +5761,6 @@ "Upload": "Upload", "Uploads": "Uploads", "Upload_private_app": "Upload private app", - "Upload_file_description": "File description", "Upload_file": "Upload file", "Upload_file_name": "File name", "Upload_file_question": "Upload file?", diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 5e007b1b5d372..0858d33f64853 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -2496,7 +2496,6 @@ "Unstar_Message": "Forigi Stelon", "Update_your_RocketChat": "Ĝisdatigu vian Rocket.Chat", "Updated_at": "Ĝisdatigita je", - "Upload_file_description": "Dosiero priskribo", "Upload_file_name": "Dosiernomo", "Upload_file_question": "Alŝutu dosieron?", "Upload_Folder_Path": "Alŝuti dosierujon", @@ -2752,4 +2751,4 @@ "registration.component.form.sendConfirmationEmail": "Sendu konfirman retpoŝton", "Enterprise": "Entrepreno", "UpgradeToGetMore_engagement-dashboard_Title": "Analitiko" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index fee173326f545..fecb90c5e50ac 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -4344,7 +4344,6 @@ "Updated_at": "Actualizado en", "Upload": "Subir", "Uploads": "Subidas", - "Upload_file_description": "Descripción de archivo", "Upload_file_name": "Nombre de archivo", "Upload_file_question": "¿Subir archivo?", "Upload_Folder_Path": "Ruta de carpeta de subida", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index 7557e4b7213f3..dd06286775847 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -2830,7 +2830,6 @@ "Unstar_Message": "حذف ستاره", "Update_your_RocketChat": "Rocket.Chat خود را به روز کنید", "Updated_at": "به روز شده در", - "Upload_file_description": "توضیحات فایل", "Upload_file_name": "نام فایل", "Upload_file_question": "آپلود فایل؟", "Upload_Folder_Path": "مسیر پوشه آپلود", @@ -3100,4 +3099,4 @@ "RegisterWorkspace_Features_Omnichannel_Title": "کانال همه‌کاره", "Enterprise": "شرکت، پروژه", "UpgradeToGetMore_engagement-dashboard_Title": "تجزیه و تحلیل ترافیک" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index ba94975681785..d0e2e92e21fb3 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -4977,7 +4977,6 @@ "Upload": "Lataa", "Uploads": "Lataukset", "Upload_private_app": "Lataa yksityinen sovellus", - "Upload_file_description": "Tiedoston kuvaus", "Upload_file_name": "Tiedoston nimi", "Upload_file_question": "Ladataanko tiedosto?", "Upload_Folder_Path": "Latauskansion polku", @@ -5713,4 +5712,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index c9c387a35287d..58499691a2ca1 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -4361,7 +4361,6 @@ "Upgrade_tab_upgrade_your_plan": "Améliorez votre plan", "Upload": "Charger", "Uploads": "Chargements", - "Upload_file_description": "Description du fichier", "Upload_file_name": "Nom du fichier", "Upload_file_question": "Charger le fichier ?", "Upload_Folder_Path": "Chemin du dossier de chargement", diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 4cc6f289d27a9..d0e5eee7810e2 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -812,7 +812,7 @@ "Livechat_offline": "מחובר LiveChat", "Livechat_online": "LiveChat באינטרנט", "Livechat_registration_form": "טופס הרשמה", - "Livechat_title": "כותרת Livechat", + "Livechat_title": "כותרת Livechat", "Livechat_title_color": "צבע רקע של כותרת Livechat", "Livechat_Users": "משתמשים LiveChat", "Load_more": "טעינת נוספים", @@ -1378,7 +1378,6 @@ "Update": "עדכון", "Updated_at": "עודכן ב", "Upload": "העלאה", - "Upload_file_description": "תיאור קובץ", "Upload_file_name": "שם קובץ", "Upload_file_question": "להעלות קובץ?", "Uploading_file": "מעלה קובץ...", @@ -1542,4 +1541,4 @@ "registration.component.form.confirmPassword": "אמת את הסיסמה שלך", "registration.component.form.sendConfirmationEmail": "שליחת דוא״ל אימות", "UpgradeToGetMore_engagement-dashboard_Title": "סטטיסטיקה" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index c7bada529b4c2..cde8b10675d2f 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -5252,7 +5252,6 @@ "Upload": "डालना", "Uploads": "अपलोड", "Upload_private_app": "निजी ऐप अपलोड करें", - "Upload_file_description": "फाइल विवरण", "Upload_file_name": "फ़ाइल का नाम", "Upload_file_question": "दस्तावेज अपलोड करें?", "Upload_Folder_Path": "फ़ोल्डर पथ अपलोड करें", @@ -6097,4 +6096,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index 56d8d6392b78d..9a631a5b0c9b4 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -2629,7 +2629,6 @@ "Unstar_Message": "Ukloni zvjezdicu", "Update_your_RocketChat": "Ažurirajte Rocket.Chat", "Updated_at": "Ažurirano u", - "Upload_file_description": "Opis fajla", "Upload_file_name": "Ime fajla", "Upload_file_question": "Prenesi datoteku?", "Upload_Folder_Path": "Prijenos puta mape", @@ -2884,4 +2883,4 @@ "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "Enterprise": "Poduzeće", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 6b0fa2bf6dcb4..99a8cb87cdc51 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -4789,7 +4789,6 @@ "Upgrade_tab_upgrade_your_plan": "Előfizetéses csomag frissítése", "Upload": "Feltöltés", "Uploads": "Feltöltések", - "Upload_file_description": "Fájl leírása", "Upload_file_name": "Fájlnév", "Upload_file_question": "Feltölti a fájlt?", "Upload_Folder_Path": "Feltöltési mappa útvonala", @@ -5401,4 +5400,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index 4bd6294ebdf31..77a23816b576d 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -2504,7 +2504,6 @@ "Unstar_Message": "Hapus Rating", "Update_your_RocketChat": "Perbarui Rocket Anda", "Updated_at": "Diperbarui pada", - "Upload_file_description": "Deskripsi berkas", "Upload_file_name": "Nama file", "Upload_file_question": "Unggah file?", "Upload_Folder_Path": "Unggah Jalur Folder", @@ -2759,4 +2758,4 @@ "registration.component.form.sendConfirmationEmail": "Kirim email konfirmasi", "Enterprise": "Perusahaan", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index 5920f36822799..ebf8083c940f0 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -3064,7 +3064,6 @@ "Update_your_RocketChat": "Aggiorna il tuo Rocket.Chat", "Updated_at": "Aggiornato a", "Uploads": "Upload", - "Upload_file_description": "Descrizione file", "Upload_file_name": "Nome file", "Upload_file_question": "Caricare il file?", "Upload_Folder_Path": "Carica percorso cartella", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 73c0ccd14b317..26c37d7ce1392 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -4308,7 +4308,6 @@ "Updated_at": "更新日", "Upload": "アップロード", "Uploads": "アップロード", - "Upload_file_description": "ファイルの説明", "Upload_file_name": "ファイル名", "Upload_file_question": "ファイルをアップロードしますか?", "Upload_Folder_Path": "フォルダパスのアップロード", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index d4f2de06546b7..36a67c6d391f9 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -3357,7 +3357,6 @@ "Update_your_RocketChat": "განაახლეთ თქვენი Rocket.Chat", "Updated_at": "განახლებულია __-ზე", "Upload": "ატვირთვა", - "Upload_file_description": "ფაილის აღწერა", "Upload_file_name": "ფაილის სახელი", "Upload_file_question": "გსურთ ატვირთოთ ფაილი?", "Upload_Folder_Path": "საქაღალდის გზის ატვირთვა", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index f6893349cdab9..9ee2a593a0104 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -2839,7 +2839,6 @@ "Update_to_version": "ទំនើបកម្មទៅ {{version}}", "Update_your_RocketChat": "ធ្វើបច្ចុប្បន្នភាព Rocket.Chat របស់អ្នក", "Updated_at": "ធ្វើបច្ចុប្បន្នភាពនៅ", - "Upload_file_description": "ការពិពណ៌នាឯកសារ", "Upload_file_name": "ឈ្មោះ​ឯកសារ", "Upload_file_question": "ផ្ទុក​ឯកសារ​ឡើង​ឬ?", "Upload_Folder_Path": "ផ្ទុកផ្លូវថតឡើង", @@ -3108,4 +3107,4 @@ "Enterprise": "សហគ្រាស", "UpgradeToGetMore_engagement-dashboard_Title": "វិភាគ", "UpgradeToGetMore_auditing_Title": "សវនកម្មសារ" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index 72667cf60692e..7fdcf16818298 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -3680,7 +3680,6 @@ "Update_your_RocketChat": "Rocket.Chat을 업데이트하십시오.", "Updated_at": "업데이트:", "Upload": "업로드", - "Upload_file_description": "파일 설명", "Upload_file_name": "파일 이름", "Upload_file_question": "파일을 업로드하시겠습니까?", "Upload_Folder_Path": "폴더 경로 업로드", diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index c0e9bb820059c..0b61e2e5fa631 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -2490,7 +2490,6 @@ "Unstar_Message": "jê Star", "Update_your_RocketChat": "Rawet.Chat", "Updated_at": "Nûvekirî", - "Upload_file_description": "Pirtûka pelê", "Upload_file_name": "Navê pelê", "Upload_file_question": "Pelê bar bike?", "Upload_Folder_Path": "Peldanka Peldanka Hilbijêre", @@ -2741,4 +2740,4 @@ "registration.component.form.sendConfirmationEmail": "ئیمەیڵی پشتڕاستکردنەوە بنێرە", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index 8738dd3ac31e4..b1a7d12b3796c 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -2533,7 +2533,6 @@ "Unstar_Message": "ເອົາ Star", "Update_your_RocketChat": "ອັບເດດ RocketChat ຂອງທ່ານ", "Updated_at": "Updated at", - "Upload_file_description": "ລາຍລະອຽດຂອງໄຟລ໌", "Upload_file_name": "ຊື່​ເອ​ກະ​ສານ", "Upload_file_question": "ອັບໂຫລດເອກະສານ?", "Upload_Folder_Path": "ອັບໂຫລດໂຟເດີໂຟເດີ", @@ -2788,4 +2787,4 @@ "registration.component.form.sendConfirmationEmail": "ສົ່ງອີເມວການຢືນຢັນ", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "ການວິເຄາະ" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index cf900a54ae7ce..4779cf42389f2 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -2551,7 +2551,6 @@ "Unstar_Message": "Pašalinti žvaigždutę", "Update_your_RocketChat": "Atnaujink savo \"Rocket.Chat\"", "Updated_at": "Atnaujinta", - "Upload_file_description": "Failo aprašymas", "Upload_file_name": "Failo pavadinimas", "Upload_file_question": "Įkelti failą?", "Upload_Folder_Path": "Įkelti aplanko kelią", @@ -2806,4 +2805,4 @@ "registration.component.form.sendConfirmationEmail": "Siųsti patvirtinimo el. Laišką", "Enterprise": "Įmonė", "UpgradeToGetMore_engagement-dashboard_Title": "\"Analytics\"" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index f2249e1468909..771c9db2c2525 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -2503,7 +2503,6 @@ "Unstar_Message": "Noņemt zvaigznīti", "Update_your_RocketChat": "Atjauniniet savu Rocket.Chat", "Updated_at": "Atjaunināts uz", - "Upload_file_description": "Faila apraksts", "Upload_file_name": "Faila nosaukums", "Upload_file_question": "Vai augšupielādēt failu?", "Upload_Folder_Path": "Augšupielādēt mapes ceļu", @@ -2747,4 +2746,4 @@ "registration.component.form.sendConfirmationEmail": "Nosūtīt apstiprinājuma e-pastu", "Enterprise": "Uzņēmums", "UpgradeToGetMore_engagement-dashboard_Title": "Analītika" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index d44a0dc7f2f3a..2f4ec23cbcffb 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -2489,7 +2489,6 @@ "Unstar_Message": "Оддыг устгах", "Update_your_RocketChat": "Өөрийн Rocket.Chat-г шинэчлэх", "Updated_at": "Дээр шинэчилсэн", - "Upload_file_description": "Файлын тайлбар", "Upload_file_name": "Файлын нэр", "Upload_file_question": "Файл оруулах уу?", "Upload_Folder_Path": "Folder Path-г оруулна уу", @@ -2740,4 +2739,4 @@ "registration.component.form.sendConfirmationEmail": "Баталгаажуулах имэйл илгээх", "Enterprise": "Аж ахуйн нэгж", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитик" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index d16bdf9ccc1c0..8d741419c4842 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -2503,7 +2503,6 @@ "Unstar_Message": "Padam Bintang", "Update_your_RocketChat": "Kemas kini Rocket.Chat anda", "Updated_at": "Dikemaskini di", - "Upload_file_description": "Penerangan fail", "Upload_file_name": "Nama fail", "Upload_file_question": "Muat naik fail?", "Upload_Folder_Path": "Muatkan Laluan Folder", @@ -2755,4 +2754,4 @@ "registration.component.form.sendConfirmationEmail": "Hantar e-mel pengesahan", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analisis" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index 3705c8c09f65e..18258f5578b4c 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -4348,7 +4348,6 @@ "Upgrade_tab_upgrade_your_plan": "Upgrade je abonnement", "Upload": "Uploaden", "Uploads": "Uploads", - "Upload_file_description": "Bestandsomschrijving", "Upload_file_name": "Bestandsnaam", "Upload_file_question": "Upload bestand?", "Upload_Folder_Path": "Upload mappad", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 7b2a9e8474a5f..5b2206b75a5d9 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -4147,7 +4147,6 @@ "Updated_at": "Oppdatert på", "Upload": "Last opp", "Upload_private_app": "Last opp privat app", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Last opp fil?", "Upload_Folder_Path": "Last opp mappebane", @@ -4552,4 +4551,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index 9c772373fd0b2..a9684e8d9081f 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -4147,7 +4147,6 @@ "Updated_at": "Oppdatert på", "Upload": "Last opp", "Upload_private_app": "Last opp privat app", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Last opp fil?", "Upload_Folder_Path": "Last opp mappebane", @@ -4554,4 +4553,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 2612e7161761c..2056d8a83a4c2 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -4780,7 +4780,6 @@ "Upgrade_tab_upgrade_your_plan": "Zwiększ poziom swojego plan", "Upload": "Wczytaj", "Uploads": "Przesyłane pliki", - "Upload_file_description": "Opis pliku", "Upload_file_name": "Nazwa pliku", "Upload_file_question": "Przesłać plik?", "Upload_Folder_Path": "Prześlij ścieżkę folderu", @@ -5396,4 +5395,4 @@ "Broadcast_hint_enabled": "Tylko właściciele {{roomType}} mogą pisać nowe wiadomości, ale każdy może odpowiadać w wątku", "Anyone_can_send_new_messages": "Każdy może wysyłać nowe wiadomości", "Select_messages_to_hide": "Wybierz wiadomości do ukrycia" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 16c6c01ec7230..16b5ccea921a9 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -4520,7 +4520,6 @@ "Upgrade_tab_upgrade_your_plan": "Faça upgrade no seu plano", "Upload": "Upload", "Uploads": "Uploads", - "Upload_file_description": "Descrição do arquivo", "Upload_file_name": "Nome do arquivo", "Upload_file_question": "Fazer upload de arquivo?", "Upload_Folder_Path": "Fazer upload de caminho da pasta", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 4aa6a81bc790d..5d0841e86d950 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -2886,7 +2886,6 @@ "Update_to_version": "Actualizado para {{version}}", "Update_your_RocketChat": "Actualize o seu Rocket.Chat", "Updated_at": "Actualizado em", - "Upload_file_description": "Descrição do ficheiro", "Upload_file_name": "Nome do ficheiro", "Upload_file_question": "Carregar ficheiro?", "Upload_Folder_Path": "Carregar caminho da pasta", diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index 53d4b5f742664..0fa65bb4fbb9b 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -2495,7 +2495,6 @@ "Unstar_Message": "Eliminați marcajul cu stea", "Update_your_RocketChat": "Actualizați-vă Rocket.Chat", "Updated_at": "Actualizat la", - "Upload_file_description": "Descrierea fisierului", "Upload_file_name": "Nume de fișier", "Upload_file_question": "Încarcă fișier?", "Upload_Folder_Path": "Încărcați calea folderelor", @@ -2745,4 +2744,4 @@ "registration.component.form.sendConfirmationEmail": "Trimite email de confirmare", "Enterprise": "Afacere", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 16f31e1b1640a..fd81dcf3a99fd 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -4529,7 +4529,6 @@ "Upgrade_tab_upgrade_your_plan": "Улучшить ваш план", "Upload": "Загрузить", "Uploads": "Загрузки", - "Upload_file_description": "Описание файла", "Upload_file_name": "Имя файла", "Upload_file_question": "Загрузить файл?", "Upload_Folder_Path": "Путь к папке загрузки", diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index 686d299a9fdd6..608f6f6a8e39b 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -5591,7 +5591,6 @@ "Upload": "Upload", "Uploads": "Uploads", "Upload_private_app": "Upload private app", - "Upload_file_description": "File description", "Upload_file": "Upload file", "Upload_file_name": "File name", "Upload_file_question": "Upload file?", @@ -6567,4 +6566,4 @@ "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index ce1bf188d3d2c..b2ca1c3f45cc6 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -2505,7 +2505,6 @@ "Unstar_Message": "Odstrániť hviezdičku", "Update_your_RocketChat": "Aktualizujte svoj Rocket.Chat", "Updated_at": "Aktualizované na", - "Upload_file_description": "Popis súboru", "Upload_file_name": "Názov súboru", "Upload_file_question": "Nahrajte súbor?", "Upload_Folder_Path": "Nahrať cestu priečinka", @@ -2757,4 +2756,4 @@ "registration.component.form.sendConfirmationEmail": "Pošlite potvrdzovací e-mail", "Enterprise": "podnik", "UpgradeToGetMore_engagement-dashboard_Title": "Analytika" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index d7198011197c4..789efdb7b8c4b 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -2485,7 +2485,6 @@ "Unstar_Message": "Odstrani zvezdico", "Update_your_RocketChat": "Posodobite vaš Rocket.Chat", "Updated_at": "Posodobljeno ob", - "Upload_file_description": "Opis datoteke", "Upload_file_name": "Ime datoteke", "Upload_file_question": "Želite naložiti datoteko?", "Upload_Folder_Path": "Naloži pot do mape", @@ -2737,4 +2736,4 @@ "registration.component.form.sendConfirmationEmail": "Pošlji potrditveno e-poštno sporočilo", "Enterprise": "Podjetje", "UpgradeToGetMore_engagement-dashboard_Title": "Analiza" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index b202c73c17063..62d9d83ceee3b 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -2495,7 +2495,6 @@ "Unstar_Message": "Hiq Star", "Update_your_RocketChat": "Përditëso Rocket.Chat tuaj", "Updated_at": "Përditësuar në", - "Upload_file_description": "Përshkrimi i skedarit", "Upload_file_name": "Emri i skedarit", "Upload_file_question": "Ngarko skedar?", "Upload_Folder_Path": "Ngarko dosjen e dosjes", @@ -2746,4 +2745,4 @@ "registration.component.form.sendConfirmationEmail": "Dërgo email konfirmimi", "Enterprise": "Ndërmarrje", "UpgradeToGetMore_engagement-dashboard_Title": "Analitikë" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index 7ca4d30271489..aed2eb2416ed9 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -2304,7 +2304,6 @@ "Unstar_Message": "ремове Стар", "Update_your_RocketChat": "Ажурирајте свој Роцкет.Цхат", "Updated_at": "Ажурирано у", - "Upload_file_description": "Опис фајла", "Upload_file_name": "Назив документа", "Upload_file_question": "Отпреми датотеку?", "Upload_Folder_Path": "Путања фолдера", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index b8f3e6cf3f723..8584db8563f6c 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -4984,7 +4984,6 @@ "Upload": "Ladda upp", "Uploads": "Uppladdningar", "Upload_private_app": "Ladda upp en privat app", - "Upload_file_description": "Filbeskrivning", "Upload_file": "Ladda upp fil", "Upload_file_name": "Filnamn", "Upload_file_question": "Ladda upp fil?", @@ -5715,4 +5714,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index ee93281a64735..e09c3feea187d 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -2495,7 +2495,6 @@ "Unstar_Message": "ஸ்டார் நீக்க", "Update_your_RocketChat": "உங்கள் ராக்கெட் புதுப்பிக்கவும்", "Updated_at": "புதுப்பிக்கப்பட்டது", - "Upload_file_description": "கோப்பு விளக்கம்", "Upload_file_name": "கோப்பு பெயர்", "Upload_file_question": "கோப்பை பதிவேற்ற?", "Upload_Folder_Path": "கோப்புறை பாதை பதிவேற்றவும்", @@ -2749,4 +2748,4 @@ "registration.component.form.sendConfirmationEmail": "உறுதிப்படுத்தும் மின்னஞ்சல் அனுப்பவும்", "Enterprise": "நிறுவன", "UpgradeToGetMore_engagement-dashboard_Title": "அனலிட்டிக்ஸ்" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index a613a89b9b69e..86b2c728b2665 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -2487,7 +2487,6 @@ "Unstar_Message": "นำดาวออก", "Update_your_RocketChat": "อัพเดต Rocket.Chat ของคุณ", "Updated_at": "อัปเดตเมื่อวันที่", - "Upload_file_description": "คำอธิบายไฟล์", "Upload_file_name": "ชื่อไฟล์", "Upload_file_question": "อัปโหลดไฟล์หรือไม่?", "Upload_Folder_Path": "อัปโหลดเส้นทางโฟลเดอร์", @@ -2735,4 +2734,4 @@ "registration.component.form.sendConfirmationEmail": "ส่งอีเมลยืนยัน", "Enterprise": "องค์กร", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 1c7d0bb8be451..a34bdacc44c50 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -2971,7 +2971,6 @@ "Update_to_version": "{{version}} sürümüne güncelle", "Update_your_RocketChat": "Rocket.Chat'i güncelleyin", "Updated_at": "Güncelleme zamanı", - "Upload_file_description": "Dosya açıklaması", "Upload_file_name": "Dosya adı", "Upload_file_question": "Dosya yükle?", "Upload_Folder_Path": "Dosya yükleme konumu", @@ -3257,4 +3256,4 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Çoklu Kanal", "Enterprise": "Kuruluş", "UpgradeToGetMore_engagement-dashboard_Title": "Mantıksal Analiz" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index e8d14b7f8fc00..cd95ca6ff853c 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -3059,7 +3059,6 @@ "Update_LatestAvailableVersion": "Оновити до останньої доступної версії", "Update_your_RocketChat": "Оновіть свій Rocket.Chat", "Updated_at": "Оновлено на", - "Upload_file_description": "Опис файлу", "Upload_file_name": "Ім'я файлу", "Upload_file_question": "Завантажити файл?", "Upload_Folder_Path": "Завантажте шлях до папки", diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index 6ec1c662fc5be..c7116ef09a379 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -2596,7 +2596,6 @@ "Unstar_Message": "Xóa dấu sao", "Update_your_RocketChat": "Cập nhật Rocket.Chat của bạn", "Updated_at": "Cập nhật tại", - "Upload_file_description": "Mô tả tập tin", "Upload_file_name": "Tên tệp", "Upload_file_question": "Cập nhật dử liệu?", "Upload_Folder_Path": "Tải lên đường dẫn thư mục", @@ -2846,4 +2845,4 @@ "registration.component.form.sendConfirmationEmail": "Gửi email xác nhận", "Enterprise": "Doanh nghiệp", "UpgradeToGetMore_engagement-dashboard_Title": "phân tích" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-HK.i18n.json b/packages/i18n/src/locales/zh-HK.i18n.json index 155981160d3f7..d662644e95676 100644 --- a/packages/i18n/src/locales/zh-HK.i18n.json +++ b/packages/i18n/src/locales/zh-HK.i18n.json @@ -2523,7 +2523,6 @@ "Unstar_Message": "删除星", "Update_your_RocketChat": "更新你的Rocket.Chat", "Updated_at": "更新于", - "Upload_file_description": "文件描述", "Upload_file_name": "文件名", "Upload_file_question": "上传文件?", "Upload_Folder_Path": "上传文件夹路径", @@ -2770,4 +2769,4 @@ "registration.component.form.sendConfirmationEmail": "已发送确认电子邮件", "Enterprise": "企业", "UpgradeToGetMore_engagement-dashboard_Title": "分析" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 33cfdf7886437..12d32e267bd72 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -4115,7 +4115,6 @@ "Update_your_RocketChat": "更新你的 Rocket.Chat", "Updated_at": "更新於", "Upload": "上傳", - "Upload_file_description": "檔案敘述", "Upload_file_name": "檔案名稱", "Upload_file_question": "是否上傳檔案?", "Upload_Folder_Path": "上傳資料夾路徑", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 301377b4675ff..c76f979c0b509 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -3776,7 +3776,6 @@ "Update_your_RocketChat": "更新你的 Rocket.Chat", "Updated_at": "更新于", "Upload": "上传", - "Upload_file_description": "文件描述", "Upload_file_name": "文件名", "Upload_file_question": "上传文件?", "Upload_Folder_Path": "上传文件夹路径", From c206d67b0e4c4647070bb95f71c4fc8386f0bb12 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 26 Feb 2025 18:54:31 -0300 Subject: [PATCH 147/215] backend code improvements --- .../server/methods/sendFileMessage.ts | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 48b42207d0fd0..3aeacbbdbee50 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -11,6 +11,7 @@ import type { import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Logger } from '@rocket.chat/logger'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -33,37 +34,25 @@ function validateFileRequiredFields(file: Partial): asserts file is AtL const logger = new Logger('sendFileMessage'); -type parseMultipleFilesIntoMessageAttachmentsResult = { files: FileProp[]; attachments: MessageAttachment[] }; export const parseMultipleFilesIntoMessageAttachments = async ( filesToConfirm: Partial[], roomId: string, user: IUser, -): Promise => { - const result: parseMultipleFilesIntoMessageAttachmentsResult = { files: [], attachments: [] }; - - await Promise.all( - filesToConfirm.reduce>>((acc, file) => { - if (!file) return acc; - - acc.push( - (async () => { - try { - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - result.files.push(...files); - result.attachments.push(...attachments); - } catch (error) { - // Not an important error, it should not happen and if it happens wil affect the attachment - // preview in the message object only - logger.warn('Error processing file:', file, error); - } - })(), - ); - - return acc; - }, []), +): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { + const results = await Promise.all( + filesToConfirm.map((file) => + wrapExceptions(() => parseFileIntoMessageAttachments(file, roomId, user)).catch(async (error) => { + // Not an important error, it should not happen and if it happens wil affect the attachment preview in the message object only + logger.warn({ msg: 'Error processing file: ', file, error }); + return { files: [], attachments: [] }; + }), + ), ); - return result; + return { + files: results.flatMap(({ files }) => files), + attachments: results.flatMap(({ attachments }) => attachments), + }; }; export const parseFileIntoMessageAttachments = async ( From bbfe2811f219d53d1fda74c5579eb9eb76af83e3 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 5 Mar 2025 16:57:38 -0300 Subject: [PATCH 148/215] chore: review tweaks --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 7 +------ .../room/composer/messageBox/MessageComposerFileArea.tsx | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 03eaaea3c42a7..88d2dbea895ba 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -3,7 +3,6 @@ import { settings } from '../../../../app/settings/client'; import { t } from '../../../../app/utils/lib/i18n'; import { MAX_MULTIPLE_UPLOADED_FILES } from '../../constants'; import { dispatchToastMessage } from '../../toast'; -// import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI, UploadsAPI, EncryptedFileUploadContent } from '../ChatAPI'; export const uploadFiles = async ( @@ -14,14 +13,10 @@ export const uploadFiles = async ( if (uploadsStore.get().length > MAX_MULTIPLE_UPLOADED_FILES) { return dispatchToastMessage({ type: 'error', - message: t('You_cant_upload_more_than__count__files', { count: 10 }), + message: t('You_cant_upload_more_than__count__files', { count: MAX_MULTIPLE_UPLOADED_FILES }), }); } - // const replies = chat.composer?.quotedMessages.get() ?? []; - - // const msg = await prependReplies('', replies); - const room = await chat.data.getRoom(); const queue = [...files]; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx index f0683114161fb..412458b0659fc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx @@ -13,8 +13,8 @@ type MessageComposerFileAreaProps = { const MessageComposerFileArea = ({ uploads, onRemove, onEdit, onCancel }: MessageComposerFileAreaProps) => { return ( - {uploads?.map((upload, index) => ( -
+ {uploads?.map((upload) => ( +
))} From 7bea8d85eeba6a41c9675f00cd01f7ee4fdb6720 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 6 Mar 2025 15:28:43 -0300 Subject: [PATCH 149/215] test: add unit test for `rooms.mediaEdit` --- apps/meteor/tests/data/rooms.helper.ts | 7 +++ apps/meteor/tests/end-to-end/api/rooms.ts | 53 ++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts index 29059cf2f42b6..d17794eaaef84 100644 --- a/apps/meteor/tests/data/rooms.helper.ts +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -141,3 +141,10 @@ export const addUserToRoom = ({ }), }); }; + +export const uploadFile = ({ rid, fileUrl }: { rid: IRoom['_id']; fileUrl: string }) => { + return request + .post(api(`rooms.media/${rid}`)) + .set(credentials) + .attach('file', fileUrl); +}; diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index 94a1e3b1eca6a..a0f855a5fc4ad 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -14,7 +14,7 @@ import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper'; import { assignRoleToUser, createCustomRole, deleteCustomRole } from '../../data/roles.helper'; -import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom, uploadFile } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; import { password } from '../../data/user'; import type { TestUser } from '../../data/users.helper'; @@ -897,6 +897,57 @@ describe('[Rooms]', () => { }); }); + describe('/rooms.mediaEdit', () => { + let testChannel: IRoom; + let user: TestUser; + let file: { _id: string; url: string }; + const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; + + before(async () => { + user = await createUser({ joinDefaultChannels: false }); + testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; + file = (await uploadFile({ rid: testChannel._id, fileUrl: imgURL })).body.file; + }); + + after(() => Promise.all([deleteRoom({ type: 'c', roomId: testChannel._id }), deleteUser(user)])); + + it('should throw error when file id is invalid', (done) => { + void request + .post(api(`rooms.mediaEdit/${testChannel._id}/invalid`)) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-params'); + }) + .end(done); + }); + + it('should throw error when fileName param is not provided', (done) => { + void request + .post(api(`rooms.mediaEdit/${testChannel._id}/${file._id}`)) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-params'); + }) + .end(done); + }); + + it('should update the file name', (done) => { + void request + .post(api(`rooms.mediaEdit/${testChannel._id}/${file._id}`)) + .set(credentials) + .send({ fileName: 'test.png' }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + }); + describe('/rooms.favorite', () => { let testChannel: IRoom; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; From 3fe2d3b12dcaeb73862854bdc39b9a83cfa7762a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 7 Mar 2025 14:43:21 -0300 Subject: [PATCH 150/215] test: adapt current e2e tests to the new behavior --- .../client/lib/chats/flows/uploadFiles.ts | 10 +- .../views/room/composer/ComposerMessage.tsx | 4 +- .../messageBox/MessageComposerFile.tsx | 22 +++-- .../MessageComposerFileComponent.tsx | 3 +- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 95 ++++++++++--------- apps/meteor/tests/e2e/file-upload.spec.ts | 61 ++++++------ apps/meteor/tests/e2e/image-gallery.spec.ts | 15 ++- apps/meteor/tests/e2e/message-actions.spec.ts | 10 +- .../meteor/tests/e2e/message-composer.spec.ts | 4 +- .../page-objects/fragments/home-content.ts | 42 ++++---- apps/meteor/tests/e2e/threads.spec.ts | 14 +-- 11 files changed, 160 insertions(+), 120 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 88d2dbea895ba..0e981f8e4ff70 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -45,11 +45,19 @@ export const uploadFiles = async ( const e2eRoom = await e2e.getInstanceByRoomId(room._id); - if (!e2eRoom || !settings.get('E2E_Enable_Encrypt_Files')) { + if (!e2eRoom) { uploadFile(file); return; } + if (!settings.get('E2E_Enable_Encrypt_Files')) { + dispatchToastMessage({ + type: 'error', + message: t('Encrypted_file_not_allowed'), + }); + return; + } + if (!(await e2eRoom.readyToEncrypt())) { uploadFile(file); return; diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 5dab30922eea9..de47be6f8c4f4 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -46,13 +46,11 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, - tmid, }: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; - tmid?: IMessage['_id']; }): Promise => { try { await chat?.action.stop('typing'); @@ -78,7 +76,7 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac onNavigateToPreviousMessage: () => chat?.messageEditing.toPreviousMessage(), onNavigateToNextMessage: () => chat?.messageEditing.toNextMessage(), }), - [chat?.data, chat?.flows, chat?.action, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, onSend], + [chat?.data, chat?.action, chat?.flows, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, tmid, onSend], ); const publicationReady = useReactiveValue( diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx index 848f8854b09bd..cc5963bd814ea 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx @@ -2,6 +2,7 @@ import { IconButton } from '@rocket.chat/fuselage'; import { useSetModal } from '@rocket.chat/ui-contexts'; import { useState } from 'react'; import type { AllHTMLAttributes, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; import MessageComposerFileComponent from './MessageComposerFileComponent'; import MessageComposerFileError from './MessageComposerFileError'; @@ -20,12 +21,13 @@ type MessageComposerFileProps = { } & Omit, 'is'>; const MessageComposerFile = ({ upload, onRemove, onEdit, onCancel, ...props }: MessageComposerFileProps): ReactElement => { + const { t } = useTranslation(); const [isHover, setIsHover] = useState(false); const setModal = useSetModal(); const fileSize = formatBytes(upload.file.size, 2); const fileExtension = getMimeType(upload.file.type, upload.file.name); - const isLoading = upload.percentage > 0 && upload.percentage !== 100; + const isLoading = upload.percentage !== 100 && !upload.error; const handleOpenFilePreview = () => { setModal( @@ -44,25 +46,27 @@ const MessageComposerFile = ({ upload, onRemove, onEdit, onCancel, ...props }: M const dismissAction = isLoading ? () => onCancel(upload.id) : () => onRemove(upload.id); const handleDismiss = usePreventPropagation(dismissAction); - if (upload.error) { - return ( - : } - /> + const actionIcon = + isLoading && !isHover ? ( + + ) : ( + ); + + if (upload.error) { + return ; } return ( ['Enter', 'Space'].includes(e.code) && handleOpenFilePreview()} onMouseLeave={() => setIsHover(false)} onMouseEnter={() => setIsHover(true)} fileTitle={upload.file.name} fileSubtitle={`${fileSize} - ${fileExtension}`} - actionIcon={isLoading && !isHover ? : } + actionIcon={actionIcon} {...props} /> ); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx index a42fbf44ac1ca..1a54955d06943 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileComponent.tsx @@ -31,6 +31,7 @@ const MessageComposerFileComponent = ({ fileTitle, fileSubtitle, actionIcon, err tabIndex={0} role='button' rcx-input-box__wrapper + readOnly={error} className={previewWrapperStyle} display='flex' padding={4} @@ -48,7 +49,7 @@ const MessageComposerFileComponent = ({ fileTitle, fileSubtitle, actionIcon, err {fileTitle} - + {fileSubtitle} diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 39ba5fe64d015..aff8b30ba8259 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -159,20 +159,22 @@ test.describe.serial('e2e-encryption initial setup', () => { await page.goto('/home'); const channelName = faker.string.uuid(); + const updatedFileName = 'any_file1.txt'; await poHomeChannel.sidenav.createEncryptedChannel(channelName); await poHomeChannel.sidenav.openChat(channelName); await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); await test.step('disable E2EE in the room', async () => { await poHomeChannel.tabs.kebab.click(); @@ -189,17 +191,19 @@ test.describe.serial('e2e-encryption initial setup', () => { await page.reload(); await test.step('upload the file in disabled E2EE room', async () => { + const updatedFileName = 'any_file1.txt'; await expect(poHomeChannel.content.encryptedRoomHeaderIcon).not.toBeVisible(); await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); await test.step('Enable E2EE in the room', async () => { @@ -499,16 +503,20 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/direct/user2${Users.userE2EE.data.username}`); await poHomeChannel.tabs.kebab.click({ force: true }); - await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); - await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); - await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible(); - await page.getByRole('button', { name: 'Enable encryption' }).click(); - await page.waitForTimeout(1000); + // only enabled e2e if its available + const btnEnableE2EE = poHomeChannel.tabs.btnEnableE2E; + if (await btnEnableE2EE.isVisible()) { + await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); + await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible(); + await page.getByRole('button', { name: 'Enable encryption' }).click(); + await page.waitForTimeout(1000); + await poHomeChannel.dismissToast(); + } else { + await poHomeChannel.content.encryptedRoomHeaderIcon.first().click(); + } await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); - await poHomeChannel.dismissToast(); - await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableOTR).toBeVisible(); await poHomeChannel.tabs.btnEnableOTR.click({ force: true }); @@ -540,14 +548,16 @@ test.describe.serial('e2e-encryption', () => { }); await test.step('send a file in channel', async () => { + const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); }); @@ -569,29 +579,26 @@ test.describe.serial('e2e-encryption', () => { }); await test.step('send a text file in channel', async () => { + const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('message 1'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('message 1'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); await test.step('set whitelisted media type setting', async () => { await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: 'text/plain' }); }); - await test.step('send text file again with whitelist setting set', async () => { + await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('message 2'); - await poHomeChannel.content.fileNameInput.fill('any_file2.txt'); - await poHomeChannel.content.btnModalConfirm.click(); - + await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).toHaveAttribute('readonly'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt'); }); await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => { @@ -599,14 +606,14 @@ test.describe.serial('e2e-encryption', () => { }); await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { + const composerFilesLocator = poHomeChannel.content.getFileComposerByName('any_file.txt'); + const composerFiles = await composerFilesLocator.all(); + await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('message 3'); - await poHomeChannel.content.fileNameInput.fill('any_file3.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await expect(composerFilesLocator).toHaveCount(2); + await Promise.all(composerFiles.map((file) => expect(file).toHaveAttribute('readonly'))); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt'); }); }); @@ -645,15 +652,11 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); - await test.step('send a text file in channel, file should not be encrypted', async () => { + await test.step('should not attach files to the composer', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).not.toBeVisible(); + await expect(poHomeChannel.content.btnSendMainComposer).toBeDisabled(); }); }); }); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 626162df47d3f..aca4cc09ce456 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -27,53 +27,59 @@ test.describe.serial('file-upload', () => { expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); - test('expect successfully cancel upload', async () => { + test('should cancel upload', async () => { + const fileName = 'any_file.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.btnModalCancel.click(); - await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + await poHomeChannel.content.getFileComposerByName(fileName).getByRole('button', { name: 'Close' }).click(); + + await expect(poHomeChannel.content.getFileComposerByName(fileName)).not.toBeVisible(); }); - test('expect send file not show modal', async () => { + test('should dismiss edit upload modal', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.btnCancelUpdateFileUpload.click(); + await expect(poHomeChannel.content.fileUploadModal).not.toBeVisible(); }); - test('expect send file with name/description updated', async () => { + test('should send file with name updated', async () => { + const updatedFileName = 'any_file.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); - test('expect send lst file succesfully', async () => { + test('should send lst file successfully', async () => { await poHomeChannel.content.dragAndDropLstFile(); - await poHomeChannel.content.descriptionInput.fill('lst_description'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst'); }); - test('expect send drawio (unknown media type) file succesfully', async ({ page }) => { + test('should send drawio (unknown media type) file successfully', async ({ page }) => { + const fileName = 'diagram.drawio'; await page.reload(); - await poHomeChannel.content.sendFileMessage('diagram.drawio'); - await poHomeChannel.content.descriptionInput.fill('drawio_description'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.sendFileMessage(fileName); + await poHomeChannel.content.btnSendMainComposer.click(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('diagram.drawio'); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(fileName); }); - test('expect not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { + test('should not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { + const fileName = 'diagram.drawio'; await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); await page.reload(); - await poHomeChannel.content.sendFileMessage('diagram.drawio'); - await expect(poHomeChannel.content.btnModalConfirm).not.toBeVisible(); + await poHomeChannel.content.sendFileMessage(fileName); + + await expect(poHomeChannel.content.getFileComposerByName(fileName)).toHaveAttribute('readonly'); }); }); @@ -96,8 +102,9 @@ test.describe('file-upload-not-member', () => { expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); - test('expect not be able to upload if not a member', async () => { + test('should not be able to upload if not a member', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + + await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 526e695870df6..40e9899f0697e 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -38,17 +38,24 @@ test.describe.serial('Image Gallery', async () => { test.describe('When sending an image as a file', () => { test.beforeAll(async () => { + const largeFileName = 'test-large-image.jpeg'; + const updatedLargeFileName = 'test-large-image2.jpeg'; await poHomeChannel.sidenav.openChat(targetChannel); + for await (const imageName of imageNames) { await poHomeChannel.content.sendFileMessage(imageName); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(imageName); } await poHomeChannel.sidenav.openChat(targetChannelLargeImage); - await poHomeChannel.content.sendFileMessage('test-large-image.jpeg'); - await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.content.lastUserMessage).toContainText('test-large-image.jpeg'); + await poHomeChannel.content.sendFileMessage(largeFileName); + await poHomeChannel.content.getFileComposerByName(largeFileName).getByRole('button', { name: 'Close' }).waitFor({ state: 'visible' }); + await poHomeChannel.content.getFileComposerByName(largeFileName).click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedLargeFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendMainComposer.click(); + await expect(poHomeChannel.content.lastUserMessage).toContainText(updatedLargeFileName); await poHomeChannel.content.lastUserMessage.locator('img.gallery-item').click(); }); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index 1dbaf04b3b9be..b5e707c1d42fa 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -239,7 +239,7 @@ test.describe.serial('message-actions', () => { test('expect forward text file to channel', async () => { const filename = 'any_file.txt'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -251,7 +251,7 @@ test.describe.serial('message-actions', () => { test('expect forward image file to channel', async () => { const filename = 'test-image.jpeg'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -263,7 +263,7 @@ test.describe.serial('message-actions', () => { test('expect forward pdf file to channel', async () => { const filename = 'test_pdf_file.pdf'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -275,7 +275,7 @@ test.describe.serial('message-actions', () => { test('expect forward audio message to channel', async () => { const filename = 'sample-audio.mp3'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -287,7 +287,7 @@ test.describe.serial('message-actions', () => { test('expect forward video message to channel', async () => { const filename = 'test_video.mp4'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); diff --git a/apps/meteor/tests/e2e/message-composer.spec.ts b/apps/meteor/tests/e2e/message-composer.spec.ts index 324630078f3c4..2f1bbd304c180 100644 --- a/apps/meteor/tests/e2e/message-composer.spec.ts +++ b/apps/meteor/tests/e2e/message-composer.spec.ts @@ -133,14 +133,14 @@ test.describe.serial('message-composer', () => { await expect(poHomeChannel.audioRecorder).not.toBeVisible(); }); - test('should open file modal when clicking on "Finish recording"', async ({ page }) => { + test('should attach file to the composer when clicking on "Finish recording"', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.composerToolbar.getByRole('button', { name: 'Audio message', exact: true }).click(); await expect(poHomeChannel.audioRecorder).toBeVisible(); await page.waitForTimeout(1000); await poHomeChannel.audioRecorder.getByRole('button', { name: 'Finish Recording', exact: true }).click(); - await expect(poHomeChannel.content.fileUploadModal).toBeVisible(); + await expect(poHomeChannel.content.getFileComposerByName('Audio record.mp3')).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index eab20f959b515..3633191fb7cc7 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -108,34 +108,36 @@ export class HomeContent { await this.page.locator('role=button[name="Forward"]').click(); } - get btnModalCancel(): Locator { - return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--secondary'); - } - get fileUploadModal(): Locator { return this.page.getByRole('dialog', { name: 'File Upload' }); } - get modalFilePreview(): Locator { - return this.page.locator( - '//div[@id="modal-root"]//header//following-sibling::div[1]//div//div//img | //div[@id="modal-root"]//header//following-sibling::div[1]//div//div//div//i', - ); - } - get btnModalConfirm(): Locator { return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--primary'); } - get descriptionInput(): Locator { - return this.page.locator('//div[@id="modal-root"]//fieldset//div[2]//span//input'); - } - get getFileDescription(): Locator { return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="message-body"]'); } - get fileNameInput(): Locator { - return this.page.locator('//div[@id="modal-root"]//fieldset//div[1]//span//input'); + get inputFileUploadName(): Locator { + return this.fileUploadModal.getByRole('textbox', { name: 'File name' }); + } + + get btnUpdateFileUpload(): Locator { + return this.fileUploadModal.getByRole('button', { name: 'Update' }); + } + + get btnCancelUpdateFileUpload(): Locator { + return this.fileUploadModal.getByRole('button', { name: 'Cancel' }); + } + + getFileComposerByName(fileName: string) { + return this.page.getByRole('main').getByRole('button', { name: fileName }); + } + + getThreadFileComposerByName(fileName: string) { + return this.page.getByRole('dialog').getByRole('button', { name: fileName }); } get lastMessageFileName(): Locator { @@ -447,4 +449,12 @@ export class HomeContent { get btnClearSelection() { return this.page.getByRole('button', { name: 'Clear selection' }); } + + get btnSendMainComposer() { + return this.page.getByRole('main').getByRole('button', { name: 'Send', exact: true }); + } + + get btnSendThreadComposer() { + return this.page.getByRole('dialog').getByRole('button', { name: 'Send', exact: true }); + } } diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index 0794102e33b31..e5e5c62ecc2c2 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -67,18 +67,20 @@ test.describe.serial('Threads', () => { await expect(poHomeChannel.content.lastThreadMessageText).toContainText('This is a thread message also sent in channel'); }); }); - test('expect upload a file attachment in thread with description', async ({ page }) => { + test('should send a file with name updated in thread', async ({ page }) => { + const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.lastThreadMessagePreviewText.click(); await expect(page).toHaveURL(/.*thread/); await poHomeChannel.content.dragAndDropTxtFileToThread(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.getThreadFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.content.btnSendThreadComposer.click(); - await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.lastThreadMessageFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText(updatedFileName); }); test.describe('thread message actions', () => { From 726e6acc256fa319e6f583a0aa257f60cc579d9f Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 7 Mar 2025 15:10:12 -0300 Subject: [PATCH 151/215] chore: max multiple uploaded files rule --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 4 ++-- packages/i18n/src/locales/en.i18n.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 0e981f8e4ff70..717d2cac8cb34 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -9,8 +9,8 @@ export const uploadFiles = async ( chat: ChatAPI, { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, ): Promise => { - // TODO: calculate max files based on the new array and the files in the queue - if (uploadsStore.get().length > MAX_MULTIPLE_UPLOADED_FILES) { + const mergedFilesLength = files.length + uploadsStore.get().length; + if (mergedFilesLength > MAX_MULTIPLE_UPLOADED_FILES) { return dispatchToastMessage({ type: 'error', message: t('You_cant_upload_more_than__count__files', { count: MAX_MULTIPLE_UPLOADED_FILES }), diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 194942e8e8aa4..b021f3c1e30d7 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6276,7 +6276,7 @@ "You_can_do_from_account_preferences": "You can do this later from your account preferences", "You_can_search_using_RegExp_eg": "You can search using Regular Expression. e.g. /^text$/i", "You_can_try_to": "You can try to", - "You_cant_upload_more_than__count__files": "You can't upload more than {{count}} files at once. Only the first {{count}} files will be uploaded.", + "You_cant_upload_more_than__count__files": "You can't upload more than {{count}} files at once.", "You_can_use_an_emoji_as_avatar": "You can also use an emoji as an avatar.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate Omnichannel with your CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "You can't leave a omnichannel room. Please, use the close button.", From d8a38e3dbb24aa66905e52b35acd501189acd405 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 7 Mar 2025 16:08:37 -0300 Subject: [PATCH 152/215] fix: uploading continuously action not cleaning after upload --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 2 +- .../lib/chats/flows/processMessageUploads.ts | 5 +++-- .../client/lib/chats/flows/uploadFiles.ts | 1 + apps/meteor/client/lib/chats/uploads.ts | 19 +++---------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index cf484cf7cb54d..119adca65f09b 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -151,7 +151,7 @@ export class ChatMessages implements ChatAPI { this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); this.uploads = createUploadsAPI({ rid }); - this.threadUploads = createUploadsAPI({ rid, tmid }); + this.threadUploads = createUploadsAPI({ rid }); this.ActionManager = params.actionManager; const unimplemented = () => { diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index ff38d639da9c0..0b33d53ae9344 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -145,12 +145,13 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => } as const; try { - await sdk.call('sendMessage', composedMessage, fileUrls, fileIds); chat.composer?.clear(); store.clear(); + await sdk.call('sendMessage', composedMessage, fileUrls, fileIds); } catch (error: unknown) { - console.error(error); dispatchToastMessage({ type: 'error', message: error }); + } finally { + chat.action.stop('uploading'); } return true; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 717d2cac8cb34..80d3960a5f034 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -90,4 +90,5 @@ export const uploadFiles = async ( uploadNextFile(); resetFileInput?.(); + chat?.action.performContinuously('uploading'); }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index c012b9e3c1699..8cc880a4aa0f2 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,9 +1,8 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; import fileSize from 'filesize'; -import { UserAction, USER_ACTIVITIES } from '../../../app/ui/client/lib/UserAction'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI'; import type { Upload } from './Upload'; @@ -15,13 +14,10 @@ import { i18n } from '../../../app/utils/lib/i18n'; class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI { private rid: string; - private tmid?: string; - - constructor({ rid, tmid }: { rid: string; tmid?: IMessage['_id'] }) { + constructor({ rid }: { rid: string }) { super(); this.rid = rid; - this.tmid = tmid; } uploads: readonly Upload[] = []; @@ -180,10 +176,6 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' } }; - if (this.uploads.length) { - UserAction.performContinuously(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); - } - this.once(`cancelling-${id}`, () => { xhr.abort(); this.set(this.uploads.filter((upload) => upload.id !== id)); @@ -204,13 +196,8 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' }; }), ); - } finally { - if (!this.uploads.length) { - UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid }); - } } } } -export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => - new UploadsStore({ rid, tmid }); +export const createUploadsAPI = ({ rid }: { rid: IRoom['_id'] }): UploadsAPI => new UploadsStore({ rid }); From df671b64e229c57ed89277aa37aa359d76189b58 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 10 Mar 2025 14:21:34 -0300 Subject: [PATCH 153/215] fix: keep encryption when renaming file --- .../app/e2e/client/rocketchat.e2e.room.ts | 4 +-- apps/meteor/client/lib/chats/Upload.ts | 5 +++ .../lib/chats/flows/processMessageUploads.ts | 31 ++++++++++--------- apps/meteor/client/lib/chats/uploads.ts | 16 +++++----- packages/core-typings/src/IUpload.ts | 2 ++ 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts index 423e479e13df1..1efaaa85a5792 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts @@ -1,5 +1,5 @@ import { Base64 } from '@rocket.chat/base64'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, AtLeast } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, AtLeast, IUpload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { Optional } from '@tanstack/react-query'; import EJSON from 'ejson'; @@ -631,7 +631,7 @@ export class E2ERoom extends Emitter { // Helper function for encryption of content async encryptMessageContent( - contentToBeEncrypted: Pick & Optional, 'msg'>, + contentToBeEncrypted: (Pick & Optional, 'msg'>) | Partial, ) { const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); diff --git a/apps/meteor/client/lib/chats/Upload.ts b/apps/meteor/client/lib/chats/Upload.ts index 720a9682dc1ac..daa07cc03c66e 100644 --- a/apps/meteor/client/lib/chats/Upload.ts +++ b/apps/meteor/client/lib/chats/Upload.ts @@ -1,3 +1,5 @@ +import type { IUpload } from '@rocket.chat/core-typings'; + export type NonEncryptedUpload = { readonly id: string; readonly file: File; @@ -8,6 +10,7 @@ export type NonEncryptedUpload = { export type EncryptedUpload = NonEncryptedUpload & { readonly encryptedFile: EncryptedFile; + readonly metadataForEncryption: Partial; }; export type Upload = EncryptedUpload | NonEncryptedUpload; @@ -19,3 +22,5 @@ export type EncryptedFile = { type: File['type']; hash: string; }; + +export const isEncryptedUpload = (upload: Upload): upload is EncryptedUpload => 'encryptedFile' in upload; diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 0b33d53ae9344..9ba94f2e981ed 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -1,4 +1,4 @@ -import type { AtLeast, FileAttachmentProps, IMessage } from '@rocket.chat/core-typings'; +import type { AtLeast, FileAttachmentProps, IMessage, IUploadToConfirm } from '@rocket.chat/core-typings'; import { e2e } from '../../../../app/e2e/client'; import type { E2ERoom } from '../../../../app/e2e/client/rocketchat.e2e.room'; @@ -6,7 +6,7 @@ import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; -import type { EncryptedUpload } from '../Upload'; +import { isEncryptedUpload, type EncryptedUpload } from '../Upload'; const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { return new Promise((resolve) => { @@ -113,19 +113,22 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => return false; } - const { fileUrls, fileIds } = filesToUpload.reduce<{ fileUrls: string[]; fileIds: string[] }>( - (acc, upload) => { - if (!upload.url || !upload.id) { - return acc; - } + const fileUrls: string[] = []; + const filesToConfirm: IUploadToConfirm[] = []; - acc.fileIds.push(upload.id); - acc.fileUrls.push(upload.url); + for await (const upload of filesToUpload) { + if (!upload.url || !upload.id) { + continue; + } - return acc; - }, - { fileUrls: [], fileIds: [] }, - ); + let content; + if (e2eRoom && isEncryptedUpload(upload)) { + content = await e2eRoom.encryptMessageContent(upload.metadataForEncryption); + } + + fileUrls.push(upload.url); + filesToConfirm.push({ _id: upload.id, name: upload.file.name, content }); + } const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg }); @@ -147,7 +150,7 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => try { chat.composer?.clear(); store.clear(); - await sdk.call('sendMessage', composedMessage, fileUrls, fileIds); + await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); } finally { diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 8cc880a4aa0f2..86a90277e0e22 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -5,7 +5,7 @@ import fileSize from 'filesize'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI'; -import type { Upload } from './Upload'; +import { isEncryptedUpload, type Upload } from './Upload'; import { settings } from '../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; @@ -45,18 +45,19 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' editUploadFileName = async (uploadId: Upload['id'], fileName: Upload['file']['name']): Promise => { try { - await sdk.rest.post(`/v1/rooms.mediaEdit/${this.rid}/${uploadId}`, { - fileName, - }); - this.set( this.uploads.map((upload) => { if (upload.id !== uploadId) { return upload; } - // TODO reencrypt file - return { ...upload, file: new File([upload.file], fileName, upload.file) }; + return { + ...upload, + file: new File([upload.file], fileName, upload.file), + ...(isEncryptedUpload(upload) && { + metadataForEncryption: { ...upload.metadataForEncryption, name: fileName }, + }), + }; }), ); } catch (error) { @@ -90,6 +91,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' file: encrypted ? encrypted.rawFile : file, percentage: 0, encryptedFile: encrypted?.encryptedFile, + metadataForEncryption: encrypted?.fileContent.raw, }, ]); diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 8bcabee2fa8de..e39726c7f3f29 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -70,4 +70,6 @@ export type IE2EEUpload = IUpload & { }; }; +export type IUploadToConfirm = Pick; + export const isE2EEUpload = (upload: IUpload): upload is IE2EEUpload => Boolean(upload?.content?.ciphertext && upload?.content?.algorithm); From fbaa9908a3154396c796740a90647150f657fbed Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Mar 2025 15:25:09 -0300 Subject: [PATCH 154/215] backend renaming of encrypted files --- .../app/lib/server/functions/sendMessage.ts | 36 ++++++++++++++++--- .../app/lib/server/methods/sendMessage.ts | 26 ++++++++++---- .../src/models/IBaseUploadsModel.ts | 2 ++ packages/models/src/models/BaseUploadModel.ts | 10 ++++++ 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 3439b37c24a8c..768223aceda2d 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,6 @@ import { Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import { isE2EEMessage, type IMessage, type IRoom, type IUpload } from '@rocket.chat/core-typings'; +import { isE2EEMessage, type IMessage, type IRoom, type IUpload, type IUploadToConfirm } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -213,6 +213,24 @@ export function prepareMessageObject( } } +/** + * Update file names on the Uploads collection, as the names may have changed between the upload and the sending of the message + * For encrypted rooms, the full `content` of the file is updated as well, as the name is included there + **/ +const updateFileNames = async (filesToConfirm: IUploadToConfirm[], isE2E: boolean) => { + return Promise.all( + filesToConfirm.map(async (upload) => { + if (isE2E && upload.content) { + // on encrypted files, the `upload.name` is an useless attribute, so it doesn't need to be updated + // the name will be loaded from the encrypted data on `upload.content` instead + await Uploads.updateFileContentById(upload._id, upload.content); + } else if (upload.name) { + await Uploads.updateFileNameById(upload._id, upload.name); + } + }), + ); +}; + /** * Validates and sends the message object. */ @@ -222,15 +240,23 @@ export const sendMessage = async ( room: any, upsert = false, previewUrls?: string[], - uploadIdsToConfirm?: string[], + filesToConfirm?: IUploadToConfirm[], ) => { if (!user || !message || !room._id) { return false; } - if (uploadIdsToConfirm !== undefined && !isE2EEMessage(message)) { - const filesToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm).toArray(); - const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(filesToConfirm, message.rid, user); + const isE2E = isE2EEMessage(message); + + if (filesToConfirm) { + await updateFileNames(filesToConfirm, isE2E); + } + + const uploadIdsToConfirm = filesToConfirm?.map(({ _id }) => _id); + + if (uploadIdsToConfirm !== undefined && !isE2E) { + const uploadsToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm).toArray(); + const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(uploadsToConfirm, message.rid, user); message.files = files; message.attachments = attachments; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 4e60ff8a67ba6..5d9a589f84ae3 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, IUploadToConfirm } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { Messages, Users } from '@rocket.chat/models'; @@ -23,7 +23,7 @@ export async function executeSendMessage( uid: IUser['_id'], message: AtLeast, previewUrls?: string[], - uploadIdsToConfirm?: string[], + filesToConfirm?: IUploadToConfirm[], ) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { @@ -101,7 +101,7 @@ export async function executeSendMessage( } metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - return await sendMessage(user, message, room, false, previewUrls, uploadIdsToConfirm); + return await sendMessage(user, message, room, false, previewUrls, filesToConfirm); } catch (err: any) { SystemLogger.error({ msg: 'Error sending message:', err }); @@ -122,12 +122,12 @@ export async function executeSendMessage( declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - sendMessage(message: AtLeast, previewUrls?: string[], uploadIdsToConfirm?: string[]): any; + sendMessage(message: AtLeast, previewUrls?: string[], filesToConfirm?: IUploadToConfirm[]): any; } } Meteor.methods({ - async sendMessage(message, previewUrls, uploadIdsToConfirm) { + async sendMessage(message, previewUrls, filesToConfirm) { check(message, { _id: Match.Maybe(String), rid: Match.Maybe(String), @@ -147,7 +147,19 @@ Meteor.methods({ sentByEmail: Match.Maybe(Boolean), }); - check(uploadIdsToConfirm, Match.Maybe([String])); + check( + filesToConfirm, + Match.Maybe([ + Match.ObjectIncluding({ + _id: String, + name: Match.Maybe(String), + content: Match.Maybe({ + algorithm: String, + ciphertext: String, + }), + }), + ]), + ); const uid = Meteor.userId(); if (!uid) { @@ -161,7 +173,7 @@ Meteor.methods({ } try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, previewUrls, uploadIdsToConfirm)); + return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, previewUrls, filesToConfirm)); } catch (error: any) { if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { throw new Meteor.Error(error.error || error.message, error.reason, { diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index dddf53b6bc3ab..427974583e6ec 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -22,5 +22,7 @@ export interface IBaseUploadsModel extends IBaseModel { updateFileNameById(fileId: string, name: string): Promise; + updateFileContentById(fileId: string, content: IUpload['content']): Promise; + deleteFile(fileId: string): Promise; } diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index e8af63751dd3b..cd1f8a416b5f9 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -151,6 +151,16 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } + async updateFileContentById(fileId: string, content: IUpload['content']): Promise { + const filter = { _id: fileId }; + const update = { + $set: { + content, + }, + }; + return this.updateOne(filter, update); + } + async deleteFile(fileId: string): Promise { return this.deleteOne({ _id: fileId }); } From 163525493591baa6ec9c5415677e7a1ce20da86c Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 10 Mar 2025 16:07:06 -0300 Subject: [PATCH 155/215] fix: encrypted message being send --- apps/meteor/client/lib/chats/flows/processMessageUploads.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 9ba94f2e981ed..a525b8caa5c33 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -144,13 +144,13 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => content, ...(e2eRoom && { t: 'e2e', + msg: '', }), } as const; try { - chat.composer?.clear(); - store.clear(); await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); + store.clear(); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); } finally { From 33b659d5ad074ec148b5ac4e5761e6fcbd5220a3 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 12 Mar 2025 09:32:55 -0300 Subject: [PATCH 156/215] fix: remove `rooms.mediaEdit` --- apps/meteor/app/api/server/v1/rooms.ts | 23 ---------- apps/meteor/tests/data/rooms.helper.ts | 7 --- apps/meteor/tests/end-to-end/api/rooms.ts | 53 +---------------------- packages/rest-typings/src/v1/rooms.ts | 17 -------- 4 files changed, 1 insertion(+), 99 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 6f9a293f7dc8d..e4d10886dff76 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -13,7 +13,6 @@ import { isRoomsOpenProps, isRoomsMembersOrderedByRoleProps, isRoomsHideProps, - isMediaEditProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -334,28 +333,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.mediaEdit/:rid/:fileId', - { authRequired: true, validateParams: isMediaEditProps }, - { - async post() { - const { fileName } = this.bodyParams; - - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { - return API.v1.forbidden(); - } - - const { matchedCount } = await Uploads.updateFileNameById(this.urlParams.fileId, fileName); - - if (matchedCount === 0) { - throw new Error('invalid-file'); - } - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts index d17794eaaef84..29059cf2f42b6 100644 --- a/apps/meteor/tests/data/rooms.helper.ts +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -141,10 +141,3 @@ export const addUserToRoom = ({ }), }); }; - -export const uploadFile = ({ rid, fileUrl }: { rid: IRoom['_id']; fileUrl: string }) => { - return request - .post(api(`rooms.media/${rid}`)) - .set(credentials) - .attach('file', fileUrl); -}; diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index a0f855a5fc4ad..94a1e3b1eca6a 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -14,7 +14,7 @@ import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper'; import { assignRoleToUser, createCustomRole, deleteCustomRole } from '../../data/roles.helper'; -import { createRoom, deleteRoom, uploadFile } from '../../data/rooms.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; import { password } from '../../data/user'; import type { TestUser } from '../../data/users.helper'; @@ -897,57 +897,6 @@ describe('[Rooms]', () => { }); }); - describe('/rooms.mediaEdit', () => { - let testChannel: IRoom; - let user: TestUser; - let file: { _id: string; url: string }; - const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; - - before(async () => { - user = await createUser({ joinDefaultChannels: false }); - testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; - file = (await uploadFile({ rid: testChannel._id, fileUrl: imgURL })).body.file; - }); - - after(() => Promise.all([deleteRoom({ type: 'c', roomId: testChannel._id }), deleteUser(user)])); - - it('should throw error when file id is invalid', (done) => { - void request - .post(api(`rooms.mediaEdit/${testChannel._id}/invalid`)) - .set(credentials) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); - }) - .end(done); - }); - - it('should throw error when fileName param is not provided', (done) => { - void request - .post(api(`rooms.mediaEdit/${testChannel._id}/${file._id}`)) - .set(credentials) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); - }) - .end(done); - }); - - it('should update the file name', (done) => { - void request - .post(api(`rooms.mediaEdit/${testChannel._id}/${file._id}`)) - .set(credentials) - .send({ fileName: 'test.png' }) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - describe('/rooms.favorite', () => { let testChannel: IRoom; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index c576af4c51b63..60aabf356cb28 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -688,19 +688,6 @@ const roomsHideSchema = { export const isRoomsHideProps = ajv.compile(roomsHideSchema); -type RoomsMediaEditProps = { - fileName: string; -}; - -const mediaEditSchema = { - type: 'object', - properties: { fileName: { type: 'string' } }, - required: ['fileName'], - additionalProperties: false, -}; - -export const isMediaEditProps = ajv.compile(mediaEditSchema); - export type RoomsEndpoints = { '/v1/rooms.autocomplete.channelAndPrivate': { GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => { @@ -805,10 +792,6 @@ export type RoomsEndpoints = { }) => { message: IMessage | null }; }; - '/v1/rooms.mediaEdit/:rid/:fileId': { - POST: (params: { fileName: string }) => void; - }; - '/v1/rooms.saveNotification': { POST: (params: { roomId: string; notifications: Notifications }) => void; }; From 29404aeb1af0333d210c486a13d8259c05c37e19 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 12 Mar 2025 10:40:15 -0300 Subject: [PATCH 157/215] chore: disable update button when form is not dirty --- .../views/room/modals/FileUploadModal/FileUploadModal.tsx | 4 ++-- apps/meteor/tests/e2e/file-upload.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index ed392c9c14e41..1c99f41ec6f50 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -21,7 +21,7 @@ const FileUploadModal = ({ onClose, file, fileName, onSubmit }: FileUploadModalP const { register, handleSubmit, - formState: { errors, isSubmitting }, + formState: { errors, isDirty, isSubmitting }, } = useForm({ mode: 'onBlur', defaultValues: { name: fileName } }); return ( @@ -64,7 +64,7 @@ const FileUploadModal = ({ onClose, file, fileName, onSubmit }: FileUploadModalP - diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index aca4cc09ce456..bcb1cf0d084f6 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -43,7 +43,7 @@ test.describe.serial('file-upload', () => { }); test('should send file with name updated', async () => { - const updatedFileName = 'any_file.txt'; + const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); From 7f50d02d0e30fe71353a6cbd176d7ef989548c65 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 12 Mar 2025 16:48:41 -0300 Subject: [PATCH 158/215] fix: review --- apps/meteor/app/lib/server/functions/sendMessage.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 768223aceda2d..9f9527628fb87 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -220,10 +220,12 @@ export function prepareMessageObject( const updateFileNames = async (filesToConfirm: IUploadToConfirm[], isE2E: boolean) => { return Promise.all( filesToConfirm.map(async (upload) => { - if (isE2E && upload.content) { + if (isE2E) { // on encrypted files, the `upload.name` is an useless attribute, so it doesn't need to be updated // the name will be loaded from the encrypted data on `upload.content` instead - await Uploads.updateFileContentById(upload._id, upload.content); + if (upload.content) { + await Uploads.updateFileContentById(upload._id, upload.content); + } } else if (upload.name) { await Uploads.updateFileNameById(upload._id, upload.name); } From 8bac67fecb50c017ee999deca413fd46b7c9848a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 7 Apr 2025 13:27:08 -0300 Subject: [PATCH 159/215] chore: changeset --- .changeset/mighty-moose-return.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/mighty-moose-return.md diff --git a/.changeset/mighty-moose-return.md b/.changeset/mighty-moose-return.md new file mode 100644 index 0000000000000..c98d6e00bb556 --- /dev/null +++ b/.changeset/mighty-moose-return.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces the ability to upload multiple files and send it into a single message From 769c798537703d577304bd76e0c79991fb8b2f23 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 7 Apr 2025 13:40:24 -0300 Subject: [PATCH 160/215] chore: add comment to the ImageGallery workaround --- apps/meteor/client/providers/ImageGalleryProvider.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index 134405fae4aab..a8b7a94afdf20 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -23,6 +23,13 @@ const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { return setSingleImageUrl(target.dataset.id); } if (target?.classList.contains('gallery-item')) { + /** + * When sharing multiple files, the preview incorrectly always showed the first image. + * This was add to ensure the clicked image is displayed in the preview. + * ROOT CAUSE: `RoomMessageContent` component was only passing the first file ID to attachment elements. + * SOLUTION: We likely need to store the individual file ID within each attachment element + * and use the initially passed first ID as a fallback for older records. + */ const idFromSrc = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; From 23e940759faf8e9c7a4b2742948b8ce0cbba4f06 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 16 Jun 2025 11:48:05 -0300 Subject: [PATCH 161/215] improvements to backend code --- apps/meteor/app/api/server/v1/rooms.ts | 2 +- .../app/lib/server/functions/sendMessage.ts | 4 +- .../lib/chats/flows/processMessageUploads.ts | 115 ++++++++---------- packages/models/src/models/BaseUploadModel.ts | 8 -- 4 files changed, 52 insertions(+), 77 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 446374d934684..b946590e9dddb 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -330,7 +330,7 @@ API.v1.addRoute( sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }), ); - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + await Uploads.confirmTemporaryFile(file._id, this.userId); const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 9f9527628fb87..9508867e50bd6 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -256,7 +256,7 @@ export const sendMessage = async ( const uploadIdsToConfirm = filesToConfirm?.map(({ _id }) => _id); - if (uploadIdsToConfirm !== undefined && !isE2E) { + if (uploadIdsToConfirm?.length && !isE2E) { const uploadsToConfirm: Partial[] = await Uploads.findByIds(uploadIdsToConfirm).toArray(); const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(uploadsToConfirm, message.rid, user); message.files = files; @@ -335,7 +335,7 @@ export const sendMessage = async ( // TODO: is there an opportunity to send returned data to notifyOnMessageChange? await afterSaveMessage(message, room); - if (uploadIdsToConfirm !== undefined) { + if (uploadIdsToConfirm?.length) { await Uploads.confirmTemporaryFiles(uploadIdsToConfirm, user._id); } diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index a525b8caa5c33..22a1d1fd3414e 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -21,77 +21,60 @@ const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number }); }; -const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2eRoom: E2ERoom, msg: string) => { - const attachments = []; - const arrayOfFiles = []; - - const imgDimensions = await Promise.all( - filesToUpload.map(({ file }) => { - if (/^image\/.+/.test(file.type)) { - return getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); - } - return null; +const getAttachmentForFile = async (fileToUpload: EncryptedUpload): Promise => { + const attachment: FileAttachmentProps = { + title: fileToUpload.file.name, + type: 'file', + title_link: fileToUpload.url, + title_link_download: true, + encryption: { + key: fileToUpload.encryptedFile.key, + iv: fileToUpload.encryptedFile.iv, + }, + hashes: { + sha256: fileToUpload.encryptedFile.hash, + }, + }; + + const fileType = fileToUpload.file.type.match(/^(image|audio|video)\/.+/)?.[1] as 'image' | 'audio' | 'video' | undefined; + + if (!fileType) { + return { + ...attachment, + size: fileToUpload.file.size, + format: getFileExtension(fileToUpload.file.name), + }; + } + + return { + ...attachment, + [`${fileType}_url`]: fileToUpload.url, + [`${fileType}_type`]: fileToUpload.file.type, + [`${fileType}_size`]: fileToUpload.file.size, + ...(fileType === 'image' && { + image_dimensions: await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(fileToUpload.file)), }), - ); + }; +}; - for (let i = 0; i < filesToUpload.length; i++) { - const attachment: FileAttachmentProps = { - title: filesToUpload[i].file.name, - type: 'file', - title_link: filesToUpload[i].url, - title_link_download: true, - encryption: { - key: filesToUpload[i].encryptedFile.key, - iv: filesToUpload[i].encryptedFile.iv, - }, - hashes: { - sha256: filesToUpload[i].encryptedFile.hash, - }, - }; +const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2eRoom: E2ERoom, msg: string) => { + const attachments: FileAttachmentProps[] = []; - if (/^image\/.+/.test(filesToUpload[i].file.type)) { - const dimensions = imgDimensions[i]; - attachments.push({ - ...attachment, - image_url: filesToUpload[i].url, - image_type: filesToUpload[i].file.type, - image_size: filesToUpload[i].file.size, - ...(dimensions && { - image_dimensions: dimensions, - }), - }); - } else if (/^audio\/.+/.test(filesToUpload[i].file.type)) { - attachments.push({ - ...attachment, - audio_url: filesToUpload[i].url, - audio_type: filesToUpload[i].file.type, - audio_size: filesToUpload[i].file.size, - }); - } else if (/^video\/.+/.test(filesToUpload[i].file.type)) { - attachments.push({ - ...attachment, - video_url: filesToUpload[i].url, - video_type: filesToUpload[i].file.type, - video_size: filesToUpload[i].file.size, - }); - } else { - attachments.push({ - ...attachment, - size: filesToUpload[i].file.size, - format: getFileExtension(filesToUpload[i].file.name), - }); - } + const arrayOfFiles = await Promise.all( + filesToUpload.map(async (fileToUpload) => { + attachments.push(await getAttachmentForFile(fileToUpload)); - const files = { - _id: filesToUpload[i].id, - name: filesToUpload[i].file.name, - type: filesToUpload[i].file.type, - size: filesToUpload[i].file.size, - format: getFileExtension(filesToUpload[i].file.name), - }; + const file = { + _id: fileToUpload.id, + name: fileToUpload.file.name, + type: fileToUpload.file.type, + size: fileToUpload.file.size, + format: getFileExtension(fileToUpload.file.name), + }; - arrayOfFiles.push(files); - } + return file; + }), + ); return e2eRoom.encryptMessageContent({ attachments, diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index 51d949eb93359..a57cac396fb1e 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -74,10 +74,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo } confirmTemporaryFile(fileId: string, userId: string): Promise | undefined { - if (!fileId) { - return; - } - const filter = { _id: fileId, userId, @@ -93,10 +89,6 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo } confirmTemporaryFiles(fileIds: string[], userId: string): Promise | undefined { - if (!fileIds.length) { - return; - } - const filter = { _id: { $in: fileIds, From 720add42bf0a6eb1601671007b5c4d15d37318be Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 26 Jun 2025 11:37:24 -0300 Subject: [PATCH 162/215] fix: test locators --- apps/meteor/tests/e2e/image-upload.spec.ts | 6 ++---- apps/meteor/tests/e2e/prune-messages.spec.ts | 1 - apps/meteor/tests/e2e/quote-attachment.spec.ts | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/meteor/tests/e2e/image-upload.spec.ts b/apps/meteor/tests/e2e/image-upload.spec.ts index 1a4bac087b246..77443ad042d7c 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -33,8 +33,7 @@ test.describe('image-upload', () => { test('should show error indicator when upload fails', async () => { await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); - await poHomeChannel.content.fileNameInput.fill('bad-orientation.jpeg'); - await poHomeChannel.content.descriptionInput.fill('bad-orientation_description'); + await poHomeChannel.content.inputFileUploadName.fill('bad-orientation.jpeg'); await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.statusUploadIndicator).toContainText('Error:'); @@ -48,8 +47,7 @@ test.describe('image-upload', () => { test('should succeed upload of bad-orientation.jpeg', async () => { await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); - await poHomeChannel.content.fileNameInput.fill('bad-orientation.jpeg'); - await poHomeChannel.content.descriptionInput.fill('bad-orientation_description'); + await poHomeChannel.content.inputFileUploadName.fill('bad-orientation.jpeg'); await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('bad-orientation_description'); diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index bdb0c0028aefb..5c1a14c7dbb16 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -43,7 +43,6 @@ test.describe('prune-messages', () => { const { alert, dismiss } = poToastBar; await content.sendFileMessage('any_file.txt'); - await content.descriptionInput.fill('a message with a file'); await content.btnModalConfirm.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); diff --git a/apps/meteor/tests/e2e/quote-attachment.spec.ts b/apps/meteor/tests/e2e/quote-attachment.spec.ts index 95fe56af882fb..f17b41e856097 100644 --- a/apps/meteor/tests/e2e/quote-attachment.spec.ts +++ b/apps/meteor/tests/e2e/quote-attachment.spec.ts @@ -30,8 +30,7 @@ test.describe.parallel('Quote Attachment', () => { const imageFileName = 'test-image.jpeg'; await test.step('Send message with attachment in the channel', async () => { await poHomeChannel.content.sendFileMessage(imageFileName); - await poHomeChannel.content.fileNameInput.fill(imageFileName); - await poHomeChannel.content.descriptionInput.fill(fileDescription); + await poHomeChannel.content.inputFileUploadName.fill(imageFileName); await poHomeChannel.content.btnModalConfirm.click(); // Wait for the file to be uploaded and message to be sent @@ -71,8 +70,7 @@ test.describe.parallel('Quote Attachment', () => { await expect(page).toHaveURL(/.*thread/); await poHomeChannel.content.dragAndDropTxtFileToThread(); - await poHomeChannel.content.descriptionInput.fill(fileDescription); - await poHomeChannel.content.fileNameInput.fill(textFileName); + await poHomeChannel.content.inputFileUploadName.fill(textFileName); await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText(fileDescription); From 4286095291c07fbaa4f366b0054ce6cf19f2399c Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 3 Jul 2025 16:57:52 -0300 Subject: [PATCH 163/215] chore: update changeset --- .changeset/mighty-moose-return.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mighty-moose-return.md b/.changeset/mighty-moose-return.md index c98d6e00bb556..ca0c8bbd3ccbf 100644 --- a/.changeset/mighty-moose-return.md +++ b/.changeset/mighty-moose-return.md @@ -6,4 +6,4 @@ '@rocket.chat/meteor': minor --- -Introduces the ability to upload multiple files and send it into a single message +Introduces the ability to upload multiple files and send them in a single message From e2b0cd89dc4645c871fe6ed6cd1bdce5bd10faa7 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 3 Jul 2025 16:57:59 -0300 Subject: [PATCH 164/215] i18n: missing translation --- packages/i18n/src/locales/en.i18n.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index dd2f5bc981750..0259f7a5f6e59 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5300,6 +5300,7 @@ "Upgrade_tab_upgrade_your_plan": "Upgrade your plan", "Upgrade_to_Pro": "Upgrade to Pro", "Upload": "Upload", + "Upload_failed": "Upload failed", "Upload_Folder_Path": "Upload Folder Path", "Upload_From": "Upload from {{name}}", "Upload_anyway": "Upload anyway", From 8790dee579b6488379ed68e2286bb8ab5f198d25 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 3 Jul 2025 17:09:53 -0300 Subject: [PATCH 165/215] fix: review --- apps/meteor/client/providers/ImageGalleryProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index a8b7a94afdf20..1681126e6054f 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -30,7 +30,7 @@ const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { * SOLUTION: We likely need to store the individual file ID within each attachment element * and use the initially passed first ID as a fallback for older records. */ - const idFromSrc = target.dataset.src?.split('/file-upload/')[1].split('/')[0]; + const idFromSrc = target.dataset.src?.split('/file-upload/')[1]?.split('/')[0]; const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; return setImageId(idFromSrc || target.dataset.id || id); From eb94859d18c0228a38d2b7939d379526580b3a8a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 3 Jul 2025 17:31:49 -0300 Subject: [PATCH 166/215] refactor: do not disable submit button when updating file name --- .../views/room/modals/FileUploadModal/FileUploadModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 21a6ac50e8d20..af6fb8f94427a 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -28,7 +28,7 @@ const FileUploadModal = ({ onClose, file, fileName, onSubmit }: FileUploadModalP ) => ( - onSubmit(name))} {...props} /> + (!isDirty ? onClose() : onSubmit(name)))} {...props} /> )} > @@ -64,7 +64,7 @@ const FileUploadModal = ({ onClose, file, fileName, onSubmit }: FileUploadModalP - From 43445e5ea4470306fb30bdeb5cb7085787ac5306 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 4 Jul 2025 11:13:34 -0300 Subject: [PATCH 167/215] test: e2e-encryption --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 24 ++++++++++++++----- .../fragments/file-upload-modal.ts | 8 +++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 1cddd29ecd28f..e90b2509fd167 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -219,9 +219,15 @@ test.describe('basic features', () => { await test.step('upload the file with encryption', async () => { // Upload a file await encryptedRoomPage.dragAndDropTxtFile(); - await fileUploadModal.setName(fileName); - await fileUploadModal.setDescription(fileDescription); - await fileUploadModal.send(); + + // Update file name and send + await expect(async () => { + await encryptedRoomPage.getFileComposerByName('any_file.txt').click(); + await fileUploadModal.setName(fileName); + await fileUploadModal.update(); + await expect(encryptedRoomPage.getFileComposerByName(fileName)).toBeVisible(); + await encryptedRoomPage.sendMessage(fileDescription); + }).toPass(); // Check the file upload await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); @@ -236,9 +242,15 @@ test.describe('basic features', () => { await test.step('upload the file without encryption', async () => { await encryptedRoomPage.dragAndDropTxtFile(); - await fileUploadModal.setName(fileName); - await fileUploadModal.setDescription(fileDescription); - await fileUploadModal.send(); + + // Update file name and send + await expect(async () => { + await encryptedRoomPage.getFileComposerByName('any_file.txt').click(); + await fileUploadModal.setName(fileName); + await fileUploadModal.update(); + await expect(encryptedRoomPage.getFileComposerByName(fileName)).toBeVisible(); + await encryptedRoomPage.sendMessage(fileDescription); + }).toPass(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts index 92f2915827efa..4ee33ebc8307f 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts @@ -15,8 +15,8 @@ export class FileUploadModal extends Modal { return this.root.getByRole('textbox', { name: 'File description' }); } - private get sendButton() { - return this.root.getByRole('button', { name: 'Send' }); + private get updateButton() { + return this.root.getByRole('button', { name: 'Update' }); } setName(fileName: string) { @@ -27,8 +27,8 @@ export class FileUploadModal extends Modal { return this.fileDescriptionInput.fill(description); } - async send() { - await this.sendButton.click(); + async update() { + await this.updateButton.click(); await this.waitForDismissal(); } } From dfa3435d0336242ee381ba7d5658c74da28853eb Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 4 Jul 2025 15:47:36 -0300 Subject: [PATCH 168/215] test: wait large image request to send message --- apps/meteor/tests/e2e/image-gallery.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 13b3080320b09..23f9ea26cf5c6 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -39,7 +39,7 @@ test.describe.serial('Image Gallery', async () => { test.describe('When sending an image as a file', () => { test.beforeAll(async () => { const largeFileName = 'test-large-image.jpeg'; - const updatedLargeFileName = 'test-large-image2.jpeg'; + await poHomeChannel.sidenav.openChat(targetChannel); for await (const imageName of imageNames) { @@ -50,12 +50,13 @@ test.describe.serial('Image Gallery', async () => { await poHomeChannel.sidenav.openChat(targetChannelLargeImage); await poHomeChannel.content.sendFileMessage(largeFileName); - await poHomeChannel.content.getFileComposerByName(largeFileName).getByRole('button', { name: 'Close' }).waitFor({ state: 'visible' }); - await poHomeChannel.content.getFileComposerByName(largeFileName).click(); - await poHomeChannel.content.inputFileUploadName.fill(updatedLargeFileName); - await poHomeChannel.content.btnUpdateFileUpload.click(); + + await poHomeChannel.page.waitForResponse( + (response) => /api\/v1\/rooms.media/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST', + ); + await poHomeChannel.content.btnSendMainComposer.click(); - await expect(poHomeChannel.content.lastUserMessage).toContainText(updatedLargeFileName); + await expect(poHomeChannel.content.lastUserMessage).toContainText(largeFileName); await poHomeChannel.content.lastUserMessage.locator('img.gallery-item').click(); }); From b3e2541c09677334721d0003e23780f16ab03b42 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 4 Jul 2025 19:27:53 -0300 Subject: [PATCH 169/215] fix: typo --- apps/meteor/client/lib/chats/uploads.ts | 40 +++++++++++++++---------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 86a90277e0e22..55e1e9f43efea 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -70,7 +70,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' return { ...upload, percentage: 0, - error: new Error('Could not updated file name'), + error: new Error('Could not update file name'), }; }), ); @@ -160,21 +160,29 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' ); xhr.onload = () => { - if (xhr.readyState === xhr.DONE && xhr.status === 200) { - const result = JSON.parse(xhr.responseText); - this.set( - this.uploads.map((upload) => { - if (upload.id !== id) { - return upload; - } - - return { - ...upload, - id: result.file._id, - url: result.file.url, - }; - }), - ); + if (xhr.readyState === xhr.DONE) { + if (xhr.status === 400) { + const error = JSON.parse(xhr.responseText); + this.set(this.uploads.map((upload) => ({ ...upload, error: new Error(error.error) }))); + return; + } + + if (xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + this.set( + this.uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + id: result.file._id, + url: result.file.url, + }; + }), + ); + } } }; From a96e873d8a720b9d9fcd0edc63d8584b6f0bd0e5 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 4 Jul 2025 19:29:12 -0300 Subject: [PATCH 170/215] i18n: remove unused translation --- packages/i18n/src/locales/en.i18n.json | 1 - packages/i18n/src/locales/nb.i18n.json | 1 - packages/i18n/src/locales/sv.i18n.json | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 0259f7a5f6e59..286aaf30bd758 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5311,7 +5311,6 @@ "Upload_private_app": "Upload private app", "Upload_user_avatar": "Upload avatar", "Uploading_file": "Uploading file...", - "Uploading_file__fileName__": "Uploading file {{fileName}}", "Uploads": "Uploads", "Uptime": "Uptime", "Usage": "Usage", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index d68fea10bf1c2..181b76d1ebb4a 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -5289,7 +5289,6 @@ "Upload_private_app": "Last opp privat app", "Upload_user_avatar": "Last opp avatar", "Uploading_file": "Laster opp fil ...", - "Uploading_file__fileName__": "Laste opp fil {{fileName}}", "Uploads": "Opplastinger", "Uptime": "Oppetid", "Usage": "Bruk", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 700101dfb71e9..1917342cae5d2 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -5280,7 +5280,6 @@ "Upload_private_app": "Ladda upp en privat app", "Upload_user_avatar": "Ladda upp avatar", "Uploading_file": "Laddar upp fil...", - "Uploading_file__fileName__": "Ladda upp fil {{fileName}}", "Uploads": "Uppladdningar", "Uptime": "Upptid", "Usage": "Användning", From 7c8cfbbf6710ea3d0bcc67b982c0218cb68de317 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 7 Jul 2025 18:55:08 -0300 Subject: [PATCH 171/215] test: `image-upload` --- .../tests/e2e/fixtures/responses/mediaResponse.ts | 14 ++++++++++++++ apps/meteor/tests/e2e/image-gallery.spec.ts | 4 ---- apps/meteor/tests/e2e/image-upload.spec.ts | 13 +++++-------- .../e2e/page-objects/fragments/home-content.ts | 2 ++ apps/meteor/tests/e2e/page-objects/home-channel.ts | 4 ---- 5 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts diff --git a/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts b/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts new file mode 100644 index 0000000000000..044908810dd8f --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts @@ -0,0 +1,14 @@ +import type { Page } from '@playwright/test'; + +export default async function waitForMediaResponse(page: Page) { + let responsePromise; + try { + responsePromise = page.waitForResponse( + (response) => /api\/v1\/rooms.media/.test(response.url()) && response.request().method() === 'POST', + ); + } catch (error) { + console.error(error); + } + + return responsePromise; +} diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 23f9ea26cf5c6..b860a41c86109 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -51,10 +51,6 @@ test.describe.serial('Image Gallery', async () => { await poHomeChannel.sidenav.openChat(targetChannelLargeImage); await poHomeChannel.content.sendFileMessage(largeFileName); - await poHomeChannel.page.waitForResponse( - (response) => /api\/v1\/rooms.media/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST', - ); - await poHomeChannel.content.btnSendMainComposer.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(largeFileName); diff --git a/apps/meteor/tests/e2e/image-upload.spec.ts b/apps/meteor/tests/e2e/image-upload.spec.ts index 77443ad042d7c..85b667b43dcd5 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -33,10 +33,8 @@ test.describe('image-upload', () => { test('should show error indicator when upload fails', async () => { await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); - await poHomeChannel.content.inputFileUploadName.fill('bad-orientation.jpeg'); - await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.statusUploadIndicator).toContainText('Error:'); + await expect(poHomeChannel.content.getFileComposerByName('bad-orientation')).toHaveAttribute('readonly'); }); }); @@ -46,11 +44,10 @@ test.describe('image-upload', () => { }); test('should succeed upload of bad-orientation.jpeg', async () => { - await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); - await poHomeChannel.content.inputFileUploadName.fill('bad-orientation.jpeg'); - await poHomeChannel.content.btnModalConfirm.click(); - - await expect(poHomeChannel.content.getFileDescription).toHaveText('bad-orientation_description'); + const imgName = 'bad-orientation.jpeg'; + await poHomeChannel.content.sendFileMessage(imgName); + await poHomeChannel.content.btnSendMainComposer.click(); + await expect(poHomeChannel.content.lastUserMessage).toContainText(imgName); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index ef9bfb8962ec6..d4660694f4ad8 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -2,6 +2,7 @@ import fs from 'fs/promises'; import type { Locator, Page } from '@playwright/test'; +import waitForMediaResponse from '../../fixtures/responses/mediaResponse'; import { expect } from '../../utils/test'; export class HomeContent { @@ -406,6 +407,7 @@ export class HomeContent { async sendFileMessage(fileName: string): Promise { await this.page.locator('input[type=file]').setInputFiles(`./tests/e2e/fixtures/files/${fileName}`); + await waitForMediaResponse(this.page); } async openLastMessageMenu(): Promise { diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 17d84d7b9b760..466f85dcecea8 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -132,8 +132,4 @@ export class HomeChannel { get btnJoinRoom(): Locator { return this.page.getByRole('button', { name: 'Join' }); } - - get statusUploadIndicator(): Locator { - return this.page.getByRole('main').getByRole('status'); - } } From 822d1c10efc9d1f9d28ea5fcb505cad7d500f77b Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 7 Jul 2025 16:14:36 -0300 Subject: [PATCH 172/215] test: quote-attachment --- apps/meteor/tests/e2e/quote-attachment.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/meteor/tests/e2e/quote-attachment.spec.ts b/apps/meteor/tests/e2e/quote-attachment.spec.ts index f17b41e856097..f43f31e251bd5 100644 --- a/apps/meteor/tests/e2e/quote-attachment.spec.ts +++ b/apps/meteor/tests/e2e/quote-attachment.spec.ts @@ -30,8 +30,7 @@ test.describe.parallel('Quote Attachment', () => { const imageFileName = 'test-image.jpeg'; await test.step('Send message with attachment in the channel', async () => { await poHomeChannel.content.sendFileMessage(imageFileName); - await poHomeChannel.content.inputFileUploadName.fill(imageFileName); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.sendMessage(fileDescription); // Wait for the file to be uploaded and message to be sent await expect(poHomeChannel.content.lastUserMessage).toBeVisible(); @@ -57,7 +56,7 @@ test.describe.parallel('Quote Attachment', () => { }); test('should show file preview and description when quoting attachment file within a thread', async ({ page }) => { - const textFileName = 'any_file1.txt'; + const textFileName = 'any_file.txt'; await test.step('Send initial message in channel', async () => { await poHomeChannel.content.sendMessage('Initial message for thread test'); @@ -70,8 +69,7 @@ test.describe.parallel('Quote Attachment', () => { await expect(page).toHaveURL(/.*thread/); await poHomeChannel.content.dragAndDropTxtFileToThread(); - await poHomeChannel.content.inputFileUploadName.fill(textFileName); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.content.sendMessageInThread(fileDescription); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText(fileDescription); await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText(textFileName); From 573d5c6d357a1587e684a22969230702262220fd Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 7 Jul 2025 19:57:09 -0300 Subject: [PATCH 173/215] test: file-upload --- apps/meteor/tests/e2e/file-upload.spec.ts | 15 ++++++++++----- .../tests/e2e/fixtures/responses/mediaResponse.ts | 8 ++++---- .../e2e/page-objects/fragments/home-content.ts | 6 ++++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 8341970a5ef41..f07039ad76eba 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -45,10 +45,15 @@ test.describe.serial('file-upload', () => { test('should send file with name updated', async () => { const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); - await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); - await poHomeChannel.content.btnUpdateFileUpload.click(); - await poHomeChannel.content.btnSendMainComposer.click(); + + await test.step('update file name and send', async () => { + await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + + expect(poHomeChannel.content.getFileComposerByName(updatedFileName)); + await poHomeChannel.content.btnSendMainComposer.click(); + }); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); @@ -77,7 +82,7 @@ test.describe.serial('file-upload', () => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); await page.reload(); - await poHomeChannel.content.sendFileMessage(fileName); + await poHomeChannel.content.sendFileMessage(fileName, { waitForResponse: false }); await expect(poHomeChannel.content.getFileComposerByName(fileName)).toHaveAttribute('readonly'); }); diff --git a/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts b/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts index 044908810dd8f..908ba441d65b1 100644 --- a/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts +++ b/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts @@ -1,11 +1,11 @@ -import type { Page } from '@playwright/test'; +import type { Page, Response } from '@playwright/test'; + +const isMediaResponse = (response: Response) => /api\/v1\/rooms.media/.test(response.url()) && response.request().method() === 'POST'; export default async function waitForMediaResponse(page: Page) { let responsePromise; try { - responsePromise = page.waitForResponse( - (response) => /api\/v1\/rooms.media/.test(response.url()) && response.request().method() === 'POST', - ); + responsePromise = page.waitForResponse((response) => isMediaResponse(response)); } catch (error) { console.error(error); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index d4660694f4ad8..505e4b404d037 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -405,9 +405,11 @@ export class HomeContent { await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); } - async sendFileMessage(fileName: string): Promise { + async sendFileMessage(fileName: string, { waitForResponse = true } = {}): Promise { await this.page.locator('input[type=file]').setInputFiles(`./tests/e2e/fixtures/files/${fileName}`); - await waitForMediaResponse(this.page); + if (waitForResponse) { + await waitForMediaResponse(this.page); + } } async openLastMessageMenu(): Promise { From 6850713df833926beac68dab4ca92677710902a2 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 23 Dec 2025 13:49:13 -0300 Subject: [PATCH 174/215] fix: ts --- apps/meteor/client/lib/chats/flows/processMessageUploads.ts | 4 ++-- apps/meteor/client/lib/chats/uploads.ts | 4 ++-- packages/i18n/src/locales/de-IN.i18n.json | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 22a1d1fd3414e..502581f718d24 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -1,9 +1,9 @@ import type { AtLeast, FileAttachmentProps, IMessage, IUploadToConfirm } from '@rocket.chat/core-typings'; -import { e2e } from '../../../../app/e2e/client'; -import type { E2ERoom } from '../../../../app/e2e/client/rocketchat.e2e.room'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; +import { e2e } from '../../e2ee/rocketchat.e2e'; +import type { E2ERoom } from '../../e2ee/rocketchat.e2e.room'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { isEncryptedUpload, type EncryptedUpload } from '../Upload'; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 55e1e9f43efea..1402b99019068 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -6,10 +6,10 @@ import fileSize from 'filesize'; import { getErrorMessage } from '../errorHandling'; import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI'; import { isEncryptedUpload, type Upload } from './Upload'; -import { settings } from '../../../app/settings/client'; import { fileUploadIsValidContentType } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { i18n } from '../../../app/utils/lib/i18n'; +import { settings } from '../settings'; class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI { private rid: string; @@ -80,7 +80,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' clear = () => this.set([]); async send(file: File, encrypted?: EncryptedFileUploadContent): Promise { - const maxFileSize = settings.get('FileUpload_MaxFileSize'); + const maxFileSize = settings.peek('FileUpload_MaxFileSize'); const invalidContentType = !fileUploadIsValidContentType(file.type); const id = Random.id(); diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index a50e902b2451f..df571a160a0dc 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -2985,8 +2985,6 @@ "to_see_more_details_on_how_to_integrate": "um weitere Informationen zur Integration zu sehen", "unarchive-room": "Raum aus dem Archiv holen", "unarchive-room_description": "Berechtigung, einen Raum aus dem Archiv holen", - "User_added": "Der Benutzer {{user_added}} wurde hinzugefügt", - "User_left": "Benutzer {{user_left}} hat den Kanal verlassen", "user-generate-access-token": "Zugriffs-Token erzeugen", "user-generate-access-token_description": "Bereichtigung, Zugangs-Tokens für Benutzer zu erstellen", "user_sent_an_attachment": "{{user}} hat einen Anhang gesendet", From 2934c061095dc7464526f5c5ee64a44dc8d0b6dc Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 29 Dec 2025 16:41:53 -0300 Subject: [PATCH 175/215] fix: sendMessage filesToConfirm check --- .../app/lib/server/methods/sendMessage.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index c4d5ad3c0c75f..42ee1eccdf303 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser, IUploadToConfirm } from '@rocket.chat/core-typings'; +import { type AtLeast, type IMessage, type IUser, type IUploadToConfirm } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { MessageTypes } from '@rocket.chat/message-types'; @@ -146,10 +146,20 @@ Meteor.methods({ Match.ObjectIncluding({ _id: String, name: Match.Maybe(String), - content: Match.Maybe({ - algorithm: String, - ciphertext: String, - }), + content: Match.Maybe( + Match.OneOf( + { + algorithm: 'rc.v1.aes-sha2', + ciphertext: String, + }, + { + algorithm: 'rc.v2.aes-sha2', + ciphertext: String, + kid: String, + iv: String, + }, + ), + ), }), ]), ); From cc55b166fbc7f97eb35320e82c6d163a6d52ea14 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 29 Dec 2025 13:55:33 -0300 Subject: [PATCH 176/215] chore: revert `rocketchat.e2e.room` changes --- .../client/lib/chats/flows/uploadFiles.ts | 2 +- .../client/lib/e2ee/rocketchat.e2e.room.ts | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 074bf655f8af3..59e4906adc7e1 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -58,7 +58,7 @@ export const uploadFiles = async ( return; } - if (!(await e2eRoom.readyToEncrypt())) { + if (!e2eRoom.isReady()) { uploadFile(file); return; } diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts index b0fd28a9d5eee..f535057383900 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts @@ -8,7 +8,6 @@ import type { AtLeast, EncryptedMessageContent, EncryptedContent, - IUpload, } from '@rocket.chat/core-typings'; import { isEncryptedMessageContent } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; @@ -28,7 +27,6 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { t } from '../../../app/utils/lib/i18n'; import { RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; import { Messages, Rooms, Subscriptions } from '../../stores'; -import type { EncryptedFile } from '../chats/Upload'; import { roomCoordinator } from '../rooms/roomCoordinator'; const log = createLogger('E2E:Room'); @@ -166,7 +164,7 @@ export class E2ERoom extends Emitter { this.setState('KEYS_RECEIVED'); } - async readyToEncrypt() { + async shouldConvertSentMessages(message: { msg: string }) { if (!this.isReady() || this[PAUSED]) { return false; } @@ -177,17 +175,11 @@ export class E2ERoom extends Emitter { }); } - return true; - } - - async shouldConvertSentMessages(message: { msg: string }) { - if (!(await this.readyToEncrypt())) { - return false; - } - if (message.msg[0] === '/') { return false; } + + return true; } shouldConvertReceivedMessages() { @@ -580,7 +572,7 @@ export class E2ERoom extends Emitter { } // Encrypts files before upload. I/O is in arraybuffers. - async encryptFile(file: File): Promise { + async encryptFile(file: File) { const span = log.span('encryptFile'); const fileArrayBuffer = await file.arrayBuffer(); @@ -636,7 +628,7 @@ export class E2ERoom extends Emitter { // Helper function for encryption of content async encryptMessageContent( - contentToBeEncrypted: (Pick & Optional, 'msg'>) | Partial, + contentToBeEncrypted: Pick & Optional, 'msg'>, ) { const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); From 1f2cd49657fa91bd7efba3ea93ceed1fc5f0f101 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 5 Jan 2026 16:19:50 -0300 Subject: [PATCH 177/215] test: fix outdated updated e2e tests --- .../e2e/e2e-encryption/e2ee-file-encryption.spec.ts | 4 ++-- .../tests/e2e/page-objects/fragments/home-content.ts | 11 +++++------ apps/meteor/tests/e2e/prune-messages.spec.ts | 8 +++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index e6ed27606ad5f..bcb9aef5be58e 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -55,10 +55,10 @@ test.describe('E2EE File Encryption', () => { await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.content.sendMessage('any_description'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 04a75a3d82fa3..58cc54f9b3944 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -166,10 +166,6 @@ export class HomeContent { return this.page.getByRole('button', { name: 'Dismiss quoted message' }); } - get descriptionInput(): Locator { - return this.page.locator('//div[@id="modal-root"]//fieldset//div[2]//span//input'); - } - get getFileDescription(): Locator { return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="message-body"]'); } @@ -424,7 +420,7 @@ export class HomeContent { await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); } - async dragAndDropTxtFileToThread(): Promise { + async dragAndDropTxtFileToThread({ waitForResponse = true } = {}): Promise { const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8'); const dataTransfer = await this.page.evaluateHandle((contract) => { const data = new DataTransfer(); @@ -436,8 +432,11 @@ export class HomeContent { }, contract); await this.inputThreadMessage.dispatchEvent('dragenter', { dataTransfer }); - await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); + + if (waitForResponse) { + await waitForMediaResponse(this.page); + } } async sendFileMessage(fileName: string, { waitForResponse = true } = {}): Promise { diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index 33051a7cf06ac..d5fab0d472035 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -42,7 +42,7 @@ test.describe('prune-messages', () => { } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.btnModalConfirm.click(); + await content.btnSendMainComposer.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await sendTargetChannelMessage(api, targetChannel.fname as string, { @@ -109,8 +109,7 @@ test.describe('prune-messages', () => { } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.descriptionInput.fill('a message with a file'); - await content.btnModalConfirm.click(); + await content.btnSendMainComposer.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await test.step('prune files only', async () => { @@ -144,8 +143,7 @@ test.describe('prune-messages', () => { const { content } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.descriptionInput.fill('a message with a file'); - await content.btnModalConfirm.click(); + await content.btnSendMainComposer.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await content.lastUserMessage.hover(); From 6b6f901324d4a4d2615c695154f57e862ca75056 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 5 Jan 2026 18:59:34 -0300 Subject: [PATCH 178/215] fix: change upload sequence --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 59e4906adc7e1..45b39f39ec94a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -32,7 +32,7 @@ export const uploadFiles = async ( }; const uploadNextFile = async (): Promise => { - const file = queue.pop(); + const file = queue.shift(); if (!file) { chat.composer?.dismissAllQuotedMessages(); return; From 00415a5acc83900b1f67d8f200b8e986baa65dff Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Tue, 6 Jan 2026 10:14:35 -0300 Subject: [PATCH 179/215] fix: handle error of specific file in upload process --- apps/meteor/client/lib/chats/uploads.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 1402b99019068..09a59db404c93 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -163,7 +163,18 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' if (xhr.readyState === xhr.DONE) { if (xhr.status === 400) { const error = JSON.parse(xhr.responseText); - this.set(this.uploads.map((upload) => ({ ...upload, error: new Error(error.error) }))); + this.set( + this.uploads.map((upload) => { + if (upload.id !== id) { + return upload; + } + + return { + ...upload, + error: new Error(error.error), + }; + }), + ); return; } From 8aabc849068def1c74e10cdc398af367d8701522 Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 7 Jan 2026 15:43:12 -0300 Subject: [PATCH 180/215] feat: add modal confirmation for failed file uploads --- .../lib/chats/flows/processMessageUploads.ts | 130 +++++++++++++----- apps/meteor/client/lib/chats/uploads.ts | 6 +- packages/i18n/src/locales/en.i18n.json | 5 + 3 files changed, 100 insertions(+), 41 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 502581f718d24..780a0c8da7ebb 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -1,6 +1,8 @@ import type { AtLeast, FileAttachmentProps, IMessage, IUploadToConfirm } from '@rocket.chat/core-typings'; +import { imperativeModal, GenericModal } from '@rocket.chat/ui-client'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; +import { t } from '../../../../app/utils/lib/i18n'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { e2e } from '../../e2ee/rocketchat.e2e'; import type { E2ERoom } from '../../e2ee/rocketchat.e2e.room'; @@ -84,7 +86,7 @@ const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2 }); }; -export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => { +export const processMessageUploads = async (chat: ChatAPI, message: IMessage): Promise => { const { tmid, msg } = message; const room = await chat.data.getRoom(); const e2eRoom = await e2e.getInstanceByRoomId(room._id); @@ -96,49 +98,101 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage) => return false; } - const fileUrls: string[] = []; - const filesToConfirm: IUploadToConfirm[] = []; + const failedUploads = filesToUpload.filter((upload) => upload.error); - for await (const upload of filesToUpload) { - if (!upload.url || !upload.id) { - continue; - } + if (!failedUploads.length) { + return continueSendingMessage(); + } - let content; - if (e2eRoom && isEncryptedUpload(upload)) { - content = await e2eRoom.encryptMessageContent(upload.metadataForEncryption); + if (failedUploads.length > 0) { + const allUploadsFailed = failedUploads.length === filesToUpload.length; + + return new Promise((resolve) => { + imperativeModal.open({ + component: GenericModal, + props: { + variant: 'warning', + children: t('__count__files_failed_to_upload', { + count: failedUploads.length, + ...(failedUploads.length === 1 && { name: failedUploads[0].file.name }), + }), + ...(allUploadsFailed && { + title: t('Warning'), + confirmText: t('Ok'), + onConfirm: () => { + imperativeModal.close(); + resolve(true); + }, + }), + ...(!allUploadsFailed && { + title: t('Are_you_sure'), + confirmText: t('Send_anyway'), + cancelText: t('Cancel'), + onConfirm: () => { + imperativeModal.close(); + resolve(continueSendingMessage()); + }, + onCancel: () => { + imperativeModal.close(); + resolve(true); + }, + }), + onClose: () => { + imperativeModal.close(); + resolve(true); + }, + }, + }); + }); + } + + return continueSendingMessage(); + + async function continueSendingMessage() { + const fileUrls: string[] = []; + const filesToConfirm: IUploadToConfirm[] = []; + + for await (const upload of filesToUpload) { + if (!upload.url || !upload.id) { + continue; + } + + let content; + if (e2eRoom && isEncryptedUpload(upload)) { + content = await e2eRoom.encryptMessageContent(upload.metadataForEncryption); + } + + fileUrls.push(upload.url); + filesToConfirm.push({ _id: upload.id, name: upload.file.name, content }); } - fileUrls.push(upload.url); - filesToConfirm.push({ _id: upload.id, name: upload.file.name, content }); - } + const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg }); - const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg }); + let content; + if (e2eRoom && shouldConvertSentMessages) { + content = await getEncryptedContent(filesToUpload as EncryptedUpload[], e2eRoom, msg); + } - let content; - if (e2eRoom && shouldConvertSentMessages) { - content = await getEncryptedContent(filesToUpload as EncryptedUpload[], e2eRoom, msg); - } + const composedMessage: AtLeast = { + ...message, + tmid, + msg, + content, + ...(e2eRoom && { + t: 'e2e', + msg: '', + }), + } as const; + + try { + await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); + store.clear(); + } catch (error: unknown) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + chat.action.stop('uploading'); + } - const composedMessage: AtLeast = { - ...message, - tmid, - msg, - content, - ...(e2eRoom && { - t: 'e2e', - msg: '', - }), - } as const; - - try { - await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); - store.clear(); - } catch (error: unknown) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - chat.action.stop('uploading'); + return true; } - - return true; }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 09a59db404c93..531408017f7e0 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -98,16 +98,16 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' try { await new Promise((resolve, reject) => { if (file.size === 0) { - return reject(new Error(i18n.t('FileUpload_File_Empty'))); + reject(new Error(i18n.t('FileUpload_File_Empty'))); } // -1 maxFileSize means there is no limit if (maxFileSize > -1 && (file.size || 0) > maxFileSize) { - return reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }))); + reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }))); } if (invalidContentType) { - return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); + reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); } const xhr = sdk.rest.upload( diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index d1df85c49e924..3289ec4e86590 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4781,6 +4781,7 @@ "Selecting_users": "Selecting users", "Self_managed_hosting": "Self-managed hosting", "Send": "Send", + "Send_anyway": "Send anyway", "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", "Send_Test": "Send Test", "Send_Test_Email": "Send test email", @@ -7034,6 +7035,10 @@ "one": "{{count}} file pruned", "other": "{{count}} files pruned" }, + "__count__files_failed_to_upload": { + "one": "One file failed to upload and will not be sent: {{name}}", + "other": "{{count}} files failed to upload and will not be sent." + }, "__count__follower": { "one": "+{{count}} follower", "other": "+{{count}} followers" From 3bc60fcbe7e014dcbf40082c9e13fe976bbb2d1c Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 7 Jan 2026 15:43:22 -0300 Subject: [PATCH 181/215] test: add tests for handling multiple file uploads with failures --- apps/meteor/tests/e2e/file-upload.spec.ts | 70 +++++++++++++++++++ .../tests/e2e/fixtures/files/empty_file.txt | 0 2 files changed, 70 insertions(+) create mode 100644 apps/meteor/tests/e2e/fixtures/files/empty_file.txt diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 16ebad3a0bea5..d0c025101b2b7 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -86,6 +86,76 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.getFileComposerByName(fileName)).toHaveAttribute('readonly'); }); + + test.describe.serial('file upload fails', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); + }); + + test('should open warning modal when all file uploads fail', async () => { + const invalidFile1 = 'empty_file.txt'; + const invalidFile2 = 'diagram.drawio'; + + await poHomeChannel.content.sendFileMessage(invalidFile1, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(invalidFile2, { waitForResponse: false }); + + await expect(poHomeChannel.content.getFileComposerByName(invalidFile1)).toHaveAttribute('readonly'); + await expect(poHomeChannel.content.getFileComposerByName(invalidFile2)).toHaveAttribute('readonly'); + + await poHomeChannel.content.btnSendMainComposer.click(); + const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Warning' }); + await expect(warningModal).toBeVisible(); + await expect(warningModal).toContainText('2 files failed to upload'); + await expect(warningModal.getByRole('button', { name: 'Ok' })).toBeVisible(); + await expect(warningModal.getByRole('button', { name: 'Send anyway' })).not.toBeVisible(); + }); + + test('should handle multiple files with one failing upload', async () => { + const validFile = 'any_file.txt'; + const invalidFile = 'empty_file.txt'; + + await test.step('should only mark as "Upload failed" the specific file that failed to upload', async () => { + await poHomeChannel.content.sendFileMessage(validFile, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(invalidFile, { waitForResponse: false }); + + await expect(poHomeChannel.content.getFileComposerByName(validFile)).not.toHaveAttribute('readonly'); + await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).toHaveAttribute('readonly'); + }); + + await test.step('should open warning modal', async () => { + await poHomeChannel.content.btnSendMainComposer.click(); + + const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); + await expect(warningModal).toBeVisible(); + await expect(warningModal).toContainText('One file failed to upload'); + }); + + await test.step('should close modal when clicking "Cancel" button', async () => { + const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); + await warningModal.getByRole('button', { name: 'Cancel' }).click(); + + await expect(warningModal).not.toBeVisible(); + await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).toBeVisible(); + await expect(poHomeChannel.content.getFileComposerByName(validFile)).toBeVisible(); + }); + + await test.step('should send message with the valid file when confirming "Send anyway"', async () => { + await poHomeChannel.content.btnSendMainComposer.click(); + + const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); + + await warningModal.getByRole('button', { name: 'Send anyway' }).click(); + + await expect(warningModal).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(validFile); + await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).not.toBeVisible(); + }); + }); + }); }); test.describe('file-upload-not-member', () => { diff --git a/apps/meteor/tests/e2e/fixtures/files/empty_file.txt b/apps/meteor/tests/e2e/fixtures/files/empty_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d From c556588720b606ba05c019617af7e66e6896ddca Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 7 Jan 2026 15:53:31 -0300 Subject: [PATCH 182/215] chore: display error message in file upload component tooltip --- .../views/room/composer/messageBox/MessageComposerFileError.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx index c92c6c69d3f5c..2f76fe8c16c72 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileError.tsx @@ -18,6 +18,7 @@ const MessageComposerFileError = ({ fileTitle, error, actionIcon, ...props }: Me fileTitle={fileTitle} fileSubtitle={t('Upload_failed')} actionIcon={actionIcon} + title={error.message} {...props} /> ); From 91c58255784a70f7ecdbd567915185021c8d71d1 Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Fri, 9 Jan 2026 12:59:28 -0300 Subject: [PATCH 183/215] chore: add error feedback translations --- apps/meteor/client/lib/chats/uploads.ts | 4 ++-- packages/i18n/src/locales/en.i18n.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 531408017f7e0..8d75822ba1a97 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -70,7 +70,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' return { ...upload, percentage: 0, - error: new Error('Could not update file name'), + error: new Error(i18n.t('FileUpload_Update_Failed')), }; }), ); @@ -200,7 +200,7 @@ class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id' this.once(`cancelling-${id}`, () => { xhr.abort(); this.set(this.uploads.filter((upload) => upload.id !== id)); - reject(new Error('Upload cancelled')); + reject(new Error(i18n.t('FileUpload_Cancelled'))); }); }); } catch (error: unknown) { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3289ec4e86590..d44d5097a6053 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2266,6 +2266,8 @@ "FileUpload_Error": "File Upload Error", "FileUpload_FileSystemPath": "System Path", "FileUpload_File_Empty": "File empty", + "FileUpload_Cancelled": "Upload cancelled", + "FileUpload_Update_Failed": "Could not update file name", "FileUpload_GoogleStorage_AccessId": "Google Storage Access Id", "FileUpload_GoogleStorage_AccessId_Description": "The Access Id is generally in an email format, for example: \"`example-test@example.iam.gserviceaccount.com`\"", "FileUpload_GoogleStorage_Bucket": "Google Storage Bucket Name", From 5688b089a7a3d890f341bde81bd0418b7da895d9 Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Fri, 9 Jan 2026 16:30:30 -0300 Subject: [PATCH 184/215] test: add tests for multiple file uploads --- apps/meteor/tests/e2e/file-upload.spec.ts | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index d0c025101b2b7..b13a9b01bedc3 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -87,6 +87,67 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.getFileComposerByName(fileName)).toHaveAttribute('readonly'); }); + test.describe.serial('multiple file upload', () => { + test('should send multiple files successfully', async () => { + const file1 = 'any_file.txt'; + const file2 = 'lst-test.lst'; + + await poHomeChannel.content.sendFileMessage(file1); + await poHomeChannel.content.sendFileMessage(file2); + + await poHomeChannel.content.btnSendMainComposer.click(); + + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); + await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); + }); + + test('should be able to remove file from composer before sending', async () => { + const file1 = 'any_file.txt'; + const file2 = 'lst-test.lst'; + + await poHomeChannel.content.sendFileMessage(file1); + await poHomeChannel.content.sendFileMessage(file2); + + await poHomeChannel.content.getFileComposerByName(file1).getByRole('button', { name: 'Close' }).click(); + + await expect(poHomeChannel.content.getFileComposerByName(file1)).not.toBeVisible(); + await expect(poHomeChannel.content.getFileComposerByName(file2)).toBeVisible(); + + await poHomeChannel.content.btnSendMainComposer.click(); + + await expect(poHomeChannel.content.lastUserMessage).not.toContainText(file1); + await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); + }); + + test('should send multiple files with text message successfully', async () => { + const file1 = 'any_file.txt'; + const file2 = 'lst-test.lst'; + const message = 'Here are two files'; + + await poHomeChannel.content.sendFileMessage(file1); + await poHomeChannel.content.sendFileMessage(file2); + await poHomeChannel.content.inputMessage.fill(message); + + await poHomeChannel.content.btnSendMainComposer.click(); + + await expect(poHomeChannel.content.lastUserMessage).toContainText(message); + await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); + await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); + }); + + test('should respect the maximum number of files allowed per message: 10', async () => { + const file11 = 'number6.png'; + const files = new Array(10).fill('number1.png'); + + await Promise.all([...files, file11].map((file) => poHomeChannel.content.sendFileMessage(file))); + + // TODO: Composer regorg is needed for it to be accessible and allow locating files by their extensions and counting them + // await expect(poHomeChannel.content.getFilesInComposer('.png')).toHaveCount(10); + await expect(poHomeChannel.content.getFileComposerByName(file11)).not.toBeVisible(); + }); + }); + test.describe.serial('file upload fails', () => { test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); From 9a24a2ecd7ef072d34a7e78c05d46356ec296bfb Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Sun, 11 Jan 2026 23:11:13 -0300 Subject: [PATCH 185/215] feat: add configurable max files per message limit --- .../app/lib/server/methods/sendMessage.ts | 7 ++++ .../client/lib/chats/flows/uploadFiles.ts | 6 ++-- apps/meteor/client/lib/constants.ts | 1 - apps/meteor/server/settings/file-upload.ts | 6 ++++ apps/meteor/tests/end-to-end/api/methods.ts | 32 +++++++++++++++++++ packages/i18n/src/locales/en.i18n.json | 2 ++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 42ee1eccdf303..a04a91b06ed8f 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -164,6 +164,13 @@ Meteor.methods({ ]), ); + const maxFilesPerMessage = settings.get('FileUpload_MaxFilesPerMessage'); + if (filesToConfirm && maxFilesPerMessage && filesToConfirm.length > maxFilesPerMessage) { + throw new Meteor.Error('error-too-many-files', `Cannot send more than ${maxFilesPerMessage} files in one message`, { + method: 'sendMessage', + }); + } + const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 45b39f39ec94a..af8e6f9cb09e8 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,5 +1,4 @@ import { t } from '../../../../app/utils/lib/i18n'; -import { MAX_MULTIPLE_UPLOADED_FILES } from '../../constants'; import { e2e } from '../../e2ee'; import { settings } from '../../settings'; import { dispatchToastMessage } from '../../toast'; @@ -9,11 +8,12 @@ export const uploadFiles = async ( chat: ChatAPI, { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, ): Promise => { + const maxFilesPerMessage = settings.peek('FileUpload_MaxFilesPerMessage') as number; const mergedFilesLength = files.length + uploadsStore.get().length; - if (mergedFilesLength > MAX_MULTIPLE_UPLOADED_FILES) { + if (mergedFilesLength > maxFilesPerMessage) { return dispatchToastMessage({ type: 'error', - message: t('You_cant_upload_more_than__count__files', { count: MAX_MULTIPLE_UPLOADED_FILES }), + message: t('You_cant_upload_more_than__count__files', { count: maxFilesPerMessage }), }); } diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index 6845231f14afd..d144e03fec2ef 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -1,5 +1,4 @@ export const USER_STATUS_TEXT_MAX_LENGTH = 120; export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; -export const MAX_MULTIPLE_UPLOADED_FILES = 10; export const NAVIGATION_REGION_ID = 'navigation-region'; diff --git a/apps/meteor/server/settings/file-upload.ts b/apps/meteor/server/settings/file-upload.ts index 90032266651c8..ac99a2c5bb322 100644 --- a/apps/meteor/server/settings/file-upload.ts +++ b/apps/meteor/server/settings/file-upload.ts @@ -13,6 +13,12 @@ export const createFileUploadSettings = () => i18nDescription: 'FileUpload_MaxFileSizeDescription', }); + await this.add('FileUpload_MaxFilesPerMessage', 10, { + type: 'int', + public: true, + i18nDescription: 'FileUpload_MaxFilesPerMessage_Description', + }); + await this.add('FileUpload_MediaTypeWhiteList', '', { type: 'string', public: true, diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 088aef3aeff4d..5a25aa8a8a98b 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2088,6 +2088,38 @@ describe('Meteor.methods', () => { }) .end(done); }); + + it('should fail when sending more files than allowed by FileUpload_MaxFilesPerMessage', async () => { + await updateSetting('FileUpload_MaxFilesPerMessage', 2); + + const filesToConfirm = [ + { _id: 'file1', name: 'test1.txt' }, + { _id: 'file2', name: 'test2.txt' }, + { _id: 'file3', name: 'test3.txt' }, + ]; + + await request + .post(methodCall('sendMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [{ _id: Random.id(), rid, msg: 'test message with files' }, [], filesToConfirm], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error').that.is.an('object'); + expect(data.error).to.have.a.property('error', 'error-too-many-files'); + }); + + await updateSetting('FileUpload_MaxFilesPerMessage', 10); + }); }); describe('[@updateMessage]', () => { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index d44d5097a6053..d28e6f4e26889 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2284,6 +2284,8 @@ "FileUpload_GoogleStorage_Secret_Description": "Please follow [these instructions](https://github.com/CulturalMe/meteor-slingshot#google-cloud) and paste the result here.", "FileUpload_MaxFileSize": "Maximum File Upload Size (in bytes)", "FileUpload_MaxFileSizeDescription": "Set it to -1 to remove the file size limitation.", + "FileUpload_MaxFilesPerMessage": "Maximum Files Per Message", + "FileUpload_MaxFilesPerMessage_Description": "Maximum number of files that can be uploaded in a single message.", "FileUpload_MediaTypeBlackList": "Blocked Media Types", "FileUpload_MediaTypeBlackListDescription": "Comma-separated list of media types. This setting has priority over the Accepted Media Types.", "FileUpload_MediaTypeBlackList_Alert": "The default media type for unknown file extensions is \"application/octet-stream\", to work only with known file extensions you can add it to the \"Blocked Media Types\" list.", From 4e9d386b3cd28a1216728f5069337e64d0498124 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 12 Jan 2026 12:35:17 -0300 Subject: [PATCH 186/215] disable multi-file by default --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 5 ++++- apps/meteor/server/settings/file-upload.ts | 3 ++- packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index af8e6f9cb09e8..c922a56bd5c8a 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -13,7 +13,10 @@ export const uploadFiles = async ( if (mergedFilesLength > maxFilesPerMessage) { return dispatchToastMessage({ type: 'error', - message: t('You_cant_upload_more_than__count__files', { count: maxFilesPerMessage }), + message: + maxFilesPerMessage === 1 + ? t('You_cant_upload_more_than_one_file') + : t('You_cant_upload_more_than__count__files', { count: maxFilesPerMessage }), }); } diff --git a/apps/meteor/server/settings/file-upload.ts b/apps/meteor/server/settings/file-upload.ts index ac99a2c5bb322..bf175af7c48fb 100644 --- a/apps/meteor/server/settings/file-upload.ts +++ b/apps/meteor/server/settings/file-upload.ts @@ -13,10 +13,11 @@ export const createFileUploadSettings = () => i18nDescription: 'FileUpload_MaxFileSizeDescription', }); - await this.add('FileUpload_MaxFilesPerMessage', 10, { + await this.add('FileUpload_MaxFilesPerMessage', 1, { type: 'int', public: true, i18nDescription: 'FileUpload_MaxFilesPerMessage_Description', + alert: 'FileUpload_MaxFilesPerMessage_alert', }); await this.add('FileUpload_MediaTypeWhiteList', '', { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index d28e6f4e26889..265b8917d9fec 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2285,6 +2285,7 @@ "FileUpload_MaxFileSize": "Maximum File Upload Size (in bytes)", "FileUpload_MaxFileSizeDescription": "Set it to -1 to remove the file size limitation.", "FileUpload_MaxFilesPerMessage": "Maximum Files Per Message", + "FileUpload_MaxFilesPerMessage_alert": "Most Apps, Bridges and Integrations that read files are not compatible with more than one file per message.", "FileUpload_MaxFilesPerMessage_Description": "Maximum number of files that can be uploaded in a single message.", "FileUpload_MediaTypeBlackList": "Blocked Media Types", "FileUpload_MediaTypeBlackListDescription": "Comma-separated list of media types. This setting has priority over the Accepted Media Types.", @@ -5889,6 +5890,7 @@ "You_can_do_from_account_preferences": "You can do this later from your account preferences", "You_can_search_using_RegExp_eg": "You can search using Regular Expression. e.g. /^text$/i", "You_can_try_to": "You can try to", + "You_cant_upload_more_than_one_file": "You can't upload more than one file at once.", "You_cant_upload_more_than__count__files": "You can't upload more than {{count}} files at once.", "You_can_use_an_emoji_as_avatar": "You can also use an emoji as an avatar.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate Omnichannel with your CRM.", From 9077bfb21f7b76ca0c71678e87c1051f3fd62a14 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 12 Jan 2026 14:39:14 -0300 Subject: [PATCH 187/215] save `file` attribute with the first file on the list --- apps/meteor/app/lib/server/functions/sendMessage.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 391a19bbf7092..2f973cddbc2ec 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -264,6 +264,10 @@ export const sendMessage = async ( const { files, attachments } = await parseMultipleFilesIntoMessageAttachments(uploadsToConfirm, message.rid, user); message.files = files; message.attachments = attachments; + // For compatibility with older integrations, we save the first file to the `file` attribute of the message + if (files.length) { + message.file = files[0]; + } } await validateMessage(message, room, user); From 615c30193606d7dcdf45cb011e1965a2b6568d0d Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Mon, 12 Jan 2026 17:16:07 -0300 Subject: [PATCH 188/215] test: adjust composer locators --- .../e2ee-file-encryption.spec.ts | 4 ++-- apps/meteor/tests/e2e/file-upload.spec.ts | 20 +++++++++---------- apps/meteor/tests/e2e/image-gallery.spec.ts | 4 ++-- apps/meteor/tests/e2e/image-upload.spec.ts | 2 +- apps/meteor/tests/e2e/message-actions.spec.ts | 10 +++++----- apps/meteor/tests/e2e/prune-messages.spec.ts | 6 +++--- apps/meteor/tests/e2e/threads.spec.ts | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index b94ebd647ed26..64bc1e592ab58 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -92,7 +92,7 @@ test.describe('E2EE File Encryption', () => { await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); @@ -158,7 +158,7 @@ test.describe('E2EE File Encryption', () => { await poHomeChannel.content.dragAndDropTxtFile(); await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).not.toBeVisible(); - await expect(poHomeChannel.content.btnSendMainComposer).toBeDisabled(); + await expect(poHomeChannel.composer.btnSend).toBeDisabled(); }); }); }); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index b13a9b01bedc3..e1d76a983afdf 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -52,7 +52,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.btnUpdateFileUpload.click(); expect(poHomeChannel.content.getFileComposerByName(updatedFileName)); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); }); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); @@ -61,7 +61,7 @@ test.describe.serial('file-upload', () => { test('should send lst file successfully', async () => { await poHomeChannel.content.dragAndDropLstFile(); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst'); @@ -71,7 +71,7 @@ test.describe.serial('file-upload', () => { const fileName = 'diagram.drawio'; await page.reload(); await poHomeChannel.content.sendFileMessage(fileName); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastMessageFileName).toContainText(fileName); @@ -95,7 +95,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(file1); await poHomeChannel.content.sendFileMessage(file2); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); @@ -114,7 +114,7 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.getFileComposerByName(file1)).not.toBeVisible(); await expect(poHomeChannel.content.getFileComposerByName(file2)).toBeVisible(); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).not.toContainText(file1); await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); @@ -127,9 +127,9 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(file1); await poHomeChannel.content.sendFileMessage(file2); - await poHomeChannel.content.inputMessage.fill(message); + await poHomeChannel.composer.inputMessage.fill(message); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(message); await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); @@ -167,7 +167,7 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.getFileComposerByName(invalidFile1)).toHaveAttribute('readonly'); await expect(poHomeChannel.content.getFileComposerByName(invalidFile2)).toHaveAttribute('readonly'); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Warning' }); await expect(warningModal).toBeVisible(); await expect(warningModal).toContainText('2 files failed to upload'); @@ -188,7 +188,7 @@ test.describe.serial('file-upload', () => { }); await test.step('should open warning modal', async () => { - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); await expect(warningModal).toBeVisible(); @@ -205,7 +205,7 @@ test.describe.serial('file-upload', () => { }); await test.step('should send message with the valid file when confirming "Send anyway"', async () => { - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index def7688ef4774..3f888359336ae 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -43,13 +43,13 @@ test.describe.serial('Image Gallery', async () => { await poHomeChannel.navbar.openChat(targetChannel); for await (const imageName of imageNames) { await poHomeChannel.content.sendFileMessage(imageName); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(imageName); } await poHomeChannel.navbar.openChat(targetChannelLargeImage); await poHomeChannel.content.sendFileMessage(largeFileName); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(largeFileName); await poHomeChannel.content.lastUserMessage.locator('img.gallery-item').click(); diff --git a/apps/meteor/tests/e2e/image-upload.spec.ts b/apps/meteor/tests/e2e/image-upload.spec.ts index 8efe367f177b1..2fcd5a66298ad 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -46,7 +46,7 @@ test.describe('image-upload', () => { test('should succeed upload of bad-orientation.jpeg', async () => { const imgName = 'bad-orientation.jpeg'; await poHomeChannel.content.sendFileMessage(imgName); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(imgName); }); }); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index 98d925e19b2ab..16a5769f3a0bc 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -240,7 +240,7 @@ test.describe.serial('message-actions', () => { test('expect forward text file to channel', async () => { const filename = 'any_file.txt'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -252,7 +252,7 @@ test.describe.serial('message-actions', () => { test('expect forward image file to channel', async () => { const filename = 'test-image.jpeg'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -264,7 +264,7 @@ test.describe.serial('message-actions', () => { test('expect forward pdf file to channel', async () => { const filename = 'test_pdf_file.pdf'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -276,7 +276,7 @@ test.describe.serial('message-actions', () => { test('expect forward audio message to channel', async () => { const filename = 'sample-audio.mp3'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -288,7 +288,7 @@ test.describe.serial('message-actions', () => { test('expect forward video message to channel', async () => { const filename = 'test_video.mp4'; await poHomeChannel.content.sendFileMessage(filename); - await poHomeChannel.content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index d5fab0d472035..59082a321349b 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -42,7 +42,7 @@ test.describe('prune-messages', () => { } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await sendTargetChannelMessage(api, targetChannel.fname as string, { @@ -109,7 +109,7 @@ test.describe('prune-messages', () => { } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await test.step('prune files only', async () => { @@ -143,7 +143,7 @@ test.describe('prune-messages', () => { const { content } = poHomeChannel; await content.sendFileMessage('any_file.txt'); - await content.btnSendMainComposer.click(); + await poHomeChannel.composer.btnSend.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); await content.lastUserMessage.hover(); diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index 3b5c3ca90b37e..e5aa5c3d1d215 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -89,7 +89,7 @@ test.describe.serial('Threads', () => { await poHomeChannel.content.getThreadFileComposerByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); - await poHomeChannel.content.btnSendThreadComposer.click(); + await poHomeChannel.threadComposer.btnSend.click(); await expect(poHomeChannel.content.lastThreadMessageFileDescription).not.toBeVisible(); await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText(updatedFileName); From e450f8290436e910c5b26b1560df36a323124d60 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 12 Jan 2026 18:54:43 -0300 Subject: [PATCH 189/215] test: adjust max files per message limit --- apps/meteor/tests/end-to-end/api/methods.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 1b07e8f062e4a..faf56bf8252a3 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2111,9 +2111,9 @@ describe('Meteor.methods', () => { }), }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(400) .expect((res) => { - expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('success', false); const data = JSON.parse(res.body.message); expect(data).to.have.a.property('error').that.is.an('object'); expect(data.error).to.have.a.property('error', 'error-too-many-files'); From 2dc7cc3f334440f9007f2c2467267906c44298ea Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 12 Jan 2026 20:47:39 -0300 Subject: [PATCH 190/215] fix: delete all files when removing reported messages with multiple attachments --- .../lib/moderation/deleteReportedMessages.ts | 7 +- .../meteor/tests/end-to-end/api/moderation.ts | 78 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/apps/meteor/server/lib/moderation/deleteReportedMessages.ts b/apps/meteor/server/lib/moderation/deleteReportedMessages.ts index 204ae90d8c774..d322dd2571652 100644 --- a/apps/meteor/server/lib/moderation/deleteReportedMessages.ts +++ b/apps/meteor/server/lib/moderation/deleteReportedMessages.ts @@ -13,11 +13,10 @@ export async function deleteReportedMessages(messages: IMessage[], user: IUser): const files: string[] = []; const messageIds: string[] = []; for (const message of messages) { - if (message.file) { - files.push(message.file._id); - } if (message.files && message.files.length > 0) { - files.concat(message.files.map((file) => file._id)); + files.push(...message.files.map((file) => file._id)); + } else if (message.file) { + files.push(message.file._id); } messageIds.push(message._id); } diff --git a/apps/meteor/tests/end-to-end/api/moderation.ts b/apps/meteor/tests/end-to-end/api/moderation.ts index 162b4c65c4bc5..c6a94ff8db0a1 100644 --- a/apps/meteor/tests/end-to-end/api/moderation.ts +++ b/apps/meteor/tests/end-to-end/api/moderation.ts @@ -1,9 +1,11 @@ import type { IMessage, IModerationAudit, IModerationReport, IUser } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; +import { imgURL } from '../../data/interactions'; import { createUser, deleteUser } from '../../data/users.helper'; const makeModerationApiRequest = async ( @@ -767,6 +769,80 @@ describe('[Moderation]', () => { expect(res.body).to.have.property('error').and.to.be.a('string'); }); }); + + describe('with multiple files', () => { + let generalRoomId: string; + let messageWithFiles: IMessage; + let fileUrls: string[]; + + before(async () => { + const channelInfoResponse = await request.get(api('channels.info')).set(credentials).query({ roomName: 'general' }).expect(200); + generalRoomId = channelInfoResponse.body.channel._id; + + const file1Response = await request + .post(api(`rooms.media/${generalRoomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + + const file2Response = await request + .post(api(`rooms.media/${generalRoomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + + const uploadedFileIds = [file1Response.body.file._id, file2Response.body.file._id]; + + const filesToConfirm = uploadedFileIds.map((id) => ({ _id: id, name: 'test.png' })); + const sendMessageResponse = await request + .post(methodCall('sendMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [{ _id: Random.id(), rid: generalRoomId, msg: 'message with multiple files' }, [], filesToConfirm], + id: 'id', + msg: 'method', + }), + }) + .expect(200); + + const data = JSON.parse(sendMessageResponse.body.message); + messageWithFiles = data.result; + + fileUrls = + messageWithFiles.files?.map((f: { _id: string; name?: string }) => `/file-upload/${f._id}/${encodeURIComponent(f.name || '')}`) ?? + []; + + await request + .post(api('chat.reportMessage')) + .set(credentials) + .send({ + messageId: messageWithFiles._id, + description: 'test report for multiple files', + }) + .expect(200); + }); + + it('should delete reported messages and all associated files', async () => { + expect(fileUrls.length).to.be.greaterThan(1, 'Test requires multiple files'); + + await request + .post(api('moderation.user.deleteReportedMessages')) + .set(credentials) + .send({ + userId: messageWithFiles.u._id, + }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + for await (const fileUrl of fileUrls) { + await request.get(fileUrl).set(credentials).expect(404); + } + }); + }); }); describe('[/moderation.reportUser]', () => { From 97f4868ffec1098592790e585642e5f4670b9b8b Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Tue, 13 Jan 2026 11:25:11 -0300 Subject: [PATCH 191/215] test: update file composer methods to use new locator structure --- .../e2ee-encryption-decryption.spec.ts | 8 ++-- .../e2ee-file-encryption.spec.ts | 10 ++--- apps/meteor/tests/e2e/file-upload.spec.ts | 38 ++++++++++--------- apps/meteor/tests/e2e/image-upload.spec.ts | 2 +- .../meteor/tests/e2e/message-composer.spec.ts | 2 +- .../e2e/page-objects/fragments/composer.ts | 4 ++ .../page-objects/fragments/home-content.ts | 12 +----- apps/meteor/tests/e2e/threads.spec.ts | 2 +- 8 files changed, 38 insertions(+), 40 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts index 206352a9c96f5..e6e7b978daa33 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts @@ -99,10 +99,10 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { // Update file name and send await expect(async () => { - await encryptedRoomPage.getFileComposerByName('any_file.txt').click(); + await encryptedRoomPage.composer.getFileByName('any_file.txt').click(); await fileUploadModal.setName(fileName); await fileUploadModal.update(); - await expect(encryptedRoomPage.getFileComposerByName(fileName)).toBeVisible(); + await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible(); await encryptedRoomPage.sendMessage(fileDescription); }).toPass(); @@ -122,10 +122,10 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { // Update file name and send await expect(async () => { - await encryptedRoomPage.getFileComposerByName('any_file.txt').click(); + await encryptedRoomPage.composer.getFileByName('any_file.txt').click(); await fileUploadModal.setName(fileName); await fileUploadModal.update(); - await expect(encryptedRoomPage.getFileComposerByName(fileName)).toBeVisible(); + await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible(); await encryptedRoomPage.sendMessage(fileDescription); }).toPass(); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index 64bc1e592ab58..9216a278e0200 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -52,7 +52,7 @@ test.describe('E2EE File Encryption', () => { await test.step('send a file in channel', async () => { const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.composer.getFileByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); await poHomeChannel.content.sendMessage('any_description'); @@ -89,7 +89,7 @@ test.describe('E2EE File Encryption', () => { await test.step('send a text file in channel', async () => { const updatedFileName = 'any_file1.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.composer.getFileByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); await poHomeChannel.composer.btnSend.click(); @@ -105,7 +105,7 @@ test.describe('E2EE File Encryption', () => { await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName('any_file.txt')).toHaveAttribute('readonly'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); @@ -114,7 +114,7 @@ test.describe('E2EE File Encryption', () => { }); await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { - const composerFilesLocator = poHomeChannel.content.getFileComposerByName('any_file.txt'); + const composerFilesLocator = poHomeChannel.composer.getFileByName('any_file.txt'); const composerFiles = await composerFilesLocator.all(); await poHomeChannel.content.dragAndDropTxtFile(); @@ -157,7 +157,7 @@ test.describe('E2EE File Encryption', () => { await test.step('should not attach files to the composer', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName('any_file.txt')).not.toBeVisible(); await expect(poHomeChannel.composer.btnSend).toBeDisabled(); }); }); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index e1d76a983afdf..5627a8b34100c 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -12,6 +12,7 @@ test.describe.serial('file-upload', () => { test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); + await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 10); targetChannel = await createTargetChannel(api, { members: ['user1'] }); }); @@ -30,14 +31,14 @@ test.describe.serial('file-upload', () => { test('should successfully cancel upload', async () => { const fileName = 'any_file.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.getFileComposerByName(fileName).getByRole('button', { name: 'Close' }).click(); + await poHomeChannel.composer.getFileByName(fileName).getByRole('button', { name: 'Close' }).click(); - await expect(poHomeChannel.content.getFileComposerByName(fileName)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(fileName)).not.toBeVisible(); }); test('should not display modal when clicking in send file', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.composer.getFileByName('any_file.txt').click(); await poHomeChannel.content.btnCancelUpdateFileUpload.click(); await expect(poHomeChannel.content.fileUploadModal).not.toBeVisible(); }); @@ -47,11 +48,11 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.dragAndDropTxtFile(); await test.step('update file name and send', async () => { - await poHomeChannel.content.getFileComposerByName('any_file.txt').click(); + await poHomeChannel.composer.getFileByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); - expect(poHomeChannel.content.getFileComposerByName(updatedFileName)); + expect(poHomeChannel.composer.getFileByName(updatedFileName)); await poHomeChannel.composer.btnSend.click(); }); @@ -84,7 +85,7 @@ test.describe.serial('file-upload', () => { await page.reload(); await poHomeChannel.content.sendFileMessage(fileName, { waitForResponse: false }); - await expect(poHomeChannel.content.getFileComposerByName(fileName)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(fileName)).toHaveAttribute('readonly'); }); test.describe.serial('multiple file upload', () => { @@ -109,10 +110,10 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(file1); await poHomeChannel.content.sendFileMessage(file2); - await poHomeChannel.content.getFileComposerByName(file1).getByRole('button', { name: 'Close' }).click(); + await poHomeChannel.composer.getFileByName(file1).getByRole('button', { name: 'Close' }).click(); - await expect(poHomeChannel.content.getFileComposerByName(file1)).not.toBeVisible(); - await expect(poHomeChannel.content.getFileComposerByName(file2)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(file1)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(file2)).toBeVisible(); await poHomeChannel.composer.btnSend.click(); @@ -144,7 +145,7 @@ test.describe.serial('file-upload', () => { // TODO: Composer regorg is needed for it to be accessible and allow locating files by their extensions and counting them // await expect(poHomeChannel.content.getFilesInComposer('.png')).toHaveCount(10); - await expect(poHomeChannel.content.getFileComposerByName(file11)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(file11)).not.toBeVisible(); }); }); @@ -164,8 +165,8 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(invalidFile1, { waitForResponse: false }); await poHomeChannel.content.sendFileMessage(invalidFile2, { waitForResponse: false }); - await expect(poHomeChannel.content.getFileComposerByName(invalidFile1)).toHaveAttribute('readonly'); - await expect(poHomeChannel.content.getFileComposerByName(invalidFile2)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(invalidFile1)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(invalidFile2)).toHaveAttribute('readonly'); await poHomeChannel.composer.btnSend.click(); const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Warning' }); @@ -183,8 +184,8 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(validFile, { waitForResponse: false }); await poHomeChannel.content.sendFileMessage(invalidFile, { waitForResponse: false }); - await expect(poHomeChannel.content.getFileComposerByName(validFile)).not.toHaveAttribute('readonly'); - await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(validFile)).not.toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(invalidFile)).toHaveAttribute('readonly'); }); await test.step('should open warning modal', async () => { @@ -200,8 +201,8 @@ test.describe.serial('file-upload', () => { await warningModal.getByRole('button', { name: 'Cancel' }).click(); await expect(warningModal).not.toBeVisible(); - await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).toBeVisible(); - await expect(poHomeChannel.content.getFileComposerByName(validFile)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(invalidFile)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(validFile)).toBeVisible(); }); await test.step('should send message with the valid file when confirming "Send anyway"', async () => { @@ -212,8 +213,9 @@ test.describe.serial('file-upload', () => { await warningModal.getByRole('button', { name: 'Send anyway' }).click(); await expect(warningModal).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(validFile)).not.toBeVisible(); await expect(poHomeChannel.content.lastMessageFileName).toContainText(validFile); - await expect(poHomeChannel.content.getFileComposerByName(invalidFile)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(invalidFile)).not.toBeVisible(); }); }); }); @@ -241,6 +243,6 @@ test.describe('file-upload-not-member', () => { test('should not be able to upload if not a member', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.content.getFileComposerByName('any_file.txt')).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName('any_file.txt')).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/image-upload.spec.ts b/apps/meteor/tests/e2e/image-upload.spec.ts index 2fcd5a66298ad..bea13c2d3a4a9 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -34,7 +34,7 @@ test.describe('image-upload', () => { test('should show error indicator when upload fails', async () => { await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); - await expect(poHomeChannel.content.getFileComposerByName('bad-orientation')).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName('bad-orientation')).toHaveAttribute('readonly'); }); }); diff --git a/apps/meteor/tests/e2e/message-composer.spec.ts b/apps/meteor/tests/e2e/message-composer.spec.ts index 8c47f14419921..72f06ab6af9d0 100644 --- a/apps/meteor/tests/e2e/message-composer.spec.ts +++ b/apps/meteor/tests/e2e/message-composer.spec.ts @@ -140,7 +140,7 @@ test.describe.serial('message-composer', () => { await page.waitForTimeout(1000); await poHomeChannel.audioRecorder.getByRole('button', { name: 'Finish Recording', exact: true }).click(); - await expect(poHomeChannel.content.getFileComposerByName('Audio record.mp3')).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName('Audio record.mp3')).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts index 457ffd72fabdf..62f1862fafa63 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts @@ -29,6 +29,10 @@ export abstract class Composer { return this.toolbarPrimaryActions.getByRole('button', { name: 'Audio message' }); } + getFileByName(fileName: string): Locator { + return this.root.getByRole('button', { name: fileName }); + } + get btnSend(): Locator { return this.root.getByRole('button', { name: 'Send' }); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 4381cb794bebb..2e16ae1189dca 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -16,9 +16,9 @@ export function getFilePath(fileName: string): string { export class HomeContent { protected readonly page: Page; - protected readonly composer: RoomComposer; + readonly composer: RoomComposer; - protected readonly threadComposer: ThreadComposer; + readonly threadComposer: ThreadComposer; constructor(page: Page) { this.page = page; @@ -179,14 +179,6 @@ export class HomeContent { return this.fileUploadModal.getByRole('button', { name: 'Cancel' }); } - getFileComposerByName(fileName: string) { - return this.page.getByRole('main').getByRole('button', { name: fileName }); - } - - getThreadFileComposerByName(fileName: string) { - return this.page.getByRole('dialog').getByRole('button', { name: fileName }); - } - // ----------------------------------------- get lastMessageFileName(): Locator { diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index e5aa5c3d1d215..de86d3dbd5a26 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -86,7 +86,7 @@ test.describe.serial('Threads', () => { await expect(page).toHaveURL(/.*thread/); await poHomeChannel.content.dragAndDropTxtFileToThread(); - await poHomeChannel.content.getThreadFileComposerByName('any_file.txt').click(); + await poHomeChannel.threadComposer.getFileByName('any_file.txt').click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); await poHomeChannel.threadComposer.btnSend.click(); From 84dc8601f6731ce182a3b7eae92cea6d09959032 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 13 Jan 2026 15:34:33 -0300 Subject: [PATCH 192/215] test: reset `FileUpload_MaxFilesPerMessage` before and after tests --- .../tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts | 6 ++++-- apps/meteor/tests/e2e/file-upload.spec.ts | 5 +++-- apps/meteor/tests/e2e/page-objects/fragments/composer.ts | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index 9216a278e0200..f36d6c15106ab 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'; import { Users } from '../fixtures/userStates'; import { HomeChannel } from '../page-objects'; +import { setSettingValueById } from '../utils'; import { preserveSettings } from '../utils/preserveSettings'; import { test, expect } from '../utils/test'; @@ -21,6 +22,7 @@ test.describe('E2EE File Encryption', () => { test.use({ storageState: Users.userE2EE.state }); test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 10); await api.post('/settings/E2E_Enable', { value: true }); await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false }); @@ -31,6 +33,7 @@ test.describe('E2EE File Encryption', () => { test.afterAll(async ({ api }) => { await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' }); await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' }); + await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 1); }); test.beforeEach(async ({ page }) => { @@ -107,6 +110,7 @@ test.describe('E2EE File Encryption', () => { await poHomeChannel.content.dragAndDropTxtFile(); await expect(poHomeChannel.composer.getFileByName('any_file.txt')).toHaveAttribute('readonly'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + await poHomeChannel.composer.removeFileByName('any_file.txt'); }); await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => { @@ -116,10 +120,8 @@ test.describe('E2EE File Encryption', () => { await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { const composerFilesLocator = poHomeChannel.composer.getFileByName('any_file.txt'); const composerFiles = await composerFilesLocator.all(); - await poHomeChannel.content.dragAndDropTxtFile(); - await expect(composerFilesLocator).toHaveCount(2); await Promise.all(composerFiles.map((file) => expect(file).toHaveAttribute('readonly'))); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 5627a8b34100c..768be77e05a84 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -25,13 +25,14 @@ test.describe.serial('file-upload', () => { test.afterAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); + await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 1); expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); test('should successfully cancel upload', async () => { const fileName = 'any_file.txt'; await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.composer.getFileByName(fileName).getByRole('button', { name: 'Close' }).click(); + await poHomeChannel.composer.removeFileByName(fileName); await expect(poHomeChannel.composer.getFileByName(fileName)).not.toBeVisible(); }); @@ -110,7 +111,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.sendFileMessage(file1); await poHomeChannel.content.sendFileMessage(file2); - await poHomeChannel.composer.getFileByName(file1).getByRole('button', { name: 'Close' }).click(); + await poHomeChannel.composer.removeFileByName(file1); await expect(poHomeChannel.composer.getFileByName(file1)).not.toBeVisible(); await expect(poHomeChannel.composer.getFileByName(file2)).toBeVisible(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts index 62f1862fafa63..06e7c04af6c40 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts @@ -33,6 +33,10 @@ export abstract class Composer { return this.root.getByRole('button', { name: fileName }); } + async removeFileByName(fileName: string): Promise { + return this.getFileByName(fileName).getByRole('button', { name: 'Close' }).click(); + } + get btnSend(): Locator { return this.root.getByRole('button', { name: 'Send' }); } From 2dea937507433c7f126b3132ee743f6a890e21ee Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 14 Jan 2026 12:15:58 -0300 Subject: [PATCH 193/215] chore: improve `MessageComposerFile` a11y with `aria-busy` and update `dragAndDropLstFile` to support loading state --- .../views/room/composer/messageBox/MessageComposerFile.tsx | 1 + apps/meteor/tests/e2e/page-objects/fragments/home-content.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx index cc5963bd814ea..8a3b20d01fc3a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFile.tsx @@ -67,6 +67,7 @@ const MessageComposerFile = ({ upload, onRemove, onEdit, onCancel, ...props }: M fileTitle={upload.file.name} fileSubtitle={`${fileSize} - ${fileExtension}`} actionIcon={actionIcon} + aria-busy={isLoading} {...props} /> ); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 2e16ae1189dca..7baccb2ba5a00 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -354,7 +354,7 @@ export class HomeContent { await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); } - async dragAndDropLstFile(): Promise { + async dragAndDropLstFile({ waitForLoad = true }: { waitForLoad?: boolean } = {}): Promise { const contract = await fs.readFile(getFilePath('lst-test.lst'), 'utf-8'); const dataTransfer = await this.page.evaluateHandle((contract) => { const data = new DataTransfer(); @@ -368,6 +368,9 @@ export class HomeContent { await this.composer.inputMessage.dispatchEvent('dragenter', { dataTransfer }); await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); + if (waitForLoad) { + await waitForMediaResponse(this.page); + } } async dragAndDropTxtFileToThread({ waitForResponse = true } = {}): Promise { From b3bd39f62af1f568e5618e123eb06c59c6156c36 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 15 Jan 2026 17:50:47 -0300 Subject: [PATCH 194/215] fix: return public URLs for all files in message endpoint --- .../app/livechat/server/api/v1/message.ts | 4 +- .../functions/normalizeMessageFileUpload.ts | 55 +++++++++++------ .../end-to-end/api/livechat/20-messages.ts | 61 ++++++++++++++++++- .../core-typings/src/IMessage/IMessage.ts | 5 ++ 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index bfce5e98e23ea..06fb5334384cf 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -104,7 +104,7 @@ API.v1.addRoute( throw new Error('invalid-message'); } - if (message.file) { + if (message.file || message.files?.length) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } @@ -148,7 +148,7 @@ API.v1.addRoute( return API.v1.failure(); } - if (message?.file) { + if (message.file || message.files?.length) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } diff --git a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts index 173592a982766..9f3ed4b291a30 100644 --- a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts +++ b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts @@ -1,30 +1,49 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Uploads } from '@rocket.chat/models'; +import { isTruthy } from '../../../../lib/isTruthy'; import { FileUpload } from '../../../file-upload/server'; import { getURL } from '../getURL'; +const generateFileUploadData = async ( + message: Pick, + fileId: string, +): Promise<{ publicFilePath: string; type?: string; size?: number } | null> => { + const jwt = FileUpload.generateJWTToFileUrls({ + rid: message.rid, + userId: message.u._id, + fileId, + }); + const file = await Uploads.findOne({ _id: fileId }); + if (!file) { + return null; + } + return { + publicFilePath: file.name + ? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, { + cdn: false, + full: true, + }) + : '', + type: file.type, + size: file.size, + }; +}; + export const normalizeMessageFileUpload = async (message: Omit): Promise> => { + // handle deprecated single file property for backward compatibility if (message.file && !message.fileUpload) { - const jwt = FileUpload.generateJWTToFileUrls({ - rid: message.rid, - userId: message.u._id, - fileId: message.file._id, - }); - const file = await Uploads.findOne({ _id: message.file._id }); - if (!file) { - return message; + const fileUploadData = await generateFileUploadData(message, message.file._id); + if (fileUploadData) { + message.fileUpload = fileUploadData; } - message.fileUpload = { - publicFilePath: file.name - ? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, { - cdn: false, - full: true, - }) - : '', - type: file.type, - size: file.size, - }; } + + // handle multiple files + if (message.files?.length && !message.filesUpload) { + const filesUploadData = await Promise.all(message.files.map((file) => generateFileUploadData(message, file._id))); + message.filesUpload = filesUploadData.filter(isTruthy); + } + return message; }; diff --git a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts index a247280422ccb..b97e9472fef34 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts @@ -1,10 +1,12 @@ import { faker } from '@faker-js/faker'; import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { api, getCredentials, request } from '../../../data/api-data'; +import { api, credentials, getCredentials, methodCall, request } from '../../../data/api-data'; import { sendSimpleMessage } from '../../../data/chat.helper'; +import { imgURL } from '../../../data/interactions'; import { sendMessage, startANewLivechatRoomAndTakeIt, @@ -120,5 +122,62 @@ describe('LIVECHAT - messages', () => { expect(res.body.message._id).to.be.equal(message._id); }); }); + + it('should return filesUpload array when message has files property', async () => { + const { + room: { _id: roomId }, + visitor: { token }, + } = await startANewLivechatRoomAndTakeIt(); + + const file1Response = await request + .post(api(`rooms.media/${roomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + const file2Response = await request + .post(api(`rooms.media/${roomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + + const uploadedFileIds = [file1Response.body.file._id, file2Response.body.file._id]; + const filesToConfirm = uploadedFileIds.map((id) => ({ _id: id, name: 'test.png' })); + + // send message with multiple files as agent + const sendMessageResponse = await request + .post(methodCall('sendMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [{ _id: Random.id(), rid: roomId, msg: 'message with multiple files' }, [], filesToConfirm], + id: 'id', + msg: 'method', + }), + }) + .expect(200); + + const data = JSON.parse(sendMessageResponse.body.message); + const fileMessage = data.result; + + // fetch message as visitor and verify filesUpload + // note: image uploads also create thumbnails, so files.length may be > 2 + await request + .get(api(`livechat/message/${fileMessage._id}`)) + .query({ token, rid: roomId }) + .send() + .expect(200) + .expect((res) => { + const { message } = res.body; + expect(message._id).to.be.equal(fileMessage._id); + expect(message.file).to.be.an('object'); + expect(message.files).to.be.an('array').that.has.lengthOf(4); + expect(message.fileUpload).to.be.an('object'); + expect(message.fileUpload.publicFilePath).to.be.a('string').and.not.empty; + expect(message.fileUpload.type).to.be.a('string'); + expect(message.fileUpload.size).to.be.a('number'); + expect(message.filesUpload).to.be.an('array').with.lengthOf(message.files.length); + }); + }); }); }); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index f2c717f1505c1..27818be29906b 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -198,6 +198,11 @@ export interface IMessage extends IRocketChatRecord { size?: number; }; files?: FileProp[]; + filesUpload?: { + publicFilePath: string; + type?: string; + size?: number; + }[]; attachments?: MessageAttachment[]; reactions?: { From 654ec75708c2b6bb952310abc94b8edd3ba43d05 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 16 Jan 2026 10:19:05 -0300 Subject: [PATCH 195/215] test: set maxFilesPerMessage to 2 for omni multi-file scenario --- .../end-to-end/api/livechat/20-messages.ts | 126 ++++++++++-------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts index b97e9472fef34..5bf5426df4b9d 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom, SettingValue } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; @@ -17,7 +17,7 @@ import { closeOmnichannelRoom, } from '../../../data/livechat/rooms'; import { removeAgent } from '../../../data/livechat/users'; -import { updateEESetting, updateSetting } from '../../../data/permissions.helper'; +import { getSettingValueById, updateEESetting, updateSetting } from '../../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../../data/rooms.helper'; describe('LIVECHAT - messages', () => { @@ -123,61 +123,73 @@ describe('LIVECHAT - messages', () => { }); }); - it('should return filesUpload array when message has files property', async () => { - const { - room: { _id: roomId }, - visitor: { token }, - } = await startANewLivechatRoomAndTakeIt(); - - const file1Response = await request - .post(api(`rooms.media/${roomId}`)) - .set(credentials) - .attach('file', imgURL) - .expect(200); - const file2Response = await request - .post(api(`rooms.media/${roomId}`)) - .set(credentials) - .attach('file', imgURL) - .expect(200); - - const uploadedFileIds = [file1Response.body.file._id, file2Response.body.file._id]; - const filesToConfirm = uploadedFileIds.map((id) => ({ _id: id, name: 'test.png' })); - - // send message with multiple files as agent - const sendMessageResponse = await request - .post(methodCall('sendMessage')) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'sendMessage', - params: [{ _id: Random.id(), rid: roomId, msg: 'message with multiple files' }, [], filesToConfirm], - id: 'id', - msg: 'method', - }), - }) - .expect(200); - - const data = JSON.parse(sendMessageResponse.body.message); - const fileMessage = data.result; - - // fetch message as visitor and verify filesUpload - // note: image uploads also create thumbnails, so files.length may be > 2 - await request - .get(api(`livechat/message/${fileMessage._id}`)) - .query({ token, rid: roomId }) - .send() - .expect(200) - .expect((res) => { - const { message } = res.body; - expect(message._id).to.be.equal(fileMessage._id); - expect(message.file).to.be.an('object'); - expect(message.files).to.be.an('array').that.has.lengthOf(4); - expect(message.fileUpload).to.be.an('object'); - expect(message.fileUpload.publicFilePath).to.be.a('string').and.not.empty; - expect(message.fileUpload.type).to.be.a('string'); - expect(message.fileUpload.size).to.be.a('number'); - expect(message.filesUpload).to.be.an('array').with.lengthOf(message.files.length); - }); + describe('Multiple files per message', () => { + let originalMaxFilesPerMessageValue: SettingValue; + before(async () => { + originalMaxFilesPerMessageValue = await getSettingValueById('FileUpload_MaxFilesPerMessage'); + await updateSetting('FileUpload_MaxFilesPerMessage', 2); + }); + + after(async () => { + await updateSetting('FileUpload_MaxFilesPerMessage', originalMaxFilesPerMessageValue); + }); + + it('should return filesUpload array when message has files property', async () => { + const { + room: { _id: roomId }, + visitor: { token }, + } = await startANewLivechatRoomAndTakeIt(); + + const file1Response = await request + .post(api(`rooms.media/${roomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + const file2Response = await request + .post(api(`rooms.media/${roomId}`)) + .set(credentials) + .attach('file', imgURL) + .expect(200); + + const uploadedFileIds = [file1Response.body.file._id, file2Response.body.file._id]; + const filesToConfirm = uploadedFileIds.map((id) => ({ _id: id, name: 'test.png' })); + + // send message with multiple files as agent + const sendMessageResponse = await request + .post(methodCall('sendMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [{ _id: Random.id(), rid: roomId, msg: 'message with multiple files' }, [], filesToConfirm], + id: 'id', + msg: 'method', + }), + }) + .expect(200); + + const data = JSON.parse(sendMessageResponse.body.message); + const fileMessage = data.result; + + // fetch message as visitor and verify filesUpload + // note: image uploads also create thumbnails, so files.length may be > 2 + await request + .get(api(`livechat/message/${fileMessage._id}`)) + .query({ token, rid: roomId }) + .send() + .expect(200) + .expect((res) => { + const { message } = res.body; + expect(message._id).to.be.equal(fileMessage._id); + expect(message.file).to.be.an('object'); + expect(message.files).to.be.an('array').that.has.lengthOf(4); + expect(message.fileUpload).to.be.an('object'); + expect(message.fileUpload.publicFilePath).to.be.a('string').and.not.empty; + expect(message.fileUpload.type).to.be.a('string'); + expect(message.fileUpload.size).to.be.a('number'); + expect(message.filesUpload).to.be.an('array').with.lengthOf(message.files.length); + }); + }); }); }); }); From 7b1567adbe1e2410e054c006a843deff46b75b8b Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Mon, 19 Jan 2026 14:56:12 -0300 Subject: [PATCH 196/215] test: add test fot multifile upload in threads --- apps/meteor/tests/e2e/file-upload.spec.ts | 33 +++++++++++++++++-- .../tests/e2e/fixtures/files/another_file.txt | 1 + .../e2e/page-objects/fragments/composer.ts | 4 +++ .../page-objects/fragments/home-content.ts | 10 +++++- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/tests/e2e/fixtures/files/another_file.txt diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 768be77e05a84..513b0e419cc3d 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -46,7 +46,7 @@ test.describe.serial('file-upload', () => { test('should send file with name updated', async () => { const updatedFileName = 'any_file1.txt'; - await poHomeChannel.content.dragAndDropTxtFile(); + await poHomeChannel.content.sendFileMessage('any_file.txt'); await test.step('update file name and send', async () => { await poHomeChannel.composer.getFileByName('any_file.txt').click(); @@ -144,12 +144,39 @@ test.describe.serial('file-upload', () => { await Promise.all([...files, file11].map((file) => poHomeChannel.content.sendFileMessage(file))); - // TODO: Composer regorg is needed for it to be accessible and allow locating files by their extensions and counting them - // await expect(poHomeChannel.content.getFilesInComposer('.png')).toHaveCount(10); + await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10); await expect(poHomeChannel.composer.getFileByName(file11)).not.toBeVisible(); }); }); + test.describe.serial('thread multifile upload', () => { + test('should be able to remove file from thread composer before sending', async () => { + await poHomeChannel.content.sendMessage('this is a message for thread reply'); + await poHomeChannel.content.openReplyInThread(); + await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); + await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + + await poHomeChannel.threadComposer.removeFileByName('another_file.txt'); + + await expect(poHomeChannel.threadComposer.getFileByName('any_file.txt')).toBeVisible(); + await expect(poHomeChannel.threadComposer.getFileByName('another_file.txt')).not.toBeVisible(); + }); + + test('should send multiple files in a thread successfully', async () => { + const message = 'Here are two files in thread'; + await poHomeChannel.content.openReplyInThread(); + await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); + await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + + await poHomeChannel.threadComposer.inputMessage.fill(message); + await poHomeChannel.threadComposer.btnSend.click(); + + await expect(poHomeChannel.content.lastThreadMessageText).toContainText(message); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('another_file.txt')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('any_file.txt')).toBeVisible(); + }); + }); + test.describe.serial('file upload fails', () => { test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); diff --git a/apps/meteor/tests/e2e/fixtures/files/another_file.txt b/apps/meteor/tests/e2e/fixtures/files/another_file.txt new file mode 100644 index 0000000000000..ad7bbe091558e --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/another_file.txt @@ -0,0 +1 @@ +another_file diff --git a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts index 06e7c04af6c40..c63b8b220562e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts @@ -33,6 +33,10 @@ export abstract class Composer { return this.root.getByRole('button', { name: fileName }); } + getFilesInComposer(): Locator { + return this.root.getByRole('group', { name: 'Uploads' }).getByRole('button', { name: /^(?!Close$)/ }); + } + async removeFileByName(fileName: string): Promise { return this.getFileByName(fileName).getByRole('button', { name: 'Close' }).click(); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 7baccb2ba5a00..886ce81b7d4c2 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -394,7 +394,15 @@ export class HomeContent { } async sendFileMessage(fileName: string, { waitForResponse = true } = {}): Promise { - await this.page.locator('input[type=file]').setInputFiles(getFilePath(fileName)); + await this.page.locator('input[type=file]#room-upload').setInputFiles(getFilePath(fileName)); + if (waitForResponse) { + await waitForMediaResponse(this.page); + } + } + + async sendFileMessageToThread(fileName: string, { waitForResponse = true } = {}): Promise { + await this.threadComposer.inputMessage.click(); + await this.page.locator('input[type=file]#thread-upload').setInputFiles(getFilePath(fileName)); if (waitForResponse) { await waitForMediaResponse(this.page); } From aa8a988875ba580200b4106d3481420c3291c97e Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Tue, 20 Jan 2026 17:55:24 -0300 Subject: [PATCH 197/215] test: adjust input file locator --- .../messageBox/MessageComposerFileArea.tsx | 15 ++++++++++++++- apps/meteor/tests/e2e/file-upload.spec.ts | 3 ++- .../e2e/page-objects/fragments/home-content.ts | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx index 412458b0659fc..b93f296463611 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileArea.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; import MessageComposerFile from './MessageComposerFile'; import type { Upload } from '../../../../lib/chats/Upload'; @@ -11,8 +12,20 @@ type MessageComposerFileAreaProps = { }; const MessageComposerFileArea = ({ uploads, onRemove, onEdit, onCancel }: MessageComposerFileAreaProps) => { + const { t } = useTranslation(); return ( - + {uploads?.map((upload) => (
diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 513b0e419cc3d..424cde7079f7a 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -142,7 +142,8 @@ test.describe.serial('file-upload', () => { const file11 = 'number6.png'; const files = new Array(10).fill('number1.png'); - await Promise.all([...files, file11].map((file) => poHomeChannel.content.sendFileMessage(file))); + await Promise.all(files.map((file) => poHomeChannel.content.sendFileMessage(file))); + await poHomeChannel.content.sendFileMessage(file11); await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10); await expect(poHomeChannel.composer.getFileByName(file11)).not.toBeVisible(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 886ce81b7d4c2..7757d8fce9eb9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -394,7 +394,7 @@ export class HomeContent { } async sendFileMessage(fileName: string, { waitForResponse = true } = {}): Promise { - await this.page.locator('input[type=file]#room-upload').setInputFiles(getFilePath(fileName)); + await this.page.getByLabel('Room composer').locator('input[type=file]').setInputFiles(getFilePath(fileName)); if (waitForResponse) { await waitForMediaResponse(this.page); } @@ -402,7 +402,7 @@ export class HomeContent { async sendFileMessageToThread(fileName: string, { waitForResponse = true } = {}): Promise { await this.threadComposer.inputMessage.click(); - await this.page.locator('input[type=file]#thread-upload').setInputFiles(getFilePath(fileName)); + await this.page.getByLabel('Thread composer').locator('input[type=file]').setInputFiles(getFilePath(fileName)); if (waitForResponse) { await waitForMediaResponse(this.page); } From fefb4356fae35cb096ccdb526674779ed3b143de Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 21 Jan 2026 10:16:12 -0300 Subject: [PATCH 198/215] chore: update snapshots --- .../FileUploadModal.spec.tsx.snap | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/__snapshots__/FileUploadModal.spec.tsx.snap b/apps/meteor/client/views/room/modals/FileUploadModal/__snapshots__/FileUploadModal.spec.tsx.snap index 920eec6e41461..fe275926b5438 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/__snapshots__/FileUploadModal.spec.tsx.snap +++ b/apps/meteor/client/views/room/modals/FileUploadModal/__snapshots__/FileUploadModal.spec.tsx.snap @@ -91,29 +91,6 @@ exports[`renders Default without crashing 1`] = ` />
-
- - - - -
@@ -141,7 +118,7 @@ exports[`renders Default without crashing 1`] = ` - Send + Update
From d93c57e287b542d274bcc746969c433d7ec4940a Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Wed, 21 Jan 2026 11:07:30 -0300 Subject: [PATCH 199/215] test: adjust e2e and unit failing --- .../views/room/modals/FileUploadModal/FileUploadModal.spec.tsx | 2 +- apps/meteor/tests/e2e/file-upload.spec.ts | 2 +- packages/i18n/src/locales/zh.i18n.json | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx index f2c6175281959..c6478206286b7 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx @@ -47,7 +47,7 @@ it('should not send a renamed file with not allowed mime-type', async () => { const input = await screen.findByRole('textbox', { name: 'File name' }); await userEvent.type(input, 'testing.svg'); - const button = await screen.findByRole('button', { name: 'Send' }); + const button = await screen.findByRole('button', { name: 'Update' }); await userEvent.click(button); expect(screen.getByText('Media type not accepted: image/svg+xml')).toBeInTheDocument(); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 424cde7079f7a..a1fd376bae04d 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -143,7 +143,7 @@ test.describe.serial('file-upload', () => { const files = new Array(10).fill('number1.png'); await Promise.all(files.map((file) => poHomeChannel.content.sendFileMessage(file))); - await poHomeChannel.content.sendFileMessage(file11); + await poHomeChannel.content.dragAndDropTxtFile(); await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10); await expect(poHomeChannel.composer.getFileByName(file11)).not.toBeVisible(); diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 3244a2b493e55..defceaa975010 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1031,7 +1031,6 @@ "Cannot_invite_users_to_direct_rooms": "不能邀请用户加入私聊", "Cannot_open_conversation_with_yourself": "不能和你自己私聊", "Cannot_share_your_location": "不能分享您的位置…", - "Cannot_upload_file_character_limit": "无法上传文件,描述超过 {{count}} 个字符限制", "Cant_join": "无法加入", "Categories": "类别", "Categories*": "类别*", @@ -5515,13 +5514,11 @@ "Upload_anyway": "仍然上传", "Upload_app": "上传应用", "Upload_file": "上传文件", - "Upload_file_description": "文件描述", "Upload_file_name": "文件名", "Upload_file_question": "上传文件?", "Upload_private_app": "上传私有应用", "Upload_user_avatar": "上传头像", "Uploading_file": "文件上传中……", - "Uploading_file__fileName__": "正在上传文件 {{fileName}}", "Uploads": "上传", "Uptime": "运行时间", "Usage": "使用情况", From 5f7f95e167084c8608783a03c4d8b032846dd416 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 21 Jan 2026 18:52:39 -0300 Subject: [PATCH 200/215] disable multi-file for omnichannel rooms --- apps/meteor/app/lib/server/methods/sendMessage.ts | 7 +++++++ .../utils/server/functions/normalizeMessageFileUpload.ts | 8 -------- packages/i18n/src/locales/en.i18n.json | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index ae6ac410a790f..76c60f784ebdd 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,6 @@ import { api } from '@rocket.chat/core-services'; import { type AtLeast, type IMessage, type IUser, type IUploadToConfirm } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { MessageTypes } from '@rocket.chat/message-types'; @@ -105,6 +106,12 @@ export async function executeSendMessage( } } + if ((extraInfo?.filesToConfirm?.length || 0) > 1 && isOmnichannelRoom(room)) { + throw new Meteor.Error('error-too-many-files', `Cannot send more than one file per message in Omnichannel rooms`, { + method: 'sendMessage', + }); + } + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, extraInfo?.previewUrls, extraInfo?.filesToConfirm); } catch (err: any) { diff --git a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts index 74cec9766d90f..a7a453a67c5ee 100644 --- a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts +++ b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts @@ -1,6 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Uploads } from '@rocket.chat/models'; -import { isTruthy } from '@rocket.chat/tools'; import { FileUpload } from '../../../file-upload/server'; import { getURL } from '../getURL'; @@ -31,7 +30,6 @@ const generateFileUploadData = async ( }; export const normalizeMessageFileUpload = async (message: Omit): Promise> => { - // handle deprecated single file property for backward compatibility if (message.file && !message.fileUpload) { const fileUploadData = await generateFileUploadData(message, message.file._id); if (fileUploadData) { @@ -39,11 +37,5 @@ export const normalizeMessageFileUpload = async (message: Omit generateFileUploadData(message, file._id))); - message.filesUpload = filesUploadData.filter(isTruthy); - } - return message; }; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index b96f1d2b02005..838d7683a159c 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6316,6 +6316,7 @@ "error-token-already-exists": "A token with this name already exists", "error-token-does-not-exists": "Token does not exists", "error-too-many-requests": "Error, too many requests. Please slow down. You must wait {{seconds}} seconds before trying again.", + "error-too-many-files": "Error: number of files attached to the message is over the limit.", "error-transcript-already-requested": "Transcript already requested", "error-unable-to-update-priority": "Unable to update priority", "error-unknown-contact": "Contact is unknown.", From d2183232086250e8b76c3a9d6d05b807f209c61d Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 22 Jan 2026 10:44:14 -0300 Subject: [PATCH 201/215] validate omnichannel limit on client side too --- apps/meteor/client/lib/chats/flows/uploadFiles.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index c922a56bd5c8a..fb275a08d56b5 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,3 +1,5 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; + import { t } from '../../../../app/utils/lib/i18n'; import { e2e } from '../../e2ee'; import { settings } from '../../settings'; @@ -22,6 +24,13 @@ export const uploadFiles = async ( const room = await chat.data.getRoom(); + if (mergedFilesLength > 1 && isOmnichannelRoom(room)) { + return dispatchToastMessage({ + type: 'error', + message: t('You_cant_upload_more_than_one_file'), + }); + } + const queue = [...files]; const uploadFile = (file: File, encrypted?: EncryptedFileUploadContent) => { From bc1f3324bc82d70591612dd9d68995194b6077d5 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 22 Jan 2026 12:17:13 -0300 Subject: [PATCH 202/215] undoing some unnecessary changes --- .../app/livechat/server/api/v1/message.ts | 4 +- .../functions/normalizeMessageFileUpload.ts | 47 +++++++------------ 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index d87017eef85ff..7c05245c2b648 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -104,7 +104,7 @@ API.v1.addRoute( throw new Error('invalid-message'); } - if (message.file || message.files?.length) { + if (message.file) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } @@ -148,7 +148,7 @@ API.v1.addRoute( return API.v1.failure(); } - if (message.file || message.files?.length) { + if (message.file) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } diff --git a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts index a7a453a67c5ee..173592a982766 100644 --- a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts +++ b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts @@ -4,38 +4,27 @@ import { Uploads } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload/server'; import { getURL } from '../getURL'; -const generateFileUploadData = async ( - message: Pick, - fileId: string, -): Promise<{ publicFilePath: string; type?: string; size?: number } | null> => { - const jwt = FileUpload.generateJWTToFileUrls({ - rid: message.rid, - userId: message.u._id, - fileId, - }); - const file = await Uploads.findOne({ _id: fileId }); - if (!file) { - return null; - } - return { - publicFilePath: file.name - ? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, { - cdn: false, - full: true, - }) - : '', - type: file.type, - size: file.size, - }; -}; - export const normalizeMessageFileUpload = async (message: Omit): Promise> => { if (message.file && !message.fileUpload) { - const fileUploadData = await generateFileUploadData(message, message.file._id); - if (fileUploadData) { - message.fileUpload = fileUploadData; + const jwt = FileUpload.generateJWTToFileUrls({ + rid: message.rid, + userId: message.u._id, + fileId: message.file._id, + }); + const file = await Uploads.findOne({ _id: message.file._id }); + if (!file) { + return message; } + message.fileUpload = { + publicFilePath: file.name + ? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, { + cdn: false, + full: true, + }) + : '', + type: file.type, + size: file.size, + }; } - return message; }; From 5d1151e04af32fc48100c3ae2f77cfa1abc97123 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 22 Jan 2026 14:38:56 -0300 Subject: [PATCH 203/215] serialize processing of files --- .../server/methods/sendFileMessage.ts | 250 ++++++++++-------- 1 file changed, 135 insertions(+), 115 deletions(-) diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 388cf7de99134..0a0f188004b0b 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -11,23 +11,27 @@ import type { import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Logger } from '@rocket.chat/logger'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; -import { wrapExceptions } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { omit } from '../../../../lib/utils/omit'; import { callbacks } from '../../../../server/lib/callbacks'; -import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; -function validateFileRequiredFields(file: Partial): asserts file is AtLeast { +type MinimalUploadData = AtLeast; + +function validateFilesRequiredFields(files: Partial[]): asserts files is MinimalUploadData[] { const requiredFields = ['_id', 'name', 'type', 'size']; - for (const field of requiredFields) { - if (!Object.keys(file).includes(field)) { - throw new Meteor.Error('error-invalid-file', 'Invalid file'); + for (const file of files) { + const fields = Object.keys(file); + + for (const field of requiredFields) { + if (!fields.includes(field)) { + throw new Meteor.Error('error-invalid-file', 'Invalid file'); + } } } } @@ -38,124 +42,134 @@ export const parseMultipleFilesIntoMessageAttachments = async ( filesToConfirm: Partial[], roomId: string, user: IUser, -): Promise<{ files: FileProp[]; attachments: MessageAttachment[] }> => { - const results = await Promise.all( - filesToConfirm.map((file) => - wrapExceptions(() => parseFileIntoMessageAttachments(file, roomId, user)).catch(async (error) => { - // Not an important error, it should not happen and if it happens wil affect the attachment preview in the message object only - logger.warn({ msg: 'Error processing file: ', file, error }); - return { files: [], attachments: [] }; - }), - ), - ); - - return { - files: results.flatMap(({ files }) => files), - attachments: results.flatMap(({ attachments }) => attachments), - }; -}; - -export const parseFileIntoMessageAttachments = async ( - file: Partial, - roomId: string, - user: IUser, ): Promise => { - validateFileRequiredFields(file); - - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + // Validate every file before we process any of them + validateFilesRequiredFields(filesToConfirm); const attachments: MessageAttachment[] = []; + const files: FileProp[] = []; + + // Process one file at a time, to avoid loading too many images into memory at the same time, or sending too many simultaneous requests to external services + for await (const file of filesToConfirm) { + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - const files: FileProp[] = [ - { + files.push({ _id: file._id, name: file.name || '', type: file.type || 'file', size: file.size || 0, format: file.identify?.format || '', typeGroup: file.typeGroup, - }, - ]; - - if (/^image\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file?.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type as string, - image_size: file.size, - }; + }); - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; + const { attachment, thumbnail } = await createFileAttachment(file, { fileUrl, roomId, uid: user._id }); + if (thumbnail) { + files.push(thumbnail); } + attachments.push(attachment); + } - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; - const thumbnail = await FileUpload.uploadImageThumbnail( - { - thumbFileName, - thumbFileType, - originalFileId, - }, - thumbBuffer, - roomId, - user._id, - ); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: thumbnail.name || '', - type: thumbnail.type || 'file', - size: thumbnail.size || 0, - format: thumbnail.identify?.format || '', - typeGroup: thumbnail.typeGroup || '', - }); + return { files, attachments }; +}; + +async function createFileAttachment( + file: MinimalUploadData, + extraData: { fileUrl: string; roomId: string; uid: string }, +): Promise<{ attachment: FileAttachmentProps; thumbnail?: FileProp }> { + const { fileUrl, roomId, uid } = extraData; + + if (file.type) { + if (/^image\/.+/.test(file.type)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; + } + + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult; + const thumbnail = await FileUpload.uploadImageThumbnail( + { + thumbFileName, + thumbFileType, + originalFileId, + }, + thumbBuffer, + roomId, + uid, + ); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + return { + attachment, + thumbnail: { + _id: thumbnail._id, + name: thumbnail.name || '', + type: thumbnail.type || 'file', + size: thumbnail.size || 0, + format: thumbnail.identify?.format || '', + typeGroup: thumbnail.typeGroup || '', + }, + }; + } + } catch (err) { + logger.error({ err }); } - } catch (e) { - SystemLogger.error(e); + + return { attachment }; } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type as string, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type as string)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type as string, - video_size: file.size as number, - }; - attachments.push(attachment); - } else { - const attachment = { + + if (/^audio\/.+/.test(file.type)) { + return { + attachment: { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }, + }; + } + + if (/^video\/.+/.test(file.type)) { + return { + attachment: { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type, + video_size: file.size as number, + }, + }; + } + } + + return { + attachment: { title: file.name, type: 'file', format: getFileExtension(file.name), @@ -163,10 +177,16 @@ export const parseFileIntoMessageAttachments = async ( title_link: fileUrl, title_link_download: true, size: file.size as number, - }; - attachments.push(attachment); - } - return { files, attachments }; + }, + }; +} + +export const parseFileIntoMessageAttachments = async ( + file: Partial, + roomId: string, + user: IUser, +): Promise => { + return parseMultipleFilesIntoMessageAttachments([file], roomId, user); }; declare module '@rocket.chat/ddp-client' { @@ -200,7 +220,7 @@ export const sendFileMessage = async ( if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage', - } as any); + }); } const room = await Rooms.findOneById(roomId); @@ -261,7 +281,7 @@ Meteor.methods({ if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage', - } as any); + }); } return sendFileMessage(userId, { roomId, file, msgData }); From 026bdc004eff2f681099b299bea28d2e3a562d7b Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 22 Jan 2026 16:30:51 -0300 Subject: [PATCH 204/215] reducing changes to shrink the PR --- apps/meteor/app/lib/server/functions/sendMessage.ts | 3 ++- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 +- apps/meteor/app/livechat/server/api/v1/message.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index c36f5cd2c3dba..68bcae9638e92 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -1,6 +1,7 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; -import { isE2EEMessage, type IMessage, type IRoom, type IUpload, type IUploadToConfirm } from '@rocket.chat/core-typings'; +import { isE2EEMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUpload, IUploadToConfirm } from '@rocket.chat/core-typings'; import { Messages, Uploads } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 76c60f784ebdd..49e3702b38dce 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import { type AtLeast, type IMessage, type IUser, type IUploadToConfirm } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IUser, IUploadToConfirm } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 7c05245c2b648..3e90c1df1efe5 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -148,7 +148,7 @@ API.v1.addRoute( return API.v1.failure(); } - if (message.file) { + if (message?.file) { message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } From d608af4faaa1d42a626eecbad283c383222b3eaa Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 28 Jan 2026 13:18:02 -0300 Subject: [PATCH 205/215] use new endpoint to delete files --- .../room/contextualBar/RoomFiles/hooks/useDeleteFile.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useDeleteFile.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useDeleteFile.tsx index 8f556969bc28d..f660a88f3d6e5 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useDeleteFile.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useDeleteFile.tsx @@ -1,19 +1,19 @@ import type { IUpload } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericModal } from '@rocket.chat/ui-client'; -import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; export const useDeleteFile = (reload: () => void) => { const { t } = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); - const deleteFile = useMethod('deleteFileMessage'); + const deleteFile = useEndpoint('POST', '/v1/uploads.delete'); const handleDelete = useEffectEvent((_id: IUpload['_id']) => { const onConfirm = async () => { try { - await deleteFile(_id); + await deleteFile({ fileId: _id }); dispatchToastMessage({ type: 'success', message: t('Deleted') }); reload(); } catch (error) { From ec00572f7b661a57c5dc80d54ea0cda10ff2ed38 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 28 Jan 2026 14:42:50 -0300 Subject: [PATCH 206/215] chore: bring back old limitation --- .../app/lib/server/methods/sendMessage.ts | 13 +++---------- .../client/lib/chats/flows/uploadFiles.ts | 19 +++---------------- apps/meteor/lib/constants.ts | 1 + 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 49e3702b38dce..8c81d31cdcb63 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -1,6 +1,5 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser, IUploadToConfirm } from '@rocket.chat/core-typings'; -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { MessageTypes } from '@rocket.chat/message-types'; @@ -10,6 +9,7 @@ import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; +import { MAX_MULTIPLE_UPLOADED_FILES } from '../../../../lib/constants'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; @@ -106,12 +106,6 @@ export async function executeSendMessage( } } - if ((extraInfo?.filesToConfirm?.length || 0) > 1 && isOmnichannelRoom(room)) { - throw new Meteor.Error('error-too-many-files', `Cannot send more than one file per message in Omnichannel rooms`, { - method: 'sendMessage', - }); - } - metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, extraInfo?.previewUrls, extraInfo?.filesToConfirm); } catch (err: any) { @@ -182,9 +176,8 @@ Meteor.methods({ ]), ); - const maxFilesPerMessage = settings.get('FileUpload_MaxFilesPerMessage'); - if (filesToConfirm && maxFilesPerMessage && filesToConfirm.length > maxFilesPerMessage) { - throw new Meteor.Error('error-too-many-files', `Cannot send more than ${maxFilesPerMessage} files in one message`, { + if (filesToConfirm && filesToConfirm.length > MAX_MULTIPLE_UPLOADED_FILES) { + throw new Meteor.Error('error-too-many-files', `Cannot send more than ${MAX_MULTIPLE_UPLOADED_FILES} files in one message`, { method: 'sendMessage', }); } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fb275a08d56b5..3433cf1fcdbc4 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,6 +1,5 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; - import { t } from '../../../../app/utils/lib/i18n'; +import { MAX_MULTIPLE_UPLOADED_FILES } from '../../../../lib/constants'; import { e2e } from '../../e2ee'; import { settings } from '../../settings'; import { dispatchToastMessage } from '../../toast'; @@ -10,27 +9,15 @@ export const uploadFiles = async ( chat: ChatAPI, { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, ): Promise => { - const maxFilesPerMessage = settings.peek('FileUpload_MaxFilesPerMessage') as number; const mergedFilesLength = files.length + uploadsStore.get().length; - if (mergedFilesLength > maxFilesPerMessage) { + if (mergedFilesLength > MAX_MULTIPLE_UPLOADED_FILES) { return dispatchToastMessage({ type: 'error', - message: - maxFilesPerMessage === 1 - ? t('You_cant_upload_more_than_one_file') - : t('You_cant_upload_more_than__count__files', { count: maxFilesPerMessage }), + message: t('You_cant_upload_more_than__count__files', { count: MAX_MULTIPLE_UPLOADED_FILES }), }); } const room = await chat.data.getRoom(); - - if (mergedFilesLength > 1 && isOmnichannelRoom(room)) { - return dispatchToastMessage({ - type: 'error', - message: t('You_cant_upload_more_than_one_file'), - }); - } - const queue = [...files]; const uploadFile = (file: File, encrypted?: EncryptedFileUploadContent) => { diff --git a/apps/meteor/lib/constants.ts b/apps/meteor/lib/constants.ts index 61b66421ce446..4c4b572ca210f 100644 --- a/apps/meteor/lib/constants.ts +++ b/apps/meteor/lib/constants.ts @@ -1 +1,2 @@ export const NOTIFICATION_ATTACHMENT_COLOR = '#FD745E'; +export const MAX_MULTIPLE_UPLOADED_FILES = 10; From 4c7e72baa2fdff40262cd2747abc444a6d0543f1 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 28 Jan 2026 14:43:16 -0300 Subject: [PATCH 207/215] chore: replace number max files with boolean enable multi files --- apps/meteor/server/settings/file-upload.ts | 8 ++++---- apps/meteor/tests/end-to-end/api/methods.ts | 12 ++---------- packages/i18n/src/locales/en.i18n.json | 7 +++---- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/apps/meteor/server/settings/file-upload.ts b/apps/meteor/server/settings/file-upload.ts index bf175af7c48fb..7a397cab15095 100644 --- a/apps/meteor/server/settings/file-upload.ts +++ b/apps/meteor/server/settings/file-upload.ts @@ -13,11 +13,11 @@ export const createFileUploadSettings = () => i18nDescription: 'FileUpload_MaxFileSizeDescription', }); - await this.add('FileUpload_MaxFilesPerMessage', 1, { - type: 'int', + await this.add('FileUpload_EnableMultipleFilesPerMessage', false, { + type: 'boolean', public: true, - i18nDescription: 'FileUpload_MaxFilesPerMessage_Description', - alert: 'FileUpload_MaxFilesPerMessage_alert', + i18nDescription: 'FileUpload_EnableMultipleFilesPerMessage_Description', + alert: 'FileUpload_EnableMultipleFilesPerMessage_alert', }); await this.add('FileUpload_MediaTypeWhiteList', '', { diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index faf56bf8252a3..df32fb453d178 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2090,14 +2090,8 @@ describe('Meteor.methods', () => { .end(done); }); - it('should fail when sending more files than allowed by FileUpload_MaxFilesPerMessage', async () => { - await updateSetting('FileUpload_MaxFilesPerMessage', 2); - - const filesToConfirm = [ - { _id: 'file1', name: 'test1.txt' }, - { _id: 'file2', name: 'test2.txt' }, - { _id: 'file3', name: 'test3.txt' }, - ]; + it('should fail when sending more than 10 files', async () => { + const filesToConfirm = Array.from({ length: 11 }, (_, i) => ({ _id: `file${i + 1}`, name: `test${i + 1}.txt` })); await request .post(methodCall('sendMessage')) @@ -2118,8 +2112,6 @@ describe('Meteor.methods', () => { expect(data).to.have.a.property('error').that.is.an('object'); expect(data.error).to.have.a.property('error', 'error-too-many-files'); }); - - await updateSetting('FileUpload_MaxFilesPerMessage', 10); }); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9215e7f153437..47af61deac35e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2287,9 +2287,9 @@ "FileUpload_GoogleStorage_Secret_Description": "Please follow [these instructions](https://github.com/CulturalMe/meteor-slingshot#google-cloud) and paste the result here.", "FileUpload_MaxFileSize": "Maximum File Upload Size (in bytes)", "FileUpload_MaxFileSizeDescription": "Set it to -1 to remove the file size limitation.", - "FileUpload_MaxFilesPerMessage": "Maximum Files Per Message", - "FileUpload_MaxFilesPerMessage_alert": "Most Apps, Bridges and Integrations that read files are not compatible with more than one file per message.", - "FileUpload_MaxFilesPerMessage_Description": "Maximum number of files that can be uploaded in a single message.", + "FileUpload_EnableMultipleFilesPerMessage": "Enable Multiple Files Per Message", + "FileUpload_EnableMultipleFilesPerMessage_alert": "Most Apps, Bridges and Integrations that read files are not compatible with more than one file per message.", + "FileUpload_EnableMultipleFilesPerMessage_Description": "Enable the ability to upload multiple files in a single message.", "FileUpload_MediaTypeBlackList": "Blocked Media Types", "FileUpload_MediaTypeBlackListDescription": "Comma-separated list of media types. This setting has priority over the Accepted Media Types.", "FileUpload_MediaTypeBlackList_Alert": "The default media type for unknown file extensions is \"application/octet-stream\", to work only with known file extensions you can add it to the \"Blocked Media Types\" list.", @@ -5899,7 +5899,6 @@ "You_can_do_from_account_preferences": "You can do this later from your account preferences", "You_can_search_using_RegExp_eg": "You can search using Regular Expression. e.g. /^text$/i", "You_can_try_to": "You can try to", - "You_cant_upload_more_than_one_file": "You can't upload more than one file at once.", "You_cant_upload_more_than__count__files": "You can't upload more than {{count}} files at once.", "You_can_use_an_emoji_as_avatar": "You can also use an emoji as an avatar.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate Omnichannel with your CRM.", From c93448cc2c9ef66ec559f1e2f87ddeaeaf65d514 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 28 Jan 2026 17:48:46 -0300 Subject: [PATCH 208/215] feat: send multiple files based on setting --- .../lib/chats/flows/processMessageUploads.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts index 780a0c8da7ebb..4700f5ffc4135 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -1,4 +1,6 @@ import type { AtLeast, FileAttachmentProps, IMessage, IUploadToConfirm } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { imperativeModal, GenericModal } from '@rocket.chat/ui-client'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; @@ -6,6 +8,7 @@ import { t } from '../../../../app/utils/lib/i18n'; import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { e2e } from '../../e2ee/rocketchat.e2e'; import type { E2ERoom } from '../../e2ee/rocketchat.e2e.room'; +import { settings } from '../../settings'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { isEncryptedUpload, type EncryptedUpload } from '../Upload'; @@ -93,6 +96,7 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage): P const store = tmid ? chat.threadUploads : chat.uploads; const filesToUpload = store.get(); + const multiFilePerMessageEnabled = settings.peek('FileUpload_EnableMultipleFilesPerMessage') as boolean; if (filesToUpload.length === 0) { return false; @@ -185,7 +189,20 @@ export const processMessageUploads = async (chat: ChatAPI, message: IMessage): P } as const; try { - await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); + if ((!multiFilePerMessageEnabled || isOmnichannelRoom(room)) && filesToConfirm.length > 1) { + await Promise.all( + filesToConfirm.map((fileToConfirm, index) => { + /** + * The first message will keep the composedMessage, + * subsequent messages will have a new ID with empty text + * */ + const messageToSend = index === 0 ? composedMessage : { ...composedMessage, _id: Random.id(), msg: '' }; + return sdk.call('sendMessage', messageToSend, [fileUrls[index]], [fileToConfirm]); + }), + ); + } else { + await sdk.call('sendMessage', composedMessage, fileUrls, filesToConfirm); + } store.clear(); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); From b42cde1e9441d149b2e79c725a5275aebf0865b5 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 30 Jan 2026 17:04:37 -0300 Subject: [PATCH 209/215] chore: undesired types in `IMessage` --- packages/core-typings/src/IMessage/IMessage.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 1fdc30df19522..9f61d6fad1b56 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -196,11 +196,6 @@ export interface IMessage extends IRocketChatRecord { size?: number; }; files?: FileProp[]; - filesUpload?: { - publicFilePath: string; - type?: string; - size?: number; - }[]; attachments?: MessageAttachment[]; reactions?: { From 815439cbaa09d02021fb4800549e71d162bd2d63 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 5 Feb 2026 10:26:55 -0300 Subject: [PATCH 210/215] test: remove outdated api test --- .../end-to-end/api/livechat/20-messages.ts | 77 +------------------ 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts index 5bf5426df4b9d..a247280422ccb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts @@ -1,12 +1,10 @@ import { faker } from '@faker-js/faker'; -import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom, SettingValue } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; +import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { api, credentials, getCredentials, methodCall, request } from '../../../data/api-data'; +import { api, getCredentials, request } from '../../../data/api-data'; import { sendSimpleMessage } from '../../../data/chat.helper'; -import { imgURL } from '../../../data/interactions'; import { sendMessage, startANewLivechatRoomAndTakeIt, @@ -17,7 +15,7 @@ import { closeOmnichannelRoom, } from '../../../data/livechat/rooms'; import { removeAgent } from '../../../data/livechat/users'; -import { getSettingValueById, updateEESetting, updateSetting } from '../../../data/permissions.helper'; +import { updateEESetting, updateSetting } from '../../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../../data/rooms.helper'; describe('LIVECHAT - messages', () => { @@ -122,74 +120,5 @@ describe('LIVECHAT - messages', () => { expect(res.body.message._id).to.be.equal(message._id); }); }); - - describe('Multiple files per message', () => { - let originalMaxFilesPerMessageValue: SettingValue; - before(async () => { - originalMaxFilesPerMessageValue = await getSettingValueById('FileUpload_MaxFilesPerMessage'); - await updateSetting('FileUpload_MaxFilesPerMessage', 2); - }); - - after(async () => { - await updateSetting('FileUpload_MaxFilesPerMessage', originalMaxFilesPerMessageValue); - }); - - it('should return filesUpload array when message has files property', async () => { - const { - room: { _id: roomId }, - visitor: { token }, - } = await startANewLivechatRoomAndTakeIt(); - - const file1Response = await request - .post(api(`rooms.media/${roomId}`)) - .set(credentials) - .attach('file', imgURL) - .expect(200); - const file2Response = await request - .post(api(`rooms.media/${roomId}`)) - .set(credentials) - .attach('file', imgURL) - .expect(200); - - const uploadedFileIds = [file1Response.body.file._id, file2Response.body.file._id]; - const filesToConfirm = uploadedFileIds.map((id) => ({ _id: id, name: 'test.png' })); - - // send message with multiple files as agent - const sendMessageResponse = await request - .post(methodCall('sendMessage')) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'sendMessage', - params: [{ _id: Random.id(), rid: roomId, msg: 'message with multiple files' }, [], filesToConfirm], - id: 'id', - msg: 'method', - }), - }) - .expect(200); - - const data = JSON.parse(sendMessageResponse.body.message); - const fileMessage = data.result; - - // fetch message as visitor and verify filesUpload - // note: image uploads also create thumbnails, so files.length may be > 2 - await request - .get(api(`livechat/message/${fileMessage._id}`)) - .query({ token, rid: roomId }) - .send() - .expect(200) - .expect((res) => { - const { message } = res.body; - expect(message._id).to.be.equal(fileMessage._id); - expect(message.file).to.be.an('object'); - expect(message.files).to.be.an('array').that.has.lengthOf(4); - expect(message.fileUpload).to.be.an('object'); - expect(message.fileUpload.publicFilePath).to.be.a('string').and.not.empty; - expect(message.fileUpload.type).to.be.a('string'); - expect(message.fileUpload.size).to.be.a('number'); - expect(message.filesUpload).to.be.an('array').with.lengthOf(message.files.length); - }); - }); - }); }); }); From 068a511c53a22fa6d2bbe7cfb39e6985e3c7e177 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 5 Feb 2026 10:27:16 -0300 Subject: [PATCH 211/215] test: remove outdated setting from e2e tests --- .../e2ee-file-encryption.spec.ts | 3 -- apps/meteor/tests/e2e/file-upload.spec.ts | 52 +++++++++++-------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index f36d6c15106ab..aea68aa0188c6 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -2,7 +2,6 @@ import { faker } from '@faker-js/faker'; import { Users } from '../fixtures/userStates'; import { HomeChannel } from '../page-objects'; -import { setSettingValueById } from '../utils'; import { preserveSettings } from '../utils/preserveSettings'; import { test, expect } from '../utils/test'; @@ -22,7 +21,6 @@ test.describe('E2EE File Encryption', () => { test.use({ storageState: Users.userE2EE.state }); test.beforeAll(async ({ api }) => { - await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 10); await api.post('/settings/E2E_Enable', { value: true }); await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false }); @@ -33,7 +31,6 @@ test.describe('E2EE File Encryption', () => { test.afterAll(async ({ api }) => { await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' }); await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' }); - await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 1); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index a1fd376bae04d..e18b2a70d46f9 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -12,7 +12,6 @@ test.describe.serial('file-upload', () => { test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); - await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 10); targetChannel = await createTargetChannel(api, { members: ['user1'] }); }); @@ -25,7 +24,6 @@ test.describe.serial('file-upload', () => { test.afterAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); - await setSettingValueById(api, 'FileUpload_MaxFilesPerMessage', 1); expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); @@ -90,6 +88,14 @@ test.describe.serial('file-upload', () => { }); test.describe.serial('multiple file upload', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_EnableMultipleFilesPerMessage', true); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_EnableMultipleFilesPerMessage', false); + }); + test('should send multiple files successfully', async () => { const file1 = 'any_file.txt'; const file2 = 'lst-test.lst'; @@ -148,33 +154,33 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10); await expect(poHomeChannel.composer.getFileByName(file11)).not.toBeVisible(); }); - }); - test.describe.serial('thread multifile upload', () => { - test('should be able to remove file from thread composer before sending', async () => { - await poHomeChannel.content.sendMessage('this is a message for thread reply'); - await poHomeChannel.content.openReplyInThread(); - await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); - await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + test.describe.serial('thread multiple file upload', () => { + test('should be able to remove file from thread composer before sending', async () => { + await poHomeChannel.content.sendMessage('this is a message for thread reply'); + await poHomeChannel.content.openReplyInThread(); + await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); + await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); - await poHomeChannel.threadComposer.removeFileByName('another_file.txt'); + await poHomeChannel.threadComposer.removeFileByName('another_file.txt'); - await expect(poHomeChannel.threadComposer.getFileByName('any_file.txt')).toBeVisible(); - await expect(poHomeChannel.threadComposer.getFileByName('another_file.txt')).not.toBeVisible(); - }); + await expect(poHomeChannel.threadComposer.getFileByName('any_file.txt')).toBeVisible(); + await expect(poHomeChannel.threadComposer.getFileByName('another_file.txt')).not.toBeVisible(); + }); - test('should send multiple files in a thread successfully', async () => { - const message = 'Here are two files in thread'; - await poHomeChannel.content.openReplyInThread(); - await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); - await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + test('should send multiple files in a thread successfully', async () => { + const message = 'Here are two files in thread'; + await poHomeChannel.content.openReplyInThread(); + await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); + await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); - await poHomeChannel.threadComposer.inputMessage.fill(message); - await poHomeChannel.threadComposer.btnSend.click(); + await poHomeChannel.threadComposer.inputMessage.fill(message); + await poHomeChannel.threadComposer.btnSend.click(); - await expect(poHomeChannel.content.lastThreadMessageText).toContainText(message); - await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('another_file.txt')).toBeVisible(); - await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('any_file.txt')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText).toContainText(message); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('another_file.txt')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('any_file.txt')).toBeVisible(); + }); }); }); From 189652bb4f463c71d8f2f09a84ae0472d4eacf8e Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 10 Feb 2026 13:13:14 -0300 Subject: [PATCH 212/215] test: revamp and prepare file-upload failing tests for the fix --- .../e2ee-file-encryption.spec.ts | 33 +-- apps/meteor/tests/e2e/file-upload.spec.ts | 196 +++++++++--------- .../page-objects/fragments/home-content.ts | 6 +- .../fragments/modals/file-upload-modal.ts | 34 ++- 4 files changed, 148 insertions(+), 121 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index aea68aa0188c6..8d29cb4286172 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -15,6 +15,8 @@ const settingsList = [ const originalSettings = preserveSettings(settingsList); +const TEST_FILE_TXT = 'any_file.txt'; + test.describe('E2EE File Encryption', () => { let poHomeChannel: HomeChannel; @@ -38,25 +40,25 @@ test.describe('E2EE File Encryption', () => { await page.goto('/home'); }); - test('File and description encryption and editing the description', async ({ page }) => { + test.fixme('should edit encrypted message with file', async ({ page }) => { + const updatedFileName = `edited_${TEST_FILE_TXT}`; + await test.step('create an encrypted channel', async () => { const channelName = faker.string.uuid(); - await poHomeChannel.navbar.createEncryptedChannel(channelName); await expect(page).toHaveURL(`/group/${channelName}`); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); }); - await test.step('send a file in channel', async () => { - const updatedFileName = 'any_file1.txt'; - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.composer.getFileByName('any_file.txt').click(); + await test.step('send a file in channel and edit it', async () => { + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); - await poHomeChannel.content.sendMessage('any_description'); + await expect(poHomeChannel.composer.getFileByName(updatedFileName)).toBeVisible(); + await poHomeChannel.content.sendMessage('any_description'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); @@ -65,13 +67,12 @@ test.describe('E2EE File Encryption', () => { await test.step('edit the description', async () => { await poHomeChannel.content.openLastMessageMenu(); await poHomeChannel.content.btnOptionEditMessage.click(); - expect(await poHomeChannel.composer.inputMessage.inputValue()).toBe('any_description'); await poHomeChannel.composer.inputMessage.fill('edited any_description'); await page.keyboard.press('Enter'); - await expect(poHomeChannel.content.getFileDescription).toHaveText('edited any_description'); + await expect(poHomeChannel.content.lastUserMessage.getByRole('link').getByText(updatedFileName)).toBeVisible(); }); }); @@ -87,9 +88,9 @@ test.describe('E2EE File Encryption', () => { }); await test.step('send a text file in channel', async () => { - const updatedFileName = 'any_file1.txt'; - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.composer.getFileByName('any_file.txt').click(); + const updatedFileName = `edited_${TEST_FILE_TXT}`; + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); await poHomeChannel.composer.btnSend.click(); @@ -105,9 +106,9 @@ test.describe('E2EE File Encryption', () => { await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.composer.getFileByName('any_file.txt')).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toHaveAttribute('readonly'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await poHomeChannel.composer.removeFileByName('any_file.txt'); + await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT); }); await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => { @@ -115,7 +116,7 @@ test.describe('E2EE File Encryption', () => { }); await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { - const composerFilesLocator = poHomeChannel.composer.getFileByName('any_file.txt'); + const composerFilesLocator = poHomeChannel.composer.getFileByName(TEST_FILE_TXT); const composerFiles = await composerFilesLocator.all(); await poHomeChannel.content.dragAndDropTxtFile(); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index e18b2a70d46f9..deb1f9aa243e6 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -1,11 +1,17 @@ import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; +import { FileUploadWarningModal } from './page-objects/fragments/modals'; import { createTargetChannel } from './utils'; import { setSettingValueById } from './utils/setSettingValueById'; import { expect, test } from './utils/test'; test.use({ storageState: Users.user1.state }); +const TEST_FILE_TXT = 'any_file.txt'; +const TEST_FILE_LST = 'lst-test.lst'; +const TEST_FILE_DRAWIO = 'diagram.drawio'; +const TEST_EMPTY_FILE = 'empty_file.txt'; + test.describe.serial('file-upload', () => { let poHomeChannel: HomeChannel; let targetChannel: string; @@ -27,27 +33,19 @@ test.describe.serial('file-upload', () => { expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); - test('should successfully cancel upload', async () => { - const fileName = 'any_file.txt'; + test('should cancel uploaded file attached to message composer', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.composer.removeFileByName(fileName); - - await expect(poHomeChannel.composer.getFileByName(fileName)).not.toBeVisible(); - }); + await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT); - test('should not display modal when clicking in send file', async () => { - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.composer.getFileByName('any_file.txt').click(); - await poHomeChannel.content.btnCancelUpdateFileUpload.click(); - await expect(poHomeChannel.content.fileUploadModal).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); }); test('should send file with name updated', async () => { - const updatedFileName = 'any_file1.txt'; - await poHomeChannel.content.sendFileMessage('any_file.txt'); + const updatedFileName = `edited_${TEST_FILE_TXT}`; + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); await test.step('update file name and send', async () => { - await poHomeChannel.composer.getFileByName('any_file.txt').click(); + await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click(); await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); await poHomeChannel.content.btnUpdateFileUpload.click(); @@ -59,32 +57,49 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.lastMessageFileName).toContainText(updatedFileName); }); + test('should attach multiple files and send one per message', async () => { + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessage(TEST_FILE_LST); + expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)); + expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)); + + await poHomeChannel.composer.btnSend.click(); + await expect(poHomeChannel.content.lastUserMessageDownloadLink).toHaveCount(1); + }); + + test.fixme('should not be able to attach files when editing a message', async () => { + await poHomeChannel.content.sendMessage('message to be edited'); + await poHomeChannel.content.openLastMessageMenu(); + await poHomeChannel.content.btnOptionEditMessage.click(); + + await poHomeChannel.content.dragAndDropTxtFile(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); + }); + test('should send lst file successfully', async () => { await poHomeChannel.content.dragAndDropLstFile(); await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst'); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(TEST_FILE_LST); }); test('should send drawio (unknown media type) file successfully', async ({ page }) => { - const fileName = 'diagram.drawio'; await page.reload(); - await poHomeChannel.content.sendFileMessage(fileName); + await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO); await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); - await expect(poHomeChannel.content.lastMessageFileName).toContainText(fileName); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(TEST_FILE_DRAWIO); }); test('should not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { - const fileName = 'diagram.drawio'; await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); await page.reload(); - await poHomeChannel.content.sendFileMessage(fileName, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO, { waitForResponse: false }); - await expect(poHomeChannel.composer.getFileByName(fileName)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_DRAWIO)).toHaveAttribute('readonly'); }); test.describe.serial('multiple file upload', () => { @@ -96,52 +111,34 @@ test.describe.serial('file-upload', () => { await setSettingValueById(api, 'FileUpload_EnableMultipleFilesPerMessage', false); }); - test('should send multiple files successfully', async () => { - const file1 = 'any_file.txt'; - const file2 = 'lst-test.lst'; - - await poHomeChannel.content.sendFileMessage(file1); - await poHomeChannel.content.sendFileMessage(file2); + test('should attach multiple files and send in a single message', async () => { + const message = 'Here are two files'; + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessage(TEST_FILE_LST); + expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)); + expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)); + await poHomeChannel.composer.inputMessage.fill(message); await poHomeChannel.composer.btnSend.click(); - - await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); - await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); - await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); + await expect(poHomeChannel.content.lastUserMessage).toContainText(message); + await expect(poHomeChannel.content.lastUserMessage).toContainText(TEST_FILE_TXT); + await expect(poHomeChannel.content.lastUserMessage).toContainText(TEST_FILE_LST); + await expect(poHomeChannel.content.lastUserMessageDownloadLink).toHaveCount(2); }); test('should be able to remove file from composer before sending', async () => { - const file1 = 'any_file.txt'; - const file2 = 'lst-test.lst'; - - await poHomeChannel.content.sendFileMessage(file1); - await poHomeChannel.content.sendFileMessage(file2); + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessage(TEST_FILE_LST); - await poHomeChannel.composer.removeFileByName(file1); + await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT); - await expect(poHomeChannel.composer.getFileByName(file1)).not.toBeVisible(); - await expect(poHomeChannel.composer.getFileByName(file2)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)).toBeVisible(); await poHomeChannel.composer.btnSend.click(); - await expect(poHomeChannel.content.lastUserMessage).not.toContainText(file1); - await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); - }); - - test('should send multiple files with text message successfully', async () => { - const file1 = 'any_file.txt'; - const file2 = 'lst-test.lst'; - const message = 'Here are two files'; - - await poHomeChannel.content.sendFileMessage(file1); - await poHomeChannel.content.sendFileMessage(file2); - await poHomeChannel.composer.inputMessage.fill(message); - - await poHomeChannel.composer.btnSend.click(); - - await expect(poHomeChannel.content.lastUserMessage).toContainText(message); - await expect(poHomeChannel.content.lastUserMessage).toContainText(file1); - await expect(poHomeChannel.content.lastUserMessage).toContainText(file2); + await expect(poHomeChannel.content.lastUserMessage).not.toContainText(TEST_FILE_TXT); + await expect(poHomeChannel.content.lastUserMessage).toContainText(TEST_FILE_LST); }); test('should respect the maximum number of files allowed per message: 10', async () => { @@ -159,32 +156,34 @@ test.describe.serial('file-upload', () => { test('should be able to remove file from thread composer before sending', async () => { await poHomeChannel.content.sendMessage('this is a message for thread reply'); await poHomeChannel.content.openReplyInThread(); - await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); - await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_LST); - await poHomeChannel.threadComposer.removeFileByName('another_file.txt'); + await poHomeChannel.threadComposer.removeFileByName(TEST_FILE_LST); - await expect(poHomeChannel.threadComposer.getFileByName('any_file.txt')).toBeVisible(); - await expect(poHomeChannel.threadComposer.getFileByName('another_file.txt')).not.toBeVisible(); + await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_TXT)).toBeVisible(); + await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_LST)).not.toBeVisible(); }); - test('should send multiple files in a thread successfully', async () => { + test('should send multiple files in a single thread message', async () => { const message = 'Here are two files in thread'; await poHomeChannel.content.openReplyInThread(); - await poHomeChannel.content.sendFileMessageToThread('any_file.txt'); - await poHomeChannel.content.sendFileMessageToThread('another_file.txt'); + await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_LST); await poHomeChannel.threadComposer.inputMessage.fill(message); await poHomeChannel.threadComposer.btnSend.click(); await expect(poHomeChannel.content.lastThreadMessageText).toContainText(message); - await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('another_file.txt')).toBeVisible(); - await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText('any_file.txt')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText(TEST_FILE_LST)).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText.getByRole('link').getByText(TEST_FILE_TXT)).toBeVisible(); }); }); }); test.describe.serial('file upload fails', () => { + let fileUploadWarningModal: FileUploadWarningModal; + test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); }); @@ -193,64 +192,55 @@ test.describe.serial('file-upload', () => { await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); }); - test('should open warning modal when all file uploads fail', async () => { - const invalidFile1 = 'empty_file.txt'; - const invalidFile2 = 'diagram.drawio'; + test('should open warning modal when all file uploads fail', async ({ page }) => { + fileUploadWarningModal = new FileUploadWarningModal(page.getByRole('dialog', { name: 'Warning' })); - await poHomeChannel.content.sendFileMessage(invalidFile1, { waitForResponse: false }); - await poHomeChannel.content.sendFileMessage(invalidFile2, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_EMPTY_FILE, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO, { waitForResponse: false }); - await expect(poHomeChannel.composer.getFileByName(invalidFile1)).toHaveAttribute('readonly'); - await expect(poHomeChannel.composer.getFileByName(invalidFile2)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_DRAWIO)).toHaveAttribute('readonly'); await poHomeChannel.composer.btnSend.click(); - const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Warning' }); - await expect(warningModal).toBeVisible(); - await expect(warningModal).toContainText('2 files failed to upload'); - await expect(warningModal.getByRole('button', { name: 'Ok' })).toBeVisible(); - await expect(warningModal.getByRole('button', { name: 'Send anyway' })).not.toBeVisible(); + await fileUploadWarningModal.waitForDisplay(); + + await expect(fileUploadWarningModal.getContent('2 files failed to upload')).toBeVisible(); + await expect(fileUploadWarningModal.btnOk).toBeVisible(); + await expect(fileUploadWarningModal.btnSendAnyway).not.toBeVisible(); }); - test('should handle multiple files with one failing upload', async () => { - const validFile = 'any_file.txt'; - const invalidFile = 'empty_file.txt'; + test('should handle multiple files with one failing upload', async ({ page }) => { + fileUploadWarningModal = new FileUploadWarningModal(page.getByRole('dialog', { name: 'Are you sure' })); await test.step('should only mark as "Upload failed" the specific file that failed to upload', async () => { - await poHomeChannel.content.sendFileMessage(validFile, { waitForResponse: false }); - await poHomeChannel.content.sendFileMessage(invalidFile, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_EMPTY_FILE, { waitForResponse: false }); - await expect(poHomeChannel.composer.getFileByName(validFile)).not.toHaveAttribute('readonly'); - await expect(poHomeChannel.composer.getFileByName(invalidFile)).toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toHaveAttribute('readonly'); + await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toHaveAttribute('readonly'); }); await test.step('should open warning modal', async () => { await poHomeChannel.composer.btnSend.click(); + await fileUploadWarningModal.waitForDisplay(); - const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); - await expect(warningModal).toBeVisible(); - await expect(warningModal).toContainText('One file failed to upload'); + await expect(fileUploadWarningModal.getContent('One file failed to upload')).toBeVisible(); }); await test.step('should close modal when clicking "Cancel" button', async () => { - const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); - await warningModal.getByRole('button', { name: 'Cancel' }).click(); + await fileUploadWarningModal.cancel(); - await expect(warningModal).not.toBeVisible(); - await expect(poHomeChannel.composer.getFileByName(invalidFile)).toBeVisible(); - await expect(poHomeChannel.composer.getFileByName(validFile)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toBeVisible(); }); await test.step('should send message with the valid file when confirming "Send anyway"', async () => { await poHomeChannel.composer.btnSend.click(); + await fileUploadWarningModal.confirmSend(); - const warningModal = poHomeChannel.page.getByRole('dialog', { name: 'Are you sure' }); - - await warningModal.getByRole('button', { name: 'Send anyway' }).click(); - - await expect(warningModal).not.toBeVisible(); - await expect(poHomeChannel.composer.getFileByName(validFile)).not.toBeVisible(); - await expect(poHomeChannel.content.lastMessageFileName).toContainText(validFile); - await expect(poHomeChannel.composer.getFileByName(invalidFile)).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); + await expect(poHomeChannel.content.lastMessageFileName).toContainText(TEST_FILE_TXT); + await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).not.toBeVisible(); }); }); }); @@ -278,6 +268,6 @@ test.describe('file-upload-not-member', () => { test('should not be able to upload if not a member', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.composer.getFileByName('any_file.txt')).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 11ac178c24023..0bee4d37ac945 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -46,6 +46,10 @@ export class HomeContent { return this.page.locator('[data-qa-type="message"]').last(); } + get lastUserMessageDownloadLink(): Locator { + return this.lastUserMessage.getByRole('link', { name: 'Download' }); + } + nthMessage(index: number): Locator { return this.page.locator('[data-qa-type="message"]').nth(index); } @@ -135,7 +139,7 @@ export class HomeContent { return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--secondary'); } - get fileUploadModal(): Locator { + private get fileUploadModal(): Locator { return this.page.getByRole('dialog', { name: 'File Upload' }); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts index 4ee33ebc8307f..6db5ce1c0a6a7 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts @@ -1,4 +1,4 @@ -import type { Page } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; import { Modal } from './modal'; @@ -32,3 +32,35 @@ export class FileUploadModal extends Modal { await this.waitForDismissal(); } } + +export class FileUploadWarningModal extends Modal { + constructor(root: Locator) { + super(root); + } + + get btnOk() { + return this.root.getByRole('button', { name: 'Ok' }); + } + + get btnSendAnyway() { + return this.root.getByRole('button', { name: 'Send anyway' }); + } + + getContent(text: string) { + return this.root.getByText(text); + } + + private get btnCancel() { + return this.root.getByRole('button', { name: 'Cancel' }); + } + + async cancel() { + await this.btnCancel.click(); + await this.waitForDismissal(); + } + + async confirmSend() { + await this.btnSendAnyway.click(); + await this.waitForDismissal(); + } +} From cc3efee03752a81775c532405574ad29d17bca29 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 10 Feb 2026 13:17:25 -0300 Subject: [PATCH 213/215] fix: do not allow drop files in composer when editing message --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 1 + .../views/room/body/hooks/useFileUploadDropTarget.ts | 11 ++++++++--- apps/meteor/tests/e2e/file-upload.spec.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 7617e15b8b7db..583dfb0ddf9d3 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -124,6 +124,7 @@ export class ChatMessages implements ChatAPI { await this.currentEditingMessage.stop(); }, editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => { + message.tmid ? this.threadUploads.clear() : this.uploads.clear(); const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg; await this.currentEditingMessage.stop(); diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 710bb80901e4b..ba7b797c086cd 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -1,7 +1,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useSetting, useTranslation, useUser } from '@rocket.chat/ui-contexts'; import type { DragEvent, ReactNode } from 'react'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { useDropTarget } from './useDropTarget'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; @@ -40,6 +40,11 @@ export const useFileUploadDropTarget = ( const chat = useChat(); const subscription = useRoomSubscription(); + const isEditing = useSyncExternalStore( + chat?.composer?.editing.subscribe ?? (() => () => undefined), + chat?.composer?.editing.get ?? (() => false), + ); + const onFileDrop = useEffectEvent(async (files: File[]) => { const { getMimeType } = await import('../../../../../app/utils/lib/mimeTypes'); const getUniqueFiles = () => { @@ -73,7 +78,7 @@ export const useFileUploadDropTarget = ( } as const; } - if (!fileUploadAllowedForUser || !subscription) { + if (!fileUploadAllowedForUser || !subscription || isEditing) { return { enabled: false, reason: t('error-not-allowed'), @@ -86,7 +91,7 @@ export const useFileUploadDropTarget = ( onFileDrop, ...overlayProps, } as const; - }, [fileUploadAllowedForUser, fileUploadEnabled, isRoomOverMacLimit, onFileDrop, overlayProps, subscription, t]); + }, [isEditing, fileUploadAllowedForUser, fileUploadEnabled, isRoomOverMacLimit, onFileDrop, overlayProps, subscription, t]); return [triggerProps, allOverlayProps] as const; }; diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index deb1f9aa243e6..f6507561e44c9 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -67,7 +67,7 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.lastUserMessageDownloadLink).toHaveCount(1); }); - test.fixme('should not be able to attach files when editing a message', async () => { + test('should not be able to attach files when editing a message', async () => { await poHomeChannel.content.sendMessage('message to be edited'); await poHomeChannel.content.openLastMessageMenu(); await poHomeChannel.content.btnOptionEditMessage.click(); From eb9f40494730cdc05a8baafb559db2247b83c170 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 10 Feb 2026 13:17:50 -0300 Subject: [PATCH 214/215] fix: keep original file and files in e2ee messages --- apps/meteor/client/lib/chats/flows/sendMessage.ts | 13 +++++-------- .../e2e/e2e-encryption/e2ee-file-encryption.spec.ts | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index d7b4fa10ae3ac..cf5ff80c58b31 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -81,18 +81,15 @@ export const sendMessage = async ( originalMessage: mid ? await chat.data.findMessageByID(mid) : null, }); + // When editing an encrypted message with files, preserve the original attachments/files + // This ensures they're included in the re-encryption process if (mid) { const originalMessage = await chat.data.findMessageByID(mid); - if ( - originalMessage?.t === 'e2e' && - originalMessage.attachments && - originalMessage.attachments.length > 0 && - originalMessage.attachments[0].description !== undefined - ) { - originalMessage.attachments[0].description = message.msg; + if (originalMessage?.t === 'e2e' && originalMessage.attachments && originalMessage.attachments.length > 0) { message.attachments = originalMessage.attachments; - message.msg = originalMessage.msg; + message.files = originalMessage.files; + message.file = originalMessage.file; } } diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index 8d29cb4286172..30c0174ef5977 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -40,7 +40,7 @@ test.describe('E2EE File Encryption', () => { await page.goto('/home'); }); - test.fixme('should edit encrypted message with file', async ({ page }) => { + test('should edit encrypted message with file', async ({ page }) => { const updatedFileName = `edited_${TEST_FILE_TXT}`; await test.step('create an encrypted channel', async () => { From 52dc433d7de0103091236c71461d3f064805012e Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 10 Feb 2026 13:53:47 -0300 Subject: [PATCH 215/215] test: remove new stale test :clown: --- apps/meteor/tests/e2e/image-upload.spec.ts | 1 - apps/meteor/tests/e2e/page-objects/home-channel.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/apps/meteor/tests/e2e/image-upload.spec.ts b/apps/meteor/tests/e2e/image-upload.spec.ts index 1aa7a68aaa754..f6d7d4f6e9ee1 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -38,7 +38,6 @@ test.describe('image-upload', () => { await poHomeChannel.content.sendFileMessage('bad-orientation.jpeg'); await expect(poHomeChannel.composer.getFileByName('bad-orientation')).toHaveAttribute('readonly'); - await expect(poHomeChannel.statusUploadError).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 3a838eff0b61d..5c953ff072acf 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -97,10 +97,6 @@ export class HomeChannel { return this.page.getByRole('group', { name: 'Audio recorder', exact: true }); } - get statusUploadError(): Locator { - return this.page.getByRole('main').getByRole('status').getByText('Error'); - } - get homepageHeader(): Locator { return this.page.locator('main').getByRole('heading', { name: 'Home' }); }