diff --git a/.changeset/spotty-news-burn.md b/.changeset/spotty-news-burn.md new file mode 100644 index 0000000000000..ffdb604dd1ced --- /dev/null +++ b/.changeset/spotty-news-burn.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ui-composer': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds support for multiple files in message composer, improving file upload experience diff --git a/.github/actions/update-version-durability/index.js b/.github/actions/update-version-durability/index.js index 2065ac6660827..d5fbeffb5de49 100644 --- a/.github/actions/update-version-durability/index.js +++ b/.github/actions/update-version-durability/index.js @@ -103,7 +103,7 @@ async function generateTable({ owner, repo } = {}) { minorDate.setDate(1); supportDateStart = minorDate; supportDate = new Date(minorDate); - supportDate.setMonth(supportDate.getMonth() + (lts ? 9 : 6)); + supportDate.setMonth(supportDate.getMonth() + (lts ? 12 : 6)); releaseData.push({ release: { diff --git a/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch b/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch new file mode 100644 index 0000000000000..a8271b2de103b --- /dev/null +++ b/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch @@ -0,0 +1,15 @@ +diff --git a/lib/index.js b/lib/index.js +index c64bdf2d5f7e704a65be4e9a7116c5ee6a582701..ce6641d0b63daf7c5d3f8a1de773f290c0e9d51c 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -2,8 +2,8 @@ import { upperFirst, capitalize, parseFloat as parseFloat$1, without, pick, comp + import * as P from '@react-pdf/primitives'; + import resolveStyle, { transformColor, flatten } from '@react-pdf/stylesheet'; + import layoutEngine, { fontSubstitution, wordHyphenation, scriptItemizer, textDecoration, justification, linebreaker, bidi, fromFragments } from '@react-pdf/textkit'; +-import * as Yoga from 'yoga-layout/load'; +-import { loadYoga as loadYoga$1 } from 'yoga-layout/load'; ++import * as Yoga from 'yoga-layout/dist/src/load.js'; ++import { loadYoga as loadYoga$1 } from 'yoga-layout/dist/src/load.js'; + import emojiRegex from 'emoji-regex-xs'; + import resolveImage from '@react-pdf/image'; + diff --git a/.yarn/patches/yoga-layout-npm-3.2.1-51ec934670.patch b/.yarn/patches/yoga-layout-npm-3.2.1-51ec934670.patch new file mode 100644 index 0000000000000..c67d2ce23858f --- /dev/null +++ b/.yarn/patches/yoga-layout-npm-3.2.1-51ec934670.patch @@ -0,0 +1,26 @@ +diff --git a/dist/binaries/yoga-wasm-base64-esm.js b/dist/binaries/yoga-wasm-base64-esm.js +index 350866aeaf90bdcc7bea18adfaae1cfcf2e40af6..a973419e8569791396b7a123599fd464b8c0cff4 100644 +--- a/dist/binaries/yoga-wasm-base64-esm.js ++++ b/dist/binaries/yoga-wasm-base64-esm.js +@@ -1,6 +1,6 @@ + + var loadYoga = (() => { +- var _scriptDir = import.meta.url; ++ var _scriptDir = undefined; + + return ( + function(loadYoga) { +diff --git a/package.json b/package.json +index 1fb0482c9451d745ca010f9c1ad58f5d0f74a559..8f7705e1325c046fd671ddc163bc34b74d4389cf 100644 +--- a/package.json ++++ b/package.json +@@ -14,7 +14,8 @@ + "types": "./dist/src/index.d.ts", + "exports": { + ".": "./dist/src/index.js", +- "./load": "./dist/src/load.js" ++ "./load": "./dist/src/load.js", ++ "./dist/src/load.js": "./dist/src/load.js" + }, + "files": [ + "dist/binaries/**", diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 835c6cc5fd65b..e5f2fccc87270 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -291,6 +291,16 @@ API.v1.addRoute( file.description = this.bodyParams.description; delete this.bodyParams.description; + if (this.bodyParams.fileName) { + file.name = this.bodyParams.fileName; + delete this.bodyParams.fileName; + } + + if (this.bodyParams.fileContent) { + file.content = this.bodyParams.fileContent; + delete this.bodyParams.fileContent; + } + await applyAirGappedRestrictionsValidation(() => sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }), ); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 44cbf6bdbea43..583dfb0ddf9d3 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -7,6 +7,7 @@ import { UserAction } from './UserAction'; import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI'; import { createDataAPI } from '../../../../client/lib/chats/data'; 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'; @@ -44,6 +45,8 @@ export class ChatMessages implements ChatAPI { public uploads: UploadsAPI; + public threadUploads: UploadsAPI; + public ActionManager: any; public emojiPicker: { @@ -121,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(); @@ -147,7 +151,8 @@ export class ChatMessages implements ChatAPI { this.tmid = tmid; this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); - this.uploads = createUploadsAPI({ rid, tmid }); + this.uploads = createUploadsAPI({ rid }); + this.threadUploads = createUploadsAPI({ rid }); this.ActionManager = params.actionManager; this.currentEditingMessage = new CurrentEditingMessage(this); @@ -180,6 +185,7 @@ export class ChatMessages implements ChatAPI { 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 96f6c6f03a5f4..2f0594760c746 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -2,7 +2,7 @@ import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload } from '@roc import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { RefObject } from 'react'; -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'; @@ -100,17 +100,24 @@ export type DataAPI = { getSubscriptionFromMessage(message: IMessage): Promise; }; +export type EncryptedFileUploadContent = { + rawFile: File; + fileContent: { raw: Partial; encrypted?: IE2EEMessage['content'] }; + encryptedFile: EncryptedFile; +}; + export type UploadsAPI = { get(): readonly Upload[]; subscribe(callback: () => void): () => void; wipeFailedOnes(): void; + clear(): void; + getProcessingUploads(): boolean; + setProcessingUploads(processing: boolean): void; cancel(id: Upload['id']): 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; + removeUpload(id: Upload['id']): void; + editUploadFileName: (id: Upload['id'], fileName: string) => void; + send(file: File, encrypted?: never): Promise; + send(file: File, encrypted: EncryptedFileUploadContent): Promise; }; export type ChatAPI = { @@ -119,6 +126,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; @@ -148,7 +156,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 sendMessage: ({ text, tshow, @@ -157,6 +173,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; @@ -164,6 +181,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/Upload.ts b/apps/meteor/client/lib/chats/Upload.ts index a2d6bf18cd3ce..798d955032049 100644 --- a/apps/meteor/client/lib/chats/Upload.ts +++ b/apps/meteor/client/lib/chats/Upload.ts @@ -1,6 +1,27 @@ -export type Upload = { +import type { IUpload } from '@rocket.chat/core-typings'; + +export type NonEncryptedUpload = { readonly id: string; - readonly name: string; + readonly file: File; + readonly url?: string; readonly percentage: number; readonly error?: Error; }; + +export type EncryptedUpload = NonEncryptedUpload & { + readonly encryptedFile: EncryptedFile; + readonly metadataForEncryption: Partial; +}; + +export type Upload = EncryptedUpload | NonEncryptedUpload; + +export type EncryptedFile = { + readonly file: File; + readonly key: JsonWebKey; + readonly iv: string; + readonly type: File['type']; + readonly hash: string; +}; + +export const isEncryptedUpload = (upload: Upload): upload is EncryptedUpload => + 'encryptedFile' in upload && upload.encryptedFile !== undefined; diff --git a/apps/meteor/client/lib/chats/flows/processMessageEditing.ts b/apps/meteor/client/lib/chats/flows/processMessageEditing.ts index 0985b5ca6d974..638038d30edf7 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageEditing.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageEditing.ts @@ -23,6 +23,7 @@ export const processMessageEditing = async ( } try { + chat.composer?.clear(); await chat.data.updateMessage({ ...message, _id: mid }, previewUrls); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/lib/chats/flows/processMessageUploads.ts b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts new file mode 100644 index 0000000000000..7ea3b8ffff4de --- /dev/null +++ b/apps/meteor/client/lib/chats/flows/processMessageUploads.ts @@ -0,0 +1,212 @@ +import type { AtLeast, FileAttachmentProps, IE2EEMessage, 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'; +import { dispatchToastMessage } from '../../toast'; +import type { ChatAPI, UploadsAPI } from '../ChatAPI'; +import { isEncryptedUpload, type EncryptedUpload } from '../Upload'; + +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.onerror = () => { + reject(new Error('Failed to load image for dimensions')); + }; + img.src = dataURL; + }); +}; + +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, + }, + fileId: fileToUpload.id, + }; + + 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)), + }), + }; +}; + +const getEncryptedContent = async (filesToUpload: readonly EncryptedUpload[], e2eRoom: E2ERoom, msg: string) => { + const attachments: FileAttachmentProps[] = []; + + const arrayOfFiles = await Promise.all( + filesToUpload.map(async (fileToUpload) => { + attachments.push(await getAttachmentForFile(fileToUpload)); + + const file = { + _id: fileToUpload.id, + name: fileToUpload.file.name, + type: fileToUpload.file.type, + size: fileToUpload.file.size, + format: getFileExtension(fileToUpload.file.name), + }; + + return file; + }), + ); + + return e2eRoom.encryptMessageContent({ + attachments, + files: arrayOfFiles, + file: arrayOfFiles[0], + msg, + }); +}; + +async function continueSendingMessage(chat: ChatAPI, store: UploadsAPI, message: IMessage) { + const { msg, rid, tmid } = message; + const e2eRoom = await e2e.getInstanceByRoomId(rid); + const shouldConvertSentMessages = await e2eRoom?.shouldConvertSentMessages({ msg }); + const filesToUpload = store.get(); + + const confirmFilesQueue: (IUploadToConfirm & { + composedMessage: AtLeast & { fileName?: string; fileContent?: IE2EEMessage['content'] }; + })[] = []; + + const validFiles = filesToUpload.filter((file) => !file.error); + + for (const upload of validFiles) { + if (!upload.url || !upload.id) { + continue; + } + + /** + * The first message will keep the composedMessage, + * subsequent messages will have a empty text + * */ + const currentMsg = upload === validFiles[0] ? msg : ''; + + let content; + if (!e2eRoom || !isEncryptedUpload(upload)) { + confirmFilesQueue.push({ + _id: upload.id, + name: upload.file.name, + composedMessage: { tmid, msg: currentMsg, fileName: upload.file.name }, + }); + continue; + } + + const fileContent = await e2eRoom.encryptMessageContent(upload.metadataForEncryption); + + if (shouldConvertSentMessages) { + content = await getEncryptedContent([upload], e2eRoom, currentMsg); + } + + const composedMessage = { + tmid, + content, + t: 'e2e', + msg: '', + fileContent, + } as const; + + confirmFilesQueue.push({ _id: upload.id, name: upload.file.name, content: fileContent, composedMessage }); + } + + try { + store.setProcessingUploads(true); + for (const fileToConfirm of confirmFilesQueue) { + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${fileToConfirm._id}`, fileToConfirm.composedMessage); + } + store.clear(); + } catch (error: unknown) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + store.setProcessingUploads(false); + chat.action.stop('uploading'); + } + + return true; +} + +export const processMessageUploads = async (chat: ChatAPI, message: IMessage): Promise => { + const { tmid } = message; + + const store = tmid ? chat.threadUploads : chat.uploads; + const filesToUpload = store.get(); + + if (filesToUpload.length === 0) { + return false; + } + + const failedUploads = filesToUpload.filter((upload) => upload.error); + + if (!failedUploads.length) { + return continueSendingMessage(chat, store, message); + } + + 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(); + }, + }), + ...(!allUploadsFailed && { + title: t('Are_you_sure'), + confirmText: t('Send_anyway'), + cancelText: t('Cancel'), + onConfirm: () => { + imperativeModal.close(); + failedUploads.forEach((upload) => store.removeUpload(upload.id)); + resolve(continueSendingMessage(chat, store, message)); + }, + onCancel: () => { + imperativeModal.close(); + }, + }), + onClose: () => { + imperativeModal.close(); + }, + }, + }); + }); +}; diff --git a/apps/meteor/client/lib/chats/flows/processSetReaction.ts b/apps/meteor/client/lib/chats/flows/processSetReaction.ts index 172886960768b..1478320ef3e0b 100644 --- a/apps/meteor/client/lib/chats/flows/processSetReaction.ts +++ b/apps/meteor/client/lib/chats/flows/processSetReaction.ts @@ -21,6 +21,7 @@ export const processSetReaction = async (chat: ChatAPI, { msg }: Pick): Promise => { +export const processTooLongMessage = async (chat: ChatAPI, { msg, tmid }: Pick): Promise => { const maxAllowedSize = settings.peek('Message_MaxAllowedSize'); if (msg.length <= maxAllowedSize) { @@ -32,15 +32,14 @@ export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick { - chat.composer?.setText(msg); - imperativeModal.close(); resolve(); }; diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index dd4fae1deb952..9312d981f288b 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -1,9 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { processMessageEditing } from './processMessageEditing'; -import { processSetReaction } from './processSetReaction'; -import { processSlashCommand } from './processSlashCommand'; -import { processTooLongMessage } from './processTooLongMessage'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; import { closeUnclosedCodeBlock } from '../../../../lib/utils/closeUnclosedCodeBlock'; @@ -11,6 +7,11 @@ import { Messages } from '../../../stores'; 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'; const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise => { const mid = chat.currentEditingMessage.getMID(); @@ -27,6 +28,11 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], return; } + if (await processMessageUploads(chat, message)) { + chat.composer?.clear(); + return; + } + message = (await onClientBeforeSendMessage({ ...message, isEditing: !!mid })) as IMessage & { isEditing?: boolean }; // e2e should be a client property only @@ -37,6 +43,7 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], return; } + chat.composer?.clear(); await sdk.call('sendMessage', message, previewUrls); // after the request is complete we can go ahead and mark as sent @@ -53,7 +60,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 { @@ -66,33 +74,33 @@ export const sendMessage = async ( chat.readStateManager.clearUnreadMark(); + const uploadsStore = tmid ? chat.threadUploads : chat.uploads; + text = text.trim(); text = closeUnclosedCodeBlock(text); const mid = chat.currentEditingMessage.getMID(); - if (!text && !mid) { + + const hasFiles = uploadsStore.get().length > 0; + if (!text && !mid && !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() ?? [], 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.file = originalMessage.file; } } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index fee271eebbf3d..04de6c86dfdb3 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,207 +1,77 @@ -import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { imperativeModal } from '@rocket.chat/ui-client'; - -import { fileUploadIsValidContentType } from '../../../../app/utils/client'; -import { getFileExtension } from '../../../../lib/utils/getFileExtension'; -import FileUploadModal from '../../../views/room/modals/FileUploadModal'; +import { t } from '../../../../app/utils/lib/i18n'; +import { MAX_MULTIPLE_UPLOADED_FILES } from '../../../../lib/constants'; import { e2e } from '../../e2ee'; import { settings } from '../../settings'; -import { prependReplies } from '../../utils/prependReplies'; -import type { ChatAPI } 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; - }); -}; - -export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { - const replies = chat.composer?.quotedMessages.get() ?? []; - - const msg = await prependReplies('', replies); +import { dispatchToastMessage } from '../../toast'; +import type { ChatAPI, UploadsAPI } from '../ChatAPI'; + +export const uploadFiles = async ( + chat: ChatAPI, + { files, uploadsStore, resetFileInput }: { files: readonly File[]; uploadsStore: UploadsAPI; resetFileInput?: () => void }, +): Promise => { + 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 }), + }); + } const room = await chat.data.getRoom(); - const queue = [...files]; + if (room.encrypted && !settings.peek('E2E_Allow_Unencrypted_Messages') && !settings.peek('E2E_Enable_Encrypt_Files')) { + return dispatchToastMessage({ + type: 'error', + message: t('You_cant_send_unencrypted_files_in_an_encrypted_room'), + }); + } - const uploadFile = ( - file: File, - extraData?: Pick & { description?: string }, - getContent?: (fileId: string, fileUrl: string) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, - ) => { - chat.uploads.send( - file, - { - msg, - ...extraData, - }, - getContent, - fileContent, - ); - chat.composer?.clear(); - imperativeModal.close(); - uploadNextFile(); - }; + const uploadFile = async (file: File) => { + Object.defineProperty(file, 'name', { + writable: true, + value: file.name, + }); + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); - const uploadNextFile = (): void => { - const file = queue.pop(); - if (!file) { - chat.composer?.dismissAllQuotedMessages(); + if (!e2eRoom || !settings.peek('E2E_Enable_Encrypt_Files')) { + await uploadsStore.send(file); 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, description): 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.peek('E2E_Enable_Encrypt_Files')) { - uploadFile(file, { description }); - return; - } - - const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + const encryptedFile = await e2eRoom.encryptFile(file); - if (!shouldConvertSentMessages) { - uploadFile(file, { description }); - return; - } - - const encryptedFile = await e2eRoom.encryptFile(file); - - if (encryptedFile) { - 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, - }, - hashes: { - sha256: encryptedFile.hash, - }, - fileId: _id, - }; - - 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], - }); - }; + if (!e2eRoom.isReady() || !encryptedFile) { + dispatchToastMessage({ + type: 'error', + message: t('Error_encrypting_file'), + }); + return; + } - const fileContentData = { - type: file.type, - typeGroup: file.type.split('/')[0], - name: fileName, - encryption: { - key: encryptedFile.key, - iv: encryptedFile.iv, - }, - hashes: { - sha256: encryptedFile.hash, - }, - }; + const fileContentData = { + type: file.type, + typeGroup: file.type.split('/')[0], + name: file.name, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; - const fileContent = { - raw: fileContentData, - encrypted: await e2eRoom.encryptMessageContent(fileContentData), - }; + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), + }; - uploadFile( - encryptedFile.file, - { - t: 'e2e', - }, - getContent, - fileContent, - ); - } - }, - invalidContentType: !fileUploadIsValidContentType(file.type), - }, - }); + await uploadsStore.send(encryptedFile.file, { rawFile: file, fileContent, encryptedFile }); }; - uploadNextFile(); resetFileInput?.(); + chat?.action.performContinuously('uploading'); + + await Promise.allSettled(files.map((file) => uploadFile(file))); }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index ce475b891e689..f79f8be5d91c7 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,184 +1,190 @@ -import type { IMessage, IRoom, IE2EEMessage, IUpload } 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 { sdk } from '../../../app/utils/client/lib/SDKClient'; import { getErrorMessage } from '../errorHandling'; -import type { UploadsAPI } from './ChatAPI'; -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'); -}; - -const get = (): readonly Upload[] => uploads; - -const subscribe = (callback: () => void): (() => void) => emitter.on('update', callback); - -const cancel = (id: Upload['id']): void => { - emitter.emit(`cancelling-${id}`); -}; - -const wipeFailedOnes = (): void => { - updateUploads((uploads) => uploads.filter((upload) => !upload.error)); -}; - -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: IE2EEMessage['content'] }, -): Promise => { - const id = Random.id(); - - const upload: Upload = { - id, - name: fileContent?.raw.name || file.name, - percentage: 0, +import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI'; +import { isEncryptedUpload, type Upload } from './Upload'; +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; + + constructor({ rid }: { rid: string }) { + super(); + + this.rid = rid; + } + + private uploads: readonly Upload[] = []; + + private processingUploads: boolean = false; + + set = (uploads: Upload[]): void => { + this.uploads = uploads; + this.emit('update'); }; - updateUploads((uploads) => [...uploads, upload]); - - try { - await new Promise((resolve, reject) => { - const xhr = sdk.rest.upload( - `/v1/rooms.media/${rid}`, - { - file, - ...(fileContent && { - content: JSON.stringify(fileContent.encrypted), - }), - }, - { - load: (event) => { - resolve(event); - }, - progress: (event) => { - if (!event.lengthComputable) { - return; - } - const progress = (event.loaded / event.total) * 100; - if (progress === 100) { - return; - } + get = (): readonly Upload[] => this.uploads; - 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); - }, - }, - ); + subscribe = (callback: () => void): (() => void) => this.on('update', callback); - xhr.onload = async () => { - if (xhr.readyState === xhr.DONE) { - if (xhr.status === 400) { - const error = JSON.parse(xhr.responseText); - updateUploads((uploads) => [...uploads, { ...upload, error: new Error(error.error) }]); - return; - } + setProcessingUploads = (processing: boolean): void => { + this.processingUploads = processing; + this.emit('update'); + }; - if (xhr.status === 200) { - const result = JSON.parse(xhr.responseText); - let content; - if (getContent) { - content = await getContent(result.file._id, result.file.url); - } + getProcessingUploads = (): boolean => this.processingUploads; + + cancel = (id: Upload['id']): void => { + this.emit(`cancelling-${id}`); + }; + + wipeFailedOnes = (): void => { + this.set(this.uploads.filter((upload) => !upload.error)); + }; + + private updateUpload(id: Upload['id'], patch: Partial): void { + this.set(this.uploads.map((upload) => (upload.id !== id ? upload : { ...upload, ...patch }))); + } + + removeUpload = (id: Upload['id']): void => { + this.set(this.uploads.filter((upload) => upload.id !== id)); + }; + + editUploadFileName = (uploadId: Upload['id'], fileName: Upload['file']['name']) => { + try { + this.set( + this.uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; + } - await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { - msg, - tmid, - description, - t, - content, - }); + return { + ...upload, + file: new File([upload.file], fileName, upload.file), + ...(isEncryptedUpload(upload) && { + metadataForEncryption: { ...upload.metadataForEncryption, name: fileName }, + }), + }; + }), + ); + } catch (error) { + this.set( + this.uploads.map((upload) => { + if (upload.id !== uploadId) { + return upload; } + + return { + ...upload, + percentage: 0, + error: new Error(i18n.t('FileUpload_Update_Failed')), + }; + }), + ); + } + }; + + clear = () => this.set([]); + + async send(file: File, encrypted?: EncryptedFileUploadContent): Promise { + const maxFileSize = settings.peek('FileUpload_MaxFileSize'); + const invalidContentType = !fileUploadIsValidContentType(encrypted ? encrypted.rawFile.type : file.type); + const id = Random.id(); + + this.set([ + ...this.uploads, + { + id, + file: encrypted ? encrypted.rawFile : file, + percentage: 0, + ...(encrypted && { + encryptedFile: encrypted.encryptedFile, + metadataForEncryption: encrypted.fileContent.raw, + }), + }, + ]); + + try { + await new Promise((resolve, reject) => { + if (file.size === 0) { + return reject(new Error(i18n.t('FileUpload_File_Empty'))); } - }; - if (uploads.length) { - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); - } + // -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) }))); + } - emitter.once(`cancelling-${id}`, () => { - xhr.abort(); - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - }); - }); - - updateUploads((uploads) => uploads.filter((upload) => upload.id !== id)); - } catch (error: unknown) { - updateUploads((uploads) => - uploads.map((upload) => { - if (upload.id !== id) { - return upload; + if (invalidContentType) { + return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type }))); } - return { - ...upload, - percentage: 0, - error: new Error(getErrorMessage(error)), + const xhr = sdk.rest.upload( + `/v1/rooms.media/${this.rid}`, + { + file, + ...(encrypted && { + content: JSON.stringify(encrypted.fileContent.encrypted), + }), + }, + { + load: (event) => { + resolve(event); + }, + progress: (event) => { + if (!event.lengthComputable) { + return; + } + const progress = (event.loaded / event.total) * 100; + this.updateUpload(id, { percentage: Math.round(progress) || 0 }); + }, + error: (event) => { + this.updateUpload(id, { percentage: 0, error: new Error(xhr.responseText) }); + reject(event); + }, + }, + ); + + xhr.onload = () => { + try { + if (xhr.readyState !== xhr.DONE) { + return; + } + + if (xhr.status === 400) { + const error = JSON.parse(xhr.responseText); + this.updateUpload(id, { percentage: 0, error: new Error(error.error) }); + return; + } + + if (xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + this.updateUpload(id, { id: result.file._id, url: result.file.url }); + return; + } + + this.updateUpload(id, { percentage: 0, error: new Error(i18n.t('FileUpload_Error')) }); + } catch (error) { + this.updateUpload(id, { percentage: 0, error: new Error(getErrorMessage(error)) }); + } }; - }), - ); - } finally { - if (!uploads.length) { - UserAction.stop(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); + + this.once(`cancelling-${id}`, () => { + xhr.abort(); + this.set(this.uploads.filter((upload) => upload.id !== id)); + reject(new Error(i18n.t('FileUpload_Canceled'))); + }); + }); + } catch (error: unknown) { + this.updateUpload(id, { percentage: 0, error: new Error(getErrorMessage(error)) }); } } -}; - -export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI => ({ - get, - subscribe, - wipeFailedOnes, - cancel, - send: ( - file: File, - { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, - getContent?: (fileId: string, fileUrl: string) => Promise, - fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, - ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), -}); +} + +export const createUploadsAPI = ({ rid }: { rid: IRoom['_id'] }): UploadsAPI => new UploadsStore({ rid }); diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts index 625e305365af1..e349e404fa08a 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts @@ -164,7 +164,7 @@ export class E2ERoom extends Emitter { this.setState('KEYS_RECEIVED'); } - async shouldConvertSentMessages(message: { msg: string }) { + async shouldConvertSentMessages(message: { msg?: string }) { if (!this.isReady() || this[PAUSED]) { return false; } @@ -175,7 +175,7 @@ export class E2ERoom extends Emitter { }); } - if (message.msg[0] === '/') { + if (message.msg?.[0] === '/') { return false; } 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 cff19dec9641d..22b4e8fc8cf1c 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -12,9 +12,15 @@ import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; import LoadingMessagesIndicator from './LoadingMessagesIndicator'; import RetentionPolicyWarning from './RetentionPolicyWarning'; +import RoomForeword from './RoomForeword/RoomForeword'; +import UnreadMessagesIndicator from './UnreadMessagesIndicator'; +import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; import RoomAnnouncement from '../RoomAnnouncement'; +import UploadProgressIndicator from './UploadProgress'; import ComposerContainer from '../composer/ComposerContainer'; +import { useFileUpload } from './hooks/useFileUpload'; +import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import RoomComposer from '../composer/RoomComposer/RoomComposer'; @@ -23,15 +29,10 @@ import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomC import { useDateScroll } from '../hooks/useDateScroll'; 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 { useFileUploadDropTarget } from './hooks/useFileUploadDropTarget'; import { useGetMore } from './hooks/useGetMore'; -import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; import { useListIsAtBottom } from './hooks/useListIsAtBottom'; -import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; import { useJumpToMessageImperative } from '../MessageList/hooks/useJumpToMessage'; @@ -116,12 +117,8 @@ const RoomBody = (): ReactElement => { surroundingMessagesJumpTpRef, ); - const { - uploads, - handleUploadFiles, - handleUploadProgressClose, - targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], - } = useFileUpload(); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.uploads); + const { uploads, isUploading } = useFileUpload(chat.uploads); const { messageListRef } = useMessageListNavigation(); const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); @@ -203,20 +200,7 @@ const RoomBody = (): ReactElement => {
- {uploads.length > 0 && ( - - {uploads.map((upload) => ( - - ))} - - )} + {isUploading && } {Boolean(unread) && ( { onMarkAsReadButtonClick={handleMarkAsReadButtonClick} /> )} - -
{ onResize={handleComposerResize} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - onUploadFiles={handleUploadFiles} onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} 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 index 850f1af094958..270f961ccdde0 100644 --- a/apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx +++ b/apps/meteor/client/views/room/body/UploadProgress/UploadProgressIndicator.tsx @@ -1,71 +1,71 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, Button, Palette } from '@rocket.chat/fuselage'; +import { Box, Bubble } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import { useCallback, useMemo } from 'react'; +import { useMemo } 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; + uploads: readonly Upload[]; }; -const UploadProgressIndicator = ({ id, name, percentage, error, onClose }: UploadProgressIndicatorProps): ReactElement | null => { +const UploadProgressIndicator = ({ uploads }: 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 { percentage, count } = useMemo(() => { + const validUploads = uploads.filter((upload) => !upload.error); + const activeUploads = validUploads.filter((upload) => upload.percentage < 100); + + if (activeUploads.length === 0) { + return { percentage: 0, count: 0 }; } - `; - const handleCloseClick = useCallback(() => { - onClose?.(id); - }, [id, onClose]); + const totalPercentage = validUploads.reduce((sum, upload) => sum + upload.percentage, 0); + const avgPercentage = Math.round(totalPercentage / validUploads.length); - const uploadProgressTitle = useMemo(() => { - if (error) { - return `${error} ${name}`; - } + return { + percentage: avgPercentage, + count: activeUploads.length, + }; + }, [uploads]); + + const customClass = useMemo( + () => css` + position: relative; + display: flex; + justify-content: center; + z-index: 3; + + & .rcx-bubble__item { + position: relative; + + > span { + z-index: 2; + } + + &::after { + content: ''; + position: absolute; + left: 0; + top: 0; + width: ${percentage}%; + height: 100%; + transition: width 0.3s ease-out; + background-color: var(--rcx-color-button-background-primary-press, #10529e); + } + } + `, + [percentage], + ); - return `[${percentage}%] ${t('Uploading_file__fileName__', { fileName: name })}`; - }, [error, name, percentage, t]); + if (count === 0) { + return null; + } return ( - - - {uploadProgressTitle} - - + + {`${percentage}% ${t('Uploading__count__file', { count })}`} ); }; diff --git a/apps/meteor/client/views/room/body/UploadProgress/index.ts b/apps/meteor/client/views/room/body/UploadProgress/index.ts index b4bfb8f16b0f1..77f3aff39ab53 100644 --- a/apps/meteor/client/views/room/body/UploadProgress/index.ts +++ b/apps/meteor/client/views/room/body/UploadProgress/index.ts @@ -1,2 +1 @@ -export { default as UploadProgressIndicator } from './UploadProgressIndicator'; -export { default as UploadProgressContainer } from './UploadProgressContainer'; +export { default } from './UploadProgressIndicator'; diff --git a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts index e9803eb98a7dd..819aafa3eee38 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUpload.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUpload.ts @@ -1,39 +1,67 @@ -import { useCallback, useEffect, useSyncExternalStore } from 'react'; +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'; -export const useFileUpload = () => { +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(store.subscribe, store.get); + const isProcessingUploads = useSyncExternalStore(store.subscribe, store.getProcessingUploads); - const uploads = useSyncExternalStore(chat.uploads.subscribe, chat.uploads.get); + const stopUploadingAction = useCallback(() => { + if (uploads.length === 1) { + chat.action.stop('uploading'); + } + }, [chat.action, uploads.length]); - const handleUploadProgressClose = useCallback( + const handleRemoveUpload = useCallback( (id: Upload['id']) => { - chat.uploads.cancel(id); + store.removeUpload(id); + stopUploadingAction(); }, - [chat], + [stopUploadingAction, store], ); + const handleCancelUpload = useCallback( + (id: Upload['id']) => { + store.cancel(id); + stopUploadingAction(); + }, + [stopUploadingAction, store], + ); + + const handleEditUpload = useCallback((id: Upload['id'], fileName: string) => store.editUploadFileName(id, fileName), [store]); + const handleUploadFiles = useCallback( (files: readonly File[]): void => { - chat.flows.uploadFiles(files); + chat?.flows.uploadFiles({ files, uploadsStore: store }); }, - [chat], + [chat, store], ); - return { - uploads, - handleUploadProgressClose, - handleUploadFiles, - targeDrop: useFileUploadDropTarget(), - }; + const isUploading = uploads.length > 0 && uploads.some((upload) => upload.percentage < 100 && !upload.error); + + return useMemo( + () => ({ + uploads, + hasUploads: uploads.length > 0, + isUploading, + isProcessingUploads, + handleRemoveUpload, + handleEditUpload, + handleCancelUpload, + handleUploadFiles, + }), + [uploads, isUploading, isProcessingUploads, handleRemoveUpload, handleEditUpload, handleCancelUpload, handleUploadFiles], + ); }; diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 888e7e055080d..8c987f3e23762 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -1,16 +1,19 @@ 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'; +import type { UploadsAPI } from '../../../../lib/chats/ChatAPI'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { useIsRoomOverMacLimit } from '../../../omnichannel/hooks/useIsRoomOverMacLimit'; 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; }, @@ -37,13 +40,18 @@ export const useFileUploadDropTarget = (): readonly [ 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 = () => { const uniqueFiles: File[] = []; - const st: Set = new Set(); + const st: Set = new Set(); files.forEach((file) => { - const key = file.size; + const key = `${file.name}-${file.size}-${file.lastModified}`; if (!st.has(key)) { uniqueFiles.push(file); st.add(key); @@ -58,7 +66,7 @@ export const useFileUploadDropTarget = (): readonly [ return file; }); - chat?.flows.uploadFiles(uploads); + chat?.flows.uploadFiles({ files: uploads, uploadsStore }); }); const allOverlayProps = useMemo(() => { @@ -70,7 +78,7 @@ export const useFileUploadDropTarget = (): readonly [ } as const; } - if (!fileUploadAllowedForUser || !subscription) { + if (!fileUploadAllowedForUser || !subscription || isEditing) { return { enabled: false, reason: t('error-not-allowed'), @@ -83,7 +91,7 @@ export const useFileUploadDropTarget = (): readonly [ 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/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index cefe16936beeb..2daa10309ef45 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -20,7 +20,6 @@ export type ComposerMessageProps = { onSend?: () => void; onNavigateToNextMessage?: () => void; onNavigateToPreviousMessage?: () => void; - onUploadFiles?: (files: readonly File[]) => void; onClickSelectAll?: () => void; }; @@ -58,6 +57,7 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, + tmid, }); if (newMessageSent) onSend?.(); } catch (error) { @@ -73,11 +73,8 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac }, onNavigateToPreviousMessage: () => chat?.messageEditing.toPreviousMessage(), onNavigateToNextMessage: () => chat?.messageEditing.toNextMessage(), - onUploadFiles: (files: readonly File[]) => { - return chat?.flows.uploadFiles(files); - }, }), - [chat?.data, chat?.flows, chat?.action, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, onSend], + [chat?.data, chat?.flows, chat?.action, chat?.composer?.text, chat?.messageEditing, dispatchToastMessage, tmid, onSend], ); const { subscribe, getSnapshotValue } = useMemo(() => { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 8ad2997ef917a..33cbfed4a19b5 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -20,6 +20,7 @@ import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; import MessageBoxHint from './MessageBoxHint'; import MessageBoxReplies from './MessageBoxReplies'; +import MessageComposerFiles from './MessageComposerFiles'; 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'; @@ -31,6 +32,7 @@ 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'; @@ -85,7 +87,6 @@ type MessageBoxProps = { onEscape?: () => void; onNavigateToPreviousMessage?: () => void; onNavigateToNextMessage?: () => void; - onUploadFiles?: (files: readonly File[]) => void; tshow?: IMessage['tshow']; previewUrls?: string[]; subscription?: ISubscription; @@ -99,7 +100,6 @@ const MessageBox = ({ onJoin, onNavigateToNextMessage, onNavigateToPreviousMessage, - onUploadFiles, onEscape, onTyping, tshow, @@ -158,9 +158,24 @@ const MessageBox = ({ chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); }); + const uploadsStore = tmid ? chat.threadUploads : chat.uploads; + const { + uploads, + hasUploads, + handleUploadFiles, + handleEditUpload, + handleRemoveUpload, + handleCancelUpload, + isUploading, + isProcessingUploads, + } = useFileUpload(uploadsStore); + const handleSendMessage = useEffectEvent(() => { + if (isUploading || isProcessingUploads) { + return; + } + const text = chat.composer?.text ?? ''; - chat.composer?.clear(); popup.clear(); onSend?.({ @@ -352,7 +367,7 @@ const MessageBox = ({ if (files.length) { event.preventDefault(); - onUploadFiles?.(files); + handleUploadFiles?.(files); } }); @@ -383,6 +398,7 @@ const MessageBox = ({ ); const shouldPopupPreview = useEnablePopupPreview(popup.filter, popup.option); + return ( <> {chat.composer?.quotedMessages && } @@ -420,21 +436,30 @@ const MessageBox = ({ unencryptedMessagesAllowed={unencryptedMessagesAllowed} isMobile={isMobile} /> - {isRecordingVideo && } + {isRecordingVideo && } - {isRecordingAudio && } + {isRecordingAudio && } + {hasUploads && ( + + )} @@ -474,10 +500,10 @@ const MessageBox = ({ )} 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 9a1ed5db7c80a..7732e8a8822e4 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; }; const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { @@ -45,6 +46,7 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + isEditing = false, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); const chatContext = useChat(); @@ -54,11 +56,12 @@ 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 || typing || isRecording); - const webdavActions = useWebdavActions(); + const fileUploadAction = useFileUploadAction(!canSend || isRecording || isEditing, uploadsStore); + const webdavActions = useWebdavActions(!canSend || isRecording || isEditing, uploadsStore); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); const timestampAction = useTimestampAction(chatContext.composer); 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 0fc1e5e7fcf31..25202a7cdbd8f 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 @@ -4,11 +4,12 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; 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); @@ -31,12 +32,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/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index 0df740e38afc6..d6fa7c90e6c3f 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 = (disabled: boolean, 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)} />); @@ -40,6 +38,7 @@ export const useWebdavActions = (): GenericMenuItemProps[] => { id: account._id, content: account.name, icon: 'cloud-plus' as const, + disabled, onClick: () => handleOpenWebdav(account), })) : []), diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileItem.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileItem.tsx new file mode 100644 index 0000000000000..2f8a42a36f875 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFileItem.tsx @@ -0,0 +1,84 @@ +import { IconButton } from '@rocket.chat/fuselage'; +import { useButtonPattern } from '@rocket.chat/fuselage-hooks'; +import { MessageComposerFile, MessageComposerFileError, MessageComposerFileLoader } from '@rocket.chat/ui-composer'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +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'; + +type MessageComposerFileItemProps = { + upload: Upload; + onRemove: (id: string) => void; + onEdit: (id: Upload['id'], fileName: string) => void; + onCancel: (id: Upload['id']) => void; + disabled: boolean; +}; + +const MessageComposerFileItem = ({ upload, onRemove, onEdit, onCancel, disabled, ...props }: MessageComposerFileItemProps) => { + const { t } = useTranslation(); + const [isActive, setIsActive] = 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 !== 100 && !upload.error; + + const handleOpenFilePreview = () => { + if (isLoading || upload.error) { + return; + } + + setModal( + { + onEdit(upload.id, name); + setModal(null); + }} + fileName={upload.file.name} + file={upload.file} + onClose={() => setModal(null)} + />, + ); + }; + + const dismissAction = isLoading ? () => onCancel(upload.id) : () => onRemove(upload.id); + const handleDismiss = usePreventPropagation(dismissAction); + const buttonProps = useButtonPattern(handleDismiss); + + const actionIcon = + isLoading && !isActive ? ( + + ) : ( + + ); + + if (upload.error) { + return ( + + ); + } + + return ( + setIsActive(false)} + onPointerEnter={() => setIsActive(true)} + onFocus={() => setIsActive(true)} + onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && setIsActive(false)} + fileTitle={upload.file.name} + fileSubtitle={`${fileSize} - ${fileExtension}`} + actionIcon={actionIcon} + aria-busy={isLoading} + disabled={disabled} + {...props} + /> + ); +}; + +export default MessageComposerFileItem; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx new file mode 100644 index 0000000000000..e48055fd078c8 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageComposerFiles.tsx @@ -0,0 +1,33 @@ +import { MessageComposerFileGroup } from '@rocket.chat/ui-composer'; +import { useTranslation } from 'react-i18next'; + +import MessageComposerFileItem from './MessageComposerFileItem'; +import type { Upload } from '../../../../lib/chats/Upload'; + +type MessageComposerFileGroupProps = { + uploads?: readonly Upload[]; + onRemove: (id: Upload['id']) => void; + onEdit: (id: Upload['id'], fileName: string) => void; + onCancel: (id: Upload['id']) => void; + disabled: boolean; +}; + +const MessageComposerFiles = ({ uploads, onRemove, onEdit, onCancel, disabled }: MessageComposerFileGroupProps) => { + const { t } = useTranslation(); + return ( + + {uploads?.map((upload) => ( + + ))} + + ); +}; + +export default MessageComposerFiles; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx index 4febe2530e26d..4422f3f0f9062 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx @@ -1,9 +1,10 @@ -import { Document, Image, Page, pdf, StyleSheet, Text, View } from '@react-pdf/renderer'; +import { Document, Font, Image, Page, pdf, StyleSheet, Text, View } from '@react-pdf/renderer'; import type { IMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings'; import { MessageTypes } from '@rocket.chat/message-types'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useSetting, useToastMessageDispatch, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; @@ -12,8 +13,28 @@ import { Messages } from '../../../../stores'; const leftTab = { marginLeft: 20, }; +const NOTO_SANS_FONTS: { name: string; fontSrc: string }[] = [ + { name: 'Noto Sans Hebrew', fontSrc: '/fonts/NotoSansHebrew-Regular.ttf' }, + { name: 'Noto Sans', fontSrc: '/fonts/NotoSans-Regular.ttf' }, + { name: 'Noto Sans Arabic', fontSrc: '/fonts/NotoSansArabic-Regular.ttf' }, + { name: 'Noto Sans Devanagari', fontSrc: '/fonts/NotoSansDevanagari-Regular.ttf' }, + { name: 'Noto Sans Bengali', fontSrc: '/fonts/NotoSansBengali-Regular.ttf' }, + { name: 'Noto Sans Tamil', fontSrc: '/fonts/NotoSansTamil-Regular.ttf' }, + { name: 'Noto Sans Sinhala', fontSrc: '/fonts/NotoSansSinhala-Regular.ttf' }, + { name: 'Noto Sans Thai', fontSrc: '/fonts/NotoSansThai-Regular.ttf' }, + { name: 'Noto Sans Lao', fontSrc: '/fonts/NotoSansLao-Regular.ttf' }, + { name: 'Noto Sans Georgian', fontSrc: '/fonts/NotoSansGeorgian-Regular.ttf' }, + { name: 'Noto Sans JP', fontSrc: '/fonts/NotoSansJP-Regular.ttf' }, + { name: 'Noto Sans KR', fontSrc: '/fonts/NotoSansKR-Regular.ttf' }, + { name: 'Noto Sans SC', fontSrc: '/fonts/NotoSansSC-Regular.ttf' }, + { name: 'Noto Sans TC', fontSrc: '/fonts/NotoSansTC-Regular.ttf' }, + { name: 'Noto Sans HK', fontSrc: '/fonts/NotoSansHK-Regular.ttf' }, +]; const pdfStyles = StyleSheet.create({ + page: { + fontFamily: NOTO_SANS_FONTS.map((font) => font.name), + }, messageHeader: { display: 'flex', flexDirection: 'row', @@ -48,6 +69,17 @@ export const useExportMessagesAsPDFMutation = () => { const chatopsUsername = useSetting('Chatops_Username'); const formatDateAndTime = useFormatDateAndTime(); const dispatchToastMessage = useToastMessageDispatch(); + const absoluteUrl = useAbsoluteUrl(); + + useEffect(() => { + for (const font of NOTO_SANS_FONTS) { + Font.register({ + family: font.name, + fonts: [{ src: absoluteUrl(font.fontSrc) }], + }); + } + Font.registerHyphenationCallback((word) => [word]); + }, []); return useMutation({ mutationFn: async (messageIds: IMessage['_id'][]) => { @@ -72,7 +104,7 @@ export const useExportMessagesAsPDFMutation = () => { const jsx = ( - + {messages.map((message) => { const dateTime = formatDateAndTime(message.ts); return ( 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 e3403f4860a22..003d88a50f610 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -20,7 +20,11 @@ type ThreadChatProps = { }; const ThreadChat = ({ mainMessage }: ThreadChatProps) => { - const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(); + const chat = useChat(); + + if (!chat) { + throw new Error('No ChatContext provided'); + } const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel'); @@ -47,7 +51,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { closeTab(); }, [closeTab]); - const chat = useChat(); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(chat.threadUploads); const handleNavigateToPreviousMessage = useCallback((): void => { chat?.messageEditing.toPreviousMessage(); @@ -57,13 +61,6 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { chat?.messageEditing.toNextMessage(); }, [chat?.messageEditing]); - const handleUploadFiles = useCallback( - (files: readonly File[]): void => { - chat?.flows.uploadFiles(files); - }, - [chat?.flows], - ); - const room = useRoom(); const readThreads = useMethod('readThreads'); useEffect(() => { @@ -115,7 +112,6 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { onEscape={handleComposerEscape} onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} - onUploadFiles={handleUploadFiles} tshow={sendToChannel} > 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 a7f01e9d5c6de..73f38043d4e18 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.spec.tsx @@ -13,17 +13,12 @@ const defaultProps = { onClose: () => undefined, file: new File([], 'testing.png', { type: 'image/png' }), fileName: 'testing.png', - fileDescription: '', onSubmit: () => undefined, - invalidContentType: false, - showDescription: true, }; const defaultWrapper = mockAppRoot().withTranslations('en', 'core', { - Cannot_upload_file_character_limit: 'Cannot upload file, description is over the {{count}} character limit', - Send: 'Send', + Update: 'Update', Upload_file_name: 'File name', - Upload_file_description: 'File description', FileUpload_MediaType_NotAccepted__type__: 'Media type not accepted: {{type}}', }); @@ -39,19 +34,6 @@ test.each(testCases)('%s should have no a11y violations', async (_storyname, Sto expect(results).toHaveNoViolations(); }); -it('should display error message when description exceeds character limit', async () => { - render(, { - wrapper: defaultWrapper.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(); -}); - it('should not send a renamed file with not allowed mime-type', async () => { render(, { wrapper: defaultWrapper.withSetting('FileUpload_MediaTypeBlackList', 'image/svg+xml').build(), @@ -60,7 +42,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/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx index 97626623e902b..7d731a4d87edb 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.stories.tsx @@ -11,8 +11,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/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 26487ff0571cb..1de7bac04dc47 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -15,11 +15,9 @@ import { ModalFooter, ModalFooterControllers, } from '@rocket.chat/fuselage'; -import { useAutoFocus, useMergedRefs } from '@rocket.chat/fuselage-hooks'; -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, useCallback, useEffect, useId } from 'react'; +import { memo, useCallback, useId } from 'react'; import { useForm } from 'react-hook-form'; import FilePreview from './FilePreview'; @@ -28,36 +26,21 @@ import { getMimeTypeFromFileName } from '../../../../../app/utils/lib/mimeTypes' 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, 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; + formState: { errors, isDirty, isSubmitting }, + } = useForm({ mode: 'onBlur', defaultValues: { name: fileName } }); const validateFileName = useCallback( (fieldValue: string) => { @@ -71,54 +54,11 @@ const FileUploadModal = ({ [t], ); - 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) }), - }); - } - - onSubmit(name, description); - }; - - 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(); - const fileDescriptionField = useId(); - const autoFocusRef = useAutoFocus(); - - const { ref, ...descriptionField } = register('description', { - validate: (value) => isDescriptionValid(value || ''), - }); - - const descriptionRef = useMergedRefs(ref, autoFocusRef); - return ( ) => ( - + (!isDirty ? onClose() : onSubmit(name)))} {...props} /> )} > @@ -152,26 +92,6 @@ const FileUploadModal = ({ )} - {showDescription && ( - - {t('Upload_file_description')} - - - - {errors.description && ( - - {errors.description.message} - - )} - - )} @@ -180,7 +100,7 @@ const FileUploadModal = ({ {t('Cancel')} 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 5dc343ef6191c..87c8b68a6d66d 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 diff --git a/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx b/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx index 51ca961b6f41c..2483519ec2608 100644 --- a/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx +++ b/apps/meteor/client/views/room/webdav/WebdavFilePickerModal/WebdavFilePickerModal.tsx @@ -3,7 +3,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Modal, Box, IconButton, Select, ModalHeader, ModalTitle, ModalClose, ModalContent, ModalFooter } from '@rocket.chat/fuselage'; import { useEffectEvent, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useSort } from '@rocket.chat/ui-client'; -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'; @@ -11,21 +11,18 @@ 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 FileUploadModal from '../../modals/FileUploadModal'; export type WebdavSortOptions = 'name' | 'size' | 'dataModified'; type WebdavFilePickerModalProps = { - onUpload: (file: File, description?: string) => Promise; + onUpload: (file: File) => Promise; onClose: () => void; account: IWebdavAccountIntegration; }; 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'); @@ -131,9 +128,9 @@ const WebdavFilePickerModal = ({ onUpload, onClose, account }: WebdavFilePickerM const handleUpload = async (webdavNode: IWebdavNode): Promise => { setIsLoading(true); - const uploadFile = async (file: File, description?: string): Promise => { + const uploadFile = async (file: File): Promise => { try { - await onUpload?.(file, description); + await onUpload?.(file); } catch (error) { return dispatchToastMessage({ type: 'error', message: error }); } finally { @@ -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))} - />, - ); + await uploadFile(file); } catch (error) { return dispatchToastMessage({ type: 'error', message: error }); } 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; diff --git a/apps/meteor/licenses/NotoSans-OFL.txt b/apps/meteor/licenses/NotoSans-OFL.txt new file mode 100644 index 0000000000000..6843f31878c99 --- /dev/null +++ b/apps/meteor/licenses/NotoSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansArabic-OFL.txt b/apps/meteor/licenses/NotoSansArabic-OFL.txt new file mode 100644 index 0000000000000..14c589f638450 --- /dev/null +++ b/apps/meteor/licenses/NotoSansArabic-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansBengali-OFL.txt b/apps/meteor/licenses/NotoSansBengali-OFL.txt new file mode 100644 index 0000000000000..d11cac69316ec --- /dev/null +++ b/apps/meteor/licenses/NotoSansBengali-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/bengali) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansDevanagari-OFL.txt b/apps/meteor/licenses/NotoSansDevanagari-OFL.txt new file mode 100644 index 0000000000000..cd2cc5c94b415 --- /dev/null +++ b/apps/meteor/licenses/NotoSansDevanagari-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansGeorgian-OFL.txt b/apps/meteor/licenses/NotoSansGeorgian-OFL.txt new file mode 100644 index 0000000000000..437070a9ee723 --- /dev/null +++ b/apps/meteor/licenses/NotoSansGeorgian-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/georgian) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansHK-OFL.txt b/apps/meteor/licenses/NotoSansHK-OFL.txt new file mode 100644 index 0000000000000..1c9f43281b8f2 --- /dev/null +++ b/apps/meteor/licenses/NotoSansHK-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansHebrew-OFL.txt b/apps/meteor/licenses/NotoSansHebrew-OFL.txt new file mode 100644 index 0000000000000..56662b6243717 --- /dev/null +++ b/apps/meteor/licenses/NotoSansHebrew-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/hebrew) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansJP-OFL.txt b/apps/meteor/licenses/NotoSansJP-OFL.txt new file mode 100644 index 0000000000000..1c9f43281b8f2 --- /dev/null +++ b/apps/meteor/licenses/NotoSansJP-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansKR-OFL.txt b/apps/meteor/licenses/NotoSansKR-OFL.txt new file mode 100644 index 0000000000000..1c9f43281b8f2 --- /dev/null +++ b/apps/meteor/licenses/NotoSansKR-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansLao-OFL.txt b/apps/meteor/licenses/NotoSansLao-OFL.txt new file mode 100644 index 0000000000000..483c04d1b6a1c --- /dev/null +++ b/apps/meteor/licenses/NotoSansLao-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/lao) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansSC-OFL.txt b/apps/meteor/licenses/NotoSansSC-OFL.txt new file mode 100644 index 0000000000000..1c9f43281b8f2 --- /dev/null +++ b/apps/meteor/licenses/NotoSansSC-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansSinhala-OFL.txt b/apps/meteor/licenses/NotoSansSinhala-OFL.txt new file mode 100644 index 0000000000000..4e06fdff46ede --- /dev/null +++ b/apps/meteor/licenses/NotoSansSinhala-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/sinhala) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansTC-OFL.txt b/apps/meteor/licenses/NotoSansTC-OFL.txt new file mode 100644 index 0000000000000..1c9f43281b8f2 --- /dev/null +++ b/apps/meteor/licenses/NotoSansTC-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansTamil-OFL.txt b/apps/meteor/licenses/NotoSansTamil-OFL.txt new file mode 100644 index 0000000000000..677d559b94ba7 --- /dev/null +++ b/apps/meteor/licenses/NotoSansTamil-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/tamil) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/NotoSansThai-OFL.txt b/apps/meteor/licenses/NotoSansThai-OFL.txt new file mode 100644 index 0000000000000..7fa8dcb08ce4d --- /dev/null +++ b/apps/meteor/licenses/NotoSansThai-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/thai) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/meteor/licenses/THIRD-PARTY-LICENSES.md b/apps/meteor/licenses/THIRD-PARTY-LICENSES.md new file mode 100644 index 0000000000000..b17499b50c2ca --- /dev/null +++ b/apps/meteor/licenses/THIRD-PARTY-LICENSES.md @@ -0,0 +1,36 @@ +# Third-Party Licenses + +This document contains the licenses for third-party software and assets included in or distributed with this project. + +## Fonts + +### Noto Sans Fonts + +- **Project**: Noto Sans (variable fonts, multiple scripts) +- **Source**: https://github.com/google/fonts/tree/main/ofl +- **License**: SIL Open Font License 1.1 +- **Copyright**: The Noto Project Authors; Adobe (Noto Sans JP, KR, SC, TC, HK) + +| Font | Copyright | License file | +|------|-----------|--------------| +| Noto Sans | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) | [NotoSans-OFL.txt](./NotoSans-OFL.txt) | +| Noto Sans Arabic | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic) | [NotoSansArabic-OFL.txt](./NotoSansArabic-OFL.txt) | +| Noto Sans Bengali | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/bengali) | [NotoSansBengali-OFL.txt](./NotoSansBengali-OFL.txt) | +| Noto Sans Devanagari | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari) | [NotoSansDevanagari-OFL.txt](./NotoSansDevanagari-OFL.txt) | +| Noto Sans Georgian | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/georgian) | [NotoSansGeorgian-OFL.txt](./NotoSansGeorgian-OFL.txt) | +| Noto Sans Hebrew | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/hebrew) | [NotoSansHebrew-OFL.txt](./NotoSansHebrew-OFL.txt) | +| Noto Sans HK | Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' | [NotoSansHK-OFL.txt](./NotoSansHK-OFL.txt) | +| Noto Sans JP | Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' | [NotoSansJP-OFL.txt](./NotoSansJP-OFL.txt) | +| Noto Sans KR | Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' | [NotoSansKR-OFL.txt](./NotoSansKR-OFL.txt) | +| Noto Sans Lao | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/lao) | [NotoSansLao-OFL.txt](./NotoSansLao-OFL.txt) | +| Noto Sans SC | Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' | [NotoSansSC-OFL.txt](./NotoSansSC-OFL.txt) | +| Noto Sans Sinhala | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/sinhala) | [NotoSansSinhala-OFL.txt](./NotoSansSinhala-OFL.txt) | +| Noto Sans Tamil | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/tamil) | [NotoSansTamil-OFL.txt](./NotoSansTamil-OFL.txt) | +| Noto Sans TC | Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' | [NotoSansTC-OFL.txt](./NotoSansTC-OFL.txt) | +| Noto Sans Thai | Copyright 2022 The Noto Project Authors (https://github.com/notofonts/thai) | [NotoSansThai-OFL.txt](./NotoSansThai-OFL.txt) | + +The Noto Sans font files are used for PDF export and are licensed under the SIL Open Font License 1.1. The full license text for each variant can be found in the `licenses` folder (e.g. `NotoSans-OFL.txt`, `NotoSansHebrew-OFL.txt`) and at https://scripts.sil.org/OFL + +#### License Summary + +The SIL Open Font License allows the fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. diff --git a/apps/meteor/package.json b/apps/meteor/package.json index e0181a8d66ca1..e689325b89394 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -87,7 +87,7 @@ "@opentelemetry/sdk-node": "^0.54.2", "@parse/node-apn": "^7.0.1", "@react-aria/toolbar": "^3.0.0-nightly.5042", - "@react-pdf/renderer": "^3.4.5", + "@react-pdf/renderer": "^4.3.2", "@rocket.chat/abac": "workspace:^", "@rocket.chat/account-utils": "workspace:^", "@rocket.chat/agenda": "workspace:^", @@ -305,6 +305,7 @@ "xml-encryption": "~3.1.0", "xml2js": "~0.6.2", "yaqrcode": "^0.2.1", + "yoga-layout": "patch:yoga-layout@npm%3A3.2.1#~/.yarn/patches/yoga-layout-npm-3.2.1-51ec934670.patch", "zod": "~4.3.6", "zustand": "~5.0.10" }, diff --git a/apps/meteor/public/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf b/apps/meteor/public/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000000000..6245ba014b1f2 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf differ diff --git a/apps/meteor/public/fonts/NotoSans-Regular.ttf b/apps/meteor/public/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000000..7da1a0fad8c71 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSans-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansArabic-Regular.ttf b/apps/meteor/public/fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 0000000000000..b3b82dcc6e513 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansArabic-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansBengali-Regular.ttf b/apps/meteor/public/fonts/NotoSansBengali-Regular.ttf new file mode 100644 index 0000000000000..3bc7589601e3e Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansBengali-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansDevanagari-Regular.ttf b/apps/meteor/public/fonts/NotoSansDevanagari-Regular.ttf new file mode 100644 index 0000000000000..07480c02e0864 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansDevanagari-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansGeorgian-Regular.ttf b/apps/meteor/public/fonts/NotoSansGeorgian-Regular.ttf new file mode 100644 index 0000000000000..8f3e75d2154cc Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansGeorgian-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansHK-Regular.ttf b/apps/meteor/public/fonts/NotoSansHK-Regular.ttf new file mode 100644 index 0000000000000..3405514dd79cc Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansHK-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansHebrew-Regular.ttf b/apps/meteor/public/fonts/NotoSansHebrew-Regular.ttf new file mode 100644 index 0000000000000..f7956338f8a40 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansHebrew-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansJP-Regular.ttf b/apps/meteor/public/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 0000000000000..6c730cd848ecc Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansJP-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansKR-Regular.ttf b/apps/meteor/public/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 0000000000000..36e634d21df4e Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansKR-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansLao-Regular.ttf b/apps/meteor/public/fonts/NotoSansLao-Regular.ttf new file mode 100644 index 0000000000000..7485ca10b235b Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansLao-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansSC-Regular.ttf b/apps/meteor/public/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000000000..6043afcf4cbfd Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansSC-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansSinhala-Regular.ttf b/apps/meteor/public/fonts/NotoSansSinhala-Regular.ttf new file mode 100644 index 0000000000000..ee139658ff0c8 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansSinhala-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansTC-Regular.ttf b/apps/meteor/public/fonts/NotoSansTC-Regular.ttf new file mode 100644 index 0000000000000..2defdb937df66 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansTC-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansTamil-Regular.ttf b/apps/meteor/public/fonts/NotoSansTamil-Regular.ttf new file mode 100644 index 0000000000000..0400d8f49bee0 Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansTamil-Regular.ttf differ diff --git a/apps/meteor/public/fonts/NotoSansThai-Regular.ttf b/apps/meteor/public/fonts/NotoSansThai-Regular.ttf new file mode 100644 index 0000000000000..f17b1341f1cda Binary files /dev/null and b/apps/meteor/public/fonts/NotoSansThai-Regular.ttf differ 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 330b306cb4a86..7d522e47c3c2e 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 @@ -95,14 +95,18 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { await test.step('upload the file with encryption', async () => { // Upload a file - await encryptedRoomPage.dragAndDropTxtFile(); + await encryptedRoomPage.sendFileMessage('any_file.txt'); + + // Update file name and send + await encryptedRoomPage.composer.getFileByName('any_file.txt').click(); await fileUploadModal.setName(fileName); - await fileUploadModal.setDescription(fileDescription); - await fileUploadModal.send(); + await fileUploadModal.update(); + await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible(); - // Check the file upload + await encryptedRoomPage.composer.inputMessage.fill(fileDescription); + await encryptedRoomPage.composer.btnSend.click(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); - await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); @@ -113,12 +117,19 @@ test.describe('E2EE Encryption and Decryption - 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.composer.getFileByName('any_file.txt').click(); + await fileUploadModal.setName(fileName); + await fileUploadModal.update(); + await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible(); + }).toPass(); + + await encryptedRoomPage.composer.inputMessage.fill(fileDescription); + await encryptedRoomPage.composer.btnSend.click(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); - await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); @@ -144,7 +155,7 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { await expect(encryptedRoomPage.lastNthMessage(1).encryptedIcon).toBeVisible(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); - await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); 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 da41810380978..9ab5127448a05 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 @@ -14,6 +14,8 @@ const settingsList = [ const originalSettings = preserveSettings(settingsList); +const TEST_FILE_TXT = 'any_file.txt'; + test.use({ storageState: Users.userE2EE.state }); test.describe('E2EE File Encryption', () => { @@ -54,16 +56,21 @@ test.describe('E2EE File Encryption', () => { expect((await api.post('/groups.delete', { roomId: encryptedRoomId })).status()).toBe(200); }); - test('File and description encryption and editing the description', async ({ page }) => { - await test.step('send a file in channel', 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(); + test('should edit encrypted message with file', async ({ page }) => { + const updatedFileName = `edited_${TEST_FILE_TXT}`; + 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 expect(poHomeChannel.composer.getFileByName(updatedFileName)).toBeVisible(); + + await poHomeChannel.composer.inputMessage.fill('any_description'); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName); }); await test.step('edit the description', async () => { @@ -76,28 +83,31 @@ test.describe('E2EE File Encryption', () => { await page.keyboard.press('Enter'); await expect(poHomeChannel.content.getFileDescription).toHaveText('edited any_description'); + await expect(poHomeChannel.content.lastUserMessage.getByRole('link').getByText(updatedFileName)).toBeVisible(); }); await test.step('delete the file from files list', async () => { await poHomeChannel.roomToolbar.openMoreOptions(); await poHomeChannel.roomToolbar.menuItemFiles.click(); - await poHomeChannel.tabs.files.deleteFile('any_file1.txt'); + await poHomeChannel.tabs.files.deleteFile(updatedFileName); - await expect(poHomeChannel.tabs.files.getFileByName('any_file1.txt')).toHaveCount(0); - await expect(poHomeChannel.content.lastUserMessage).not.toBeVisible(); + await expect(poHomeChannel.tabs.files.getFileByName(updatedFileName)).toHaveCount(0); + await expect(poHomeChannel.content.lastUserMessage.getByRole('link').getByText(updatedFileName)).not.toBeVisible(); }); }); test('File encryption with whitelisted and blacklisted media types', async ({ api }) => { await test.step('send a text file in channel', async () => { - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('message 1'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.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(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('message 1'); - await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); + await expect(poHomeChannel.content.getFileDescription).not.toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName); }); await test.step('set whitelisted media type setting', async () => { @@ -105,10 +115,12 @@ test.describe('E2EE File Encryption', () => { }); await test.step('send text file again with whitelist setting set', 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 poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.composer.inputMessage.fill('message 2'); + await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click(); + await poHomeChannel.content.inputFileUploadName.fill('any_file2.txt'); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2'); @@ -121,13 +133,10 @@ 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 poHomeChannel.content.descriptionInput.fill('message 3'); - await poHomeChannel.content.fileNameInput.fill('any_file3.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + const composerFiles = await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).all(); + 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.getLastMessageByFileName('any_file2.txt')).toBeVisible(); }); }); @@ -152,13 +161,15 @@ test.describe('E2EE File Encryption', () => { await test.step('send a text file in channel, file should not be encrypted', 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 poHomeChannel.composer.inputMessage.fill('any_description'); + await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click(); + await poHomeChannel.content.inputFileUploadName.fill('any_file1.txt'); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.composer.btnSend.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.getLastMessageByFileName('any_file1.txt')).toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toContainText('any_file1.txt'); }); }); }); diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index a7a77ec903726..f864b6077c9ab 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,55 +33,177 @@ test.describe.serial('file-upload', () => { expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); - test('should successfully cancel upload', async () => { + test('should cancel uploaded file attached to message composer', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.btnModalCancel.click(); - await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT); + + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); }); - test('should not display modal when clicking in send file', async () => { - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.content.modalFilePreview).not.toBeVisible(); + test('should send file with name updated', async () => { + 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(TEST_FILE_TXT).click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + + await expect(poHomeChannel.composer.getFileByName(updatedFileName)).toBeVisible(); + await poHomeChannel.composer.btnSend.click(); + }); + + await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName); }); - test('should send file with name/description updated', async () => { - await poHomeChannel.content.dragAndDropTxtFile(); - await expect(poHomeChannel.content.descriptionInput).toBeFocused(); + 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); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)).toBeVisible(); + + await poHomeChannel.composer.btnSend.click(); + await expect(poHomeChannel.content.lastUserMessageDownloadLink).toHaveCount(1); + }); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + 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(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); + 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.content.descriptionInput.fill('lst_description'); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.composer.inputMessage.fill('lst_description'); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description'); - await expect(poHomeChannel.content.getLastMessageByFileName('lst-test.lst')).toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_LST)).toBeVisible(); }); test('should send drawio (unknown media type) file successfully', async ({ page }) => { 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(TEST_FILE_DRAWIO); + await poHomeChannel.composer.inputMessage.fill('drawio_description'); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description'); - await expect(poHomeChannel.content.getLastMessageByFileName('diagram.drawio')).toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_DRAWIO)).toBeVisible(); }); test('should not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { 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(TEST_FILE_DRAWIO, { waitForResponse: false }); + + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_DRAWIO)).toHaveAttribute('readonly'); + }); + + test('should be able to remove file from composer before sending', async () => { + await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessage(TEST_FILE_LST); + + await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT); + + 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(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 () => { + const files = new Array(10).fill('number1.png'); + + await Promise.all(files.map((file) => poHomeChannel.content.sendFileMessage(file))); + await poHomeChannel.content.dragAndDropTxtFile(); + + await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10); + await expect(poHomeChannel.composer.getFileByName('any_file.txt')).not.toBeVisible(); + }); + + 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(TEST_FILE_TXT); + await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_LST); + + await poHomeChannel.threadComposer.removeFileByName(TEST_FILE_LST); + + await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_TXT)).toBeVisible(); + await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_LST)).not.toBeVisible(); + }); + }); + + test.describe.serial('file upload fails', () => { + let fileUploadWarningModal: FileUploadWarningModal; + + 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 ({ page }) => { + fileUploadWarningModal = new FileUploadWarningModal(page.getByRole('dialog', { name: 'Warning' })); + + await poHomeChannel.content.sendFileMessage(TEST_EMPTY_FILE, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO, { waitForResponse: false }); + + 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(); + 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 ({ 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(TEST_FILE_TXT, { waitForResponse: false }); + await poHomeChannel.content.sendFileMessage(TEST_EMPTY_FILE, { waitForResponse: false }); + + 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(); + + await expect(fileUploadWarningModal.getContent('One file failed to upload')).toBeVisible(); + }); + + await test.step('should close modal when clicking "Cancel" button', async () => { + await fileUploadWarningModal.cancel(); + + 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(); + + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).toContainText(TEST_FILE_TXT); + await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).not.toBeVisible(); + }); + }); }); }); @@ -100,6 +228,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.modalFilePreview).not.toBeVisible(); + await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/files-management.spec.ts b/apps/meteor/tests/e2e/files-management.spec.ts index 2f193e614437d..55d9e41dcb172 100644 --- a/apps/meteor/tests/e2e/files-management.spec.ts +++ b/apps/meteor/tests/e2e/files-management.spec.ts @@ -28,7 +28,7 @@ test.describe.serial('files-management', () => { test('should send a file and manage it in the list', async () => { await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).toBeVisible(); await poHomeChannel.roomToolbar.openMoreOptions(); 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/fixtures/files/empty_file.txt b/apps/meteor/tests/e2e/fixtures/files/empty_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d 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..ed0c6fe9ca3e3 --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/responses/mediaResponse.ts @@ -0,0 +1,8 @@ +import type { Page, Response } from '@playwright/test'; + +const isMediaResponse = (response: Response) => + /api\/v1\/rooms\.media(?:\/|\?|$)/.test(response.url()) && response.request().method() === 'POST'; + +export const createMediaResponsePromise = (page: Page) => { + return page.waitForResponse((response) => isMediaResponse(response)); +}; diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index b4afdcbe19054..9dc2a4368d62f 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -38,17 +38,19 @@ test.describe.serial('Image Gallery', async () => { test.describe('When sending an image as a file', () => { test.beforeAll(async () => { + const largeFileName = 'test-large-image.jpeg'; + await poHomeChannel.navbar.openChat(targetChannel); for await (const imageName of imageNames) { await poHomeChannel.content.sendFileMessage(imageName); - await poHomeChannel.content.btnModalConfirm.click(); + await poHomeChannel.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(imageName); } await poHomeChannel.navbar.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.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 77f8dc7c8dffe..3e19f8ae945db 100644 --- a/apps/meteor/tests/e2e/image-upload.spec.ts +++ b/apps/meteor/tests/e2e/image-upload.spec.ts @@ -36,11 +36,8 @@ 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.btnModalConfirm.click(); - await expect(poHomeChannel.statusUploadError).toBeVisible(); + await expect(poHomeChannel.composer.getFileByName('bad-orientation.jpeg')).toHaveAttribute('readonly'); }); }); @@ -52,12 +49,10 @@ 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.btnModalConfirm.click(); - - await expect(poHomeChannel.content.getFileDescription).toHaveText('bad-orientation_description'); + const imgName = 'bad-orientation.jpeg'; + await poHomeChannel.content.sendFileMessage(imgName); + 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 d39a3ea33c808..3a1e71733ca02 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -225,7 +225,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.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -237,7 +237,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.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -249,7 +249,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.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -261,7 +261,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.composer.btnSend.click(); await expect(poHomeChannel.content.lastUserMessage).toContainText(filename); await poHomeChannel.content.forwardMessage(forwardChannel); @@ -273,7 +273,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.composer.btnSend.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 8b203d4d93fcf..5816ec4783bfa 100644 --- a/apps/meteor/tests/e2e/message-composer.spec.ts +++ b/apps/meteor/tests/e2e/message-composer.spec.ts @@ -189,14 +189,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.navbar.openChat(targetChannel); await poHomeChannel.composer.btnAudioMessage.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.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 e3ecc115e52b1..675276e3cd98c 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts @@ -29,6 +29,22 @@ export abstract class Composer { return this.toolbarPrimaryActions.getByRole('button', { name: 'Audio message' }); } + getFileByName(fileName: string): Locator { + return this.root.getByRole('group', { name: fileName, exact: true }); + } + + get groupUploads() { + return this.root.getByRole('group', { name: 'Uploads', exact: true }); + } + + getFilesInComposer(): Locator { + return this.groupUploads.getByRole('group', { name: /^(?!Close$)/ }); + } + + async removeFileByName(fileName: string): Promise { + return this.getFileByName(fileName).getByRole('button', { name: 'Remove', exact: true }).click(); + } + 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 0b0831603651d..acb2f29d94d12 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -4,6 +4,7 @@ import { resolve, join, relative } from 'node:path'; import type { Locator, Page } from '@playwright/test'; import { RoomComposer, ThreadComposer } from './composer'; +import { createMediaResponsePromise } from '../../fixtures/responses/mediaResponse'; import { expect } from '../../utils/test'; const FIXTURES_PATH = relative(process.cwd(), resolve(__dirname, '../../fixtures/files')); @@ -15,7 +16,7 @@ export function getFilePath(fileName: string): string { export class HomeContent { protected readonly page: Page; - protected readonly composer: RoomComposer; + readonly composer: RoomComposer; protected readonly threadComposer: ThreadComposer; @@ -73,6 +74,10 @@ export class HomeContent { return this.page.getByRole('listitem').locator('[role="link"][aria-roledescription="thread message preview"]').last(); } + get lastUserMessageDownloadLink(): Locator { + return this.lastUserMessage.getByRole('link', { name: 'Download' }); + } + nthMessage(index: number): Locator { return this.messageListItems.nth(index); } @@ -158,7 +163,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' }); } @@ -174,12 +179,6 @@ export class HomeContent { return this.createDiscussionModal.getByRole('button', { name: 'Create' }); } - 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'); } @@ -192,16 +191,20 @@ 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.lastUserMessage.locator('[role="document"][aria-roledescription="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' }); } // ----------------------------------------- @@ -374,7 +377,7 @@ export class HomeContent { await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); } - async dragAndDropLstFile(): Promise { + async dragAndDropLstFile({ waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise { const contract = await fs.readFile(getFilePath('lst-test.lst'), 'utf-8'); const dataTransfer = await this.page.evaluateHandle((contract) => { const data = new DataTransfer(); @@ -385,12 +388,15 @@ export class HomeContent { return data; }, contract); + const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null; await this.composer.inputMessage.dispatchEvent('dragenter', { dataTransfer }); - await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); + if (responsePromise) { + await responsePromise; + } } - async dragAndDropTxtFileToThread(): Promise { + async dragAndDropTxtFileToThread({ waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise { const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8'); const dataTransfer = await this.page.evaluateHandle((contract) => { const data = new DataTransfer(); @@ -401,13 +407,29 @@ export class HomeContent { return data; }, contract); + const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null; await this.threadComposer.inputMessage.dispatchEvent('dragenter', { dataTransfer }); - await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); + if (responsePromise) { + await responsePromise; + } } - async sendFileMessage(fileName: string): Promise { - await this.page.locator('input[type=file]').setInputFiles(getFilePath(fileName)); + async sendFileMessage(fileName: string, { waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise { + const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null; + await this.page.getByLabel('Room composer').locator('input[type=file]').setInputFiles(getFilePath(fileName)); + if (responsePromise) { + await responsePromise; + } + } + + async sendFileMessageToThread(fileName: string, { waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise { + await this.threadComposer.inputMessage.click(); + const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null; + await this.page.getByLabel('Thread composer').locator('input[type=file]').setInputFiles(getFilePath(fileName)); + if (responsePromise) { + await responsePromise; + } } async openLastMessageMenu(): Promise { 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 92f2915827efa..7ea0d658d6488 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'; @@ -7,28 +7,52 @@ export class FileUploadModal extends Modal { super(page.getByRole('dialog', { name: 'File Upload' })); } - private get fileNameInput() { + private get inputFileName() { return this.root.getByRole('textbox', { name: 'File name' }); } - private get fileDescriptionInput() { - return this.root.getByRole('textbox', { name: 'File description' }); + private get updateButton() { + return this.root.getByRole('button', { name: 'Update' }); } - private get sendButton() { - return this.root.getByRole('button', { name: 'Send' }); + setName(fileName: string) { + return this.inputFileName.fill(fileName); } - setName(fileName: string) { - return this.fileNameInput.fill(fileName); + async update() { + await this.updateButton.click(); + await this.waitForDismissal(); + } +} + +export class FileUploadWarningModal extends Modal { + constructor(root: Locator) { + super(root); + } + + get btnOk() { + return this.root.getByRole('button', { name: 'Ok' }); } - setDescription(description: string) { - return this.fileDescriptionInput.fill(description); + 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 send() { - await this.sendButton.click(); + async confirmSend() { + await this.btnSendAnyway.click(); await this.waitForDismissal(); } } 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' }); } diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index 0845ebcdf2911..ea668588453a9 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -42,8 +42,8 @@ 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 expect(content.composer.getFileByName('any_file.txt')).toBeVisible(); + await poHomeChannel.composer.btnSend.click(); await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); await sendTargetChannelMessage(api, targetChannel.fname as string, { @@ -110,8 +110,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 poHomeChannel.composer.btnSend.click(); await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); await test.step('prune files only', async () => { @@ -145,8 +144,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 poHomeChannel.composer.btnSend.click(); await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); await content.openReplyInThread(); diff --git a/apps/meteor/tests/e2e/quote-attachment.spec.ts b/apps/meteor/tests/e2e/quote-attachment.spec.ts index 86f7c639205ba..2630e2bcbe3d5 100644 --- a/apps/meteor/tests/e2e/quote-attachment.spec.ts +++ b/apps/meteor/tests/e2e/quote-attachment.spec.ts @@ -30,9 +30,8 @@ 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.btnModalConfirm.click(); + await poHomeChannel.composer.inputMessage.fill(fileDescription); + await poHomeChannel.composer.btnSend.click(); // Wait for the file to be uploaded and message to be sent await expect(poHomeChannel.content.lastUserMessage).toBeVisible(); @@ -58,7 +57,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'); @@ -71,9 +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.btnModalConfirm.click(); + await poHomeChannel.content.sendMessageInThread(fileDescription); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText(fileDescription); await expect(poHomeChannel.content.getLastThreadMessageByFileName(textFileName)).toBeVisible(); diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index 2ceb6e37f3c71..fca7f74d917a5 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -78,18 +78,21 @@ test.describe.serial('Threads', () => { await expect(poHomeChannel.content.lastUserThreadMessage).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.threadComposer.getFileByName('any_file.txt').click(); + await poHomeChannel.content.inputFileUploadName.fill(updatedFileName); + await poHomeChannel.content.btnUpdateFileUpload.click(); + await poHomeChannel.threadComposer.inputMessage.fill('any_description'); + await poHomeChannel.threadComposer.btnSend.click(); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.getLastThreadMessageByFileName('any_file1.txt')).toBeVisible(); + await expect(poHomeChannel.content.getLastThreadMessageByFileName(updatedFileName)).toBeVisible(); }); test.describe('thread message actions', () => { diff --git a/package.json b/package.json index 4d066cabbb3f0..ff9c9b65ba9e0 100644 --- a/package.json +++ b/package.json @@ -35,19 +35,6 @@ "testunit": "turbo run testunit" }, "resolutions": { - "@react-pdf/fns": "2.0.1", - "@react-pdf/font": "2.3.7", - "@react-pdf/image": "2.2.2", - "@react-pdf/layout": "3.6.3", - "@react-pdf/pdfkit": "3.0.2", - "@react-pdf/png-js": "2.2.0", - "@react-pdf/primitives": "3.0.1", - "@react-pdf/render": "3.2.7", - "@react-pdf/renderer": "3.1.14", - "@react-pdf/stylesheet": "4.1.8", - "@react-pdf/textkit": "4.2.0", - "@react-pdf/types": "2.3.4", - "@react-pdf/yoga": "4.1.2", "@sematext/gc-stats@npm:^1.5.9": "patch:@sematext/gc-stats@npm%3A1.5.9#~/.yarn/patches/@sematext-gc-stats-npm-1.5.9-01e77be4d0.patch", "adm-zip": "0.5.9", "cross-spawn": "7.0.6", @@ -55,7 +42,9 @@ "lodash": "4.17.21", "minimist": "1.2.6", "mongodb": "6.10.0", - "underscore": "1.13.7" + "underscore": "1.13.7", + "yoga-layout@npm:^3.2.1": "patch:yoga-layout@npm%3A3.2.1#~/.yarn/patches/yoga-layout-npm-3.2.1-51ec934670.patch", + "@react-pdf/layout@npm:^4.4.2": "patch:@react-pdf/layout@npm%3A4.4.2#~/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch" }, "dependencies": { "@types/stream-buffers": "^3.0.8", diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 660a498badb74..ee8f11c511b42 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -74,4 +74,6 @@ export interface IE2EEUpload extends IUpload { content: EncryptedContent; } +export type IUploadToConfirm = Pick; + export const isE2EEUpload = (upload: IUpload): upload is IE2EEUpload => Boolean(upload?.content?.ciphertext && upload?.content?.algorithm); diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index f3c6cfa28c522..95b2ec5254b5e 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -2175,7 +2175,6 @@ "Updated_at": "Opgedateer op", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Laai mappad op", - "Upload_file_description": "Lêer beskrywing", "Upload_file_name": "Lêernaam", "Upload_file_question": "Laai leêr op?", "Upload_user_avatar": "Laai avatar op", diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index b03d597651d0a..a9ec900089a61 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -3808,7 +3808,6 @@ "Upload_Folder_Path": "تحميل مسار المجلد", "Upload_From": "تحميل من {{name}}", "Upload_app": "تحميل التطبيق", - "Upload_file_description": "وصف الملف", "Upload_file_name": "اسم الملف", "Upload_file_question": "تحميل الملف؟", "Upload_user_avatar": "تحميل الصورة الرمزية", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index ebd3b2252cf87..790aaacd1f6a8 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -2177,7 +2177,6 @@ "Updated_at": "Yenilənib", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Qovluq yolunu yükləyin", - "Upload_file_description": "Fayl təsviri", "Upload_file_name": "Fayl adı", "Upload_file_question": "Fayl yükləməyiniz?", "Upload_user_avatar": "Avatar yüklə", diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index a392d811166b7..421f45fb9c492 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -2196,7 +2196,6 @@ "Updated_at": "абноўлена", "UpgradeToGetMore_engagement-dashboard_Title": "аналітыка", "Upload_Folder_Path": "Загрузіць шлях да тэчцы", - "Upload_file_description": "апісанне файла", "Upload_file_name": "Імя файла", "Upload_file_question": "Загрузіць файл?", "Upload_user_avatar": "загрузіць аватар", diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index fb3a9857d9f5d..84720df14e7bd 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -2172,7 +2172,6 @@ "Updated_at": "Актуализиран на", "UpgradeToGetMore_engagement-dashboard_Title": "анализ", "Upload_Folder_Path": "Качване на пътя на папките", - "Upload_file_description": "Описание на файла", "Upload_file_name": "Име на файл", "Upload_file_question": "Качи фаил?", "Upload_user_avatar": "Качване на аватар", diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index db84696611cf8..616b825bfba9d 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -2169,7 +2169,6 @@ "Updated_at": "Ažurirano u", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "Upload_Folder_Path": "Prijenos puta mape", - "Upload_file_description": "Opis fajla", "Upload_file_name": "Ime fajla", "Upload_file_question": "Prenesi datoteku?", "Upload_user_avatar": "Učitaj avatar", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 157c26d70204b..bcf860d888ee4 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -3727,7 +3727,6 @@ "Upload_Folder_Path": "Carregar ruta de la carpeta", "Upload_From": "Pujar des de {{name}}", "Upload_app": "Pujar l'Aplicació", - "Upload_file_description": "Descripció de l'arxiu", "Upload_file_name": "Nom de l'arxiu", "Upload_file_question": "Pujar l'arxiu?", "Upload_user_avatar": "Carregar l'avatar", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 565d2e6ce1cca..12a1f107c94f8 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -3157,7 +3157,6 @@ "Upload_Folder_Path": "Cesta složky pro nahrávání souborů", "Upload_From": "Nahrát z {{name}}", "Upload_app": "Nahrát aplikaci", - "Upload_file_description": "Popis souboru", "Upload_file_name": "Název souboru", "Upload_file_question": "Nahrát soubor?", "Upload_user_avatar": "Nahrát avatara", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index 626e01f117d27..6d38576daade3 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -2170,7 +2170,6 @@ "Updated_at": "Wedi'i ddiweddaru yn", "UpgradeToGetMore_engagement-dashboard_Title": "Dadansoddiadau", "Upload_Folder_Path": "Llwytho Llwybr Ffolder", - "Upload_file_description": "Disgrifiad o'r ffeil", "Upload_file_name": "Ffeil enw", "Upload_file_question": "Llwytho ffeil?", "Upload_user_avatar": "Upload avatar", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 33be9191592b4..48b243eba896e 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -3251,7 +3251,6 @@ "Upload_Folder_Path": "Upload mappepath", "Upload_From": "Upload fra {{name}}", "Upload_app": "Upload-app", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Upload fil?", "Upload_user_avatar": "Upload avatar", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index a90b74a576025..6a6d986f4ded2 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -2176,7 +2176,6 @@ "Updated_at": "Aktualisiert am", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Ordnerpfad hochladen", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Möchten Sie eine Datei hochladen?", "Upload_user_avatar": "Hochladen von Avataren", diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index b110b2ec4e8bc..79f6cdc9fc520 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -2449,7 +2449,6 @@ "Upload_Folder_Path": "Pfad des Uploads", "Upload_From": "Upload von {{name}}", "Upload_app": "App hochladen", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Datei hochladen?", "Upload_user_avatar": "Avatar hochladen", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 4d916bf4000c1..a89c297ee4874 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -4221,7 +4221,6 @@ "Upload_Folder_Path": "Pfad des Uploads", "Upload_From": "Upload von {{name}}", "Upload_app": "App hochladen", - "Upload_file_description": "Dateibeschreibung", "Upload_file_name": "Dateiname", "Upload_file_question": "Datei hochladen?", "Upload_user_avatar": "Avatar hochladen", diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index 59cdad3007b2d..29e4ca9225c21 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -2178,7 +2178,6 @@ "Updated_at": "Ενημερώθηκε στο", "UpgradeToGetMore_engagement-dashboard_Title": "Αναλυτικά στοιχεία", "Upload_Folder_Path": "Μεταφόρτωση διαδρομής φακέλου", - "Upload_file_description": "Περιγραφή Αρχείου", "Upload_file_name": "Ονομα αρχείου", "Upload_file_question": "Να ανέβει το αρχείο;", "Upload_user_avatar": "Ανεβάστε το avatar", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 82967fddf1acb..c91d77b4cd8b7 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1037,7 +1037,6 @@ "Cannot_invite_users_to_direct_rooms": "Cannot invite users to direct rooms", "Cannot_open_conversation_with_yourself": "Cannot Direct Message with yourself", "Cannot_share_your_location": "Cannot share your location...", - "Cannot_upload_file_character_limit": "Cannot upload file, description is over the {{count}} character limit", "Cant_join": "Can't join", "Categories": "Categories", "Categories*": "Categories*", @@ -2066,6 +2065,7 @@ "Entertainment": "Entertainment", "Error": "Error", "Error_404": "Error:404", + "Error_encrypting_file": "Error while encrypting file", "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "Error: Rocket.Chat requires oplog tailing when running in multiple instances", "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "Please make sure your MongoDB is on ReplicaSet mode and MONGO_OPLOG_URL environment variable is defined correctly on the application server", "Error_Site_URL": "Invalid Site_Url", @@ -2275,6 +2275,8 @@ "FileUpload_Error": "File Upload Error", "FileUpload_FileSystemPath": "System Path", "FileUpload_File_Empty": "File empty", + "FileUpload_Canceled": "Upload canceled", + "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", @@ -4793,6 +4795,7 @@ "Self_managed_hosting": "Self-managed hosting", "Send": "Send", "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", + "Send_anyway": "Send anyway", "Send_Test": "Send Test", "Send_Test_Email": "Send test email", "Send_Visitor_navigation_history_as_a_message": "Send Visitor Navigation History as a Message", @@ -5529,18 +5532,21 @@ "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", "Upload_app": "Upload App", "Upload_file": "Upload file", - "Upload_file_description": "File description", "Upload_file_name": "File name", "Upload_file_question": "Upload file?", "Upload_private_app": "Upload private app", "Upload_user_avatar": "Upload avatar", "Uploading_file": "Uploading file...", - "Uploading_file__fileName__": "Uploading file {{fileName}}", + "Uploading__count__file": { + "one": "Uploading {{count}} file", + "other": "Uploading {{count}} files" + }, "Uploads": "Uploads", "Uptime": "Uptime", "Usage": "Usage", @@ -5906,6 +5912,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.", "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.", @@ -6323,6 +6330,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": "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.", @@ -7057,6 +7065,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" diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 15e3e3e989af6..54f1bf1e55de1 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -2173,7 +2173,6 @@ "Updated_at": "Ĝisdatigita je", "UpgradeToGetMore_engagement-dashboard_Title": "Analitiko", "Upload_Folder_Path": "Alŝuti dosierujon", - "Upload_file_description": "Dosiero priskribo", "Upload_file_name": "Dosiernomo", "Upload_file_question": "Alŝutu dosieron?", "Upload_user_avatar": "Alŝuti avataron", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index de62f59c85f4f..f16422bcaacc8 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -3935,7 +3935,6 @@ "Upload_Folder_Path": "Ruta de carpeta de subida", "Upload_From": "Subir desde {{name}}", "Upload_app": "Subir aplicación", - "Upload_file_description": "Descripción de archivo", "Upload_file_name": "Nombre de archivo", "Upload_file_question": "¿Subir archivo?", "Upload_user_avatar": "Subir avatar", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index 5697d1acabdce..3679c67566457 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -2460,7 +2460,6 @@ "Updated_at": "به روز شده در", "UpgradeToGetMore_engagement-dashboard_Title": "تجزیه و تحلیل ترافیک", "Upload_Folder_Path": "مسیر پوشه آپلود", - "Upload_file_description": "توضیحات فایل", "Upload_file_name": "نام فایل", "Upload_file_question": "آپلود فایل؟", "Upload_user_avatar": "بارگذاری تصویر", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index a9b822cb72709..e5849b20c895c 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -4378,7 +4378,6 @@ "Upload_From": "Lataukset käyttäjältä {{name}}", "Upload_anyway": "Lataa silti", "Upload_app": "Lataa sovellus", - "Upload_file_description": "Tiedoston kuvaus", "Upload_file_name": "Tiedoston nimi", "Upload_file_question": "Ladataanko tiedosto?", "Upload_private_app": "Lataa yksityinen sovellus", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index f35809160ff9a..c3a712d08ba10 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -3815,7 +3815,6 @@ "Upload_Folder_Path": "Chemin du dossier de chargement", "Upload_From": "Charger depuis {{name}}", "Upload_app": "Charger l'application", - "Upload_file_description": "Description du fichier", "Upload_file_name": "Nom du fichier", "Upload_file_question": "Charger le fichier ?", "Upload_user_avatar": "Charger un avatar", diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 0238794894bfe..b40bb6a3771e7 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -1217,7 +1217,6 @@ "Updated_at": "עודכן ב", "UpgradeToGetMore_engagement-dashboard_Title": "סטטיסטיקה", "Upload": "העלאה", - "Upload_file_description": "תיאור קובץ", "Upload_file_name": "שם קובץ", "Upload_file_question": "להעלות קובץ?", "Uploading_file": "מעלה קובץ...", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index ed95662f37bf5..a4002206d36b5 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -4684,7 +4684,6 @@ "Upload_From": "{{name}} से अपलोड करें", "Upload_anyway": "फिर भी अपलोड करें", "Upload_app": "ऐप अपलोड करें", - "Upload_file_description": "फाइल विवरण", "Upload_file_name": "फ़ाइल का नाम", "Upload_file_question": "दस्तावेज अपलोड करें?", "Upload_private_app": "निजी ऐप अपलोड करें", diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index 44193eea1571b..4ddf9275ab148 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -2293,7 +2293,6 @@ "Updated_at": "Ažurirano u", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "Upload_Folder_Path": "Prijenos puta mape", - "Upload_file_description": "Opis fajla", "Upload_file_name": "Ime fajla", "Upload_file_question": "Prenesi datoteku?", "Upload_user_avatar": "Učitaj avatar", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index ecf9204511116..f517ea946834c 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -4115,7 +4115,6 @@ "Upload_Folder_Path": "Feltöltési mappa útvonala", "Upload_From": "Feltöltés innen: {{name}}", "Upload_app": "Alkalmazás feltöltése", - "Upload_file_description": "Fájl leírása", "Upload_file_name": "Fájlnév", "Upload_file_question": "Feltölti a fájlt?", "Upload_user_avatar": "Profilkép feltöltése", diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index c1de975661fa7..facac25d7069c 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -2171,7 +2171,6 @@ "Updated_at": "Diperbarui pada", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Unggah Jalur Folder", - "Upload_file_description": "Deskripsi berkas", "Upload_file_name": "Nama file", "Upload_file_question": "Unggah file?", "Upload_user_avatar": "Upload avatar", diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index a2aa1e70778e7..16b09bf3ddad2 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -2689,7 +2689,6 @@ "UpgradeToGetMore_custom-roles_Title": "Ruoli personalizzati", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Carica percorso cartella", - "Upload_file_description": "Descrizione file", "Upload_file_name": "Nome file", "Upload_file_question": "Caricare il file?", "Upload_user_avatar": "Carica avatar", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index f0f6386b07f59..fea01bd07f297 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -3774,7 +3774,6 @@ "Upload_Folder_Path": "フォルダパスのアップロード", "Upload_From": "{{name}}からアップロード", "Upload_app": "アプリのアップロード", - "Upload_file_description": "ファイルの説明", "Upload_file_name": "ファイル名", "Upload_file_question": "ファイルをアップロードしますか?", "Upload_user_avatar": "アバターをアップロード", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 0a2fa1ee47d52..d206f5a4bfd80 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -2940,7 +2940,6 @@ "Upload_Folder_Path": "საქაღალდის გზის ატვირთვა", "Upload_From": "ატვირთვა {{name}}", "Upload_app": "აპლიკაციის ატვირთვა", - "Upload_file_description": "ფაილის აღწერა", "Upload_file_name": "ფაილის სახელი", "Upload_file_question": "გსურთ ატვირთოთ ფაილი?", "Upload_user_avatar": "ავატარის ატვირთვა", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 551f897ca67cf..88decac3a1631 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -2474,7 +2474,6 @@ "UpgradeToGetMore_engagement-dashboard_Title": "វិភាគ", "Upload_Folder_Path": "ផ្ទុកផ្លូវថតឡើង", "Upload_From": "ផ្ទុកឡើងពី {{name}}", - "Upload_file_description": "ការពិពណ៌នាឯកសារ", "Upload_file_name": "ឈ្មោះ​ឯកសារ", "Upload_file_question": "ផ្ទុក​ឯកសារ​ឡើង​ឬ?", "Upload_user_avatar": "ផ្ទុករូបតំនាង", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index b831726de9664..8ad834a9ffcee 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -3223,7 +3223,6 @@ "Upload_Folder_Path": "폴더 경로 업로드", "Upload_From": " {{name}} 에서 업로드", "Upload_app": "앱 업로드", - "Upload_file_description": "파일 설명", "Upload_file_name": "파일 이름", "Upload_file_question": "파일을 업로드하시겠습니까?", "Upload_user_avatar": "아바타 업로드", diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index 50ffa0b9c9fe6..c467f5323b86f 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -2167,7 +2167,6 @@ "Updated_at": "Nûvekirî", "UpgradeToGetMore_engagement-dashboard_Title": "analytics", "Upload_Folder_Path": "Peldanka Peldanka Hilbijêre", - "Upload_file_description": "Pirtûka pelê", "Upload_file_name": "Navê pelê", "Upload_file_question": "Pelê bar bike?", "Upload_user_avatar": "Avatar hilbijêre", diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index fae47bb89f4c6..2af8401d90444 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -2200,7 +2200,6 @@ "Updated_at": "Updated at", "UpgradeToGetMore_engagement-dashboard_Title": "ການວິເຄາະ", "Upload_Folder_Path": "ອັບໂຫລດໂຟເດີໂຟເດີ", - "Upload_file_description": "ລາຍລະອຽດຂອງໄຟລ໌", "Upload_file_name": "ຊື່​ເອ​ກະ​ສານ", "Upload_file_question": "ອັບໂຫລດເອກະສານ?", "Upload_user_avatar": "ອັບໂຫລດ avatar", diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index e9caec230cf7d..26c77c84ab205 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -2225,7 +2225,6 @@ "Updated_at": "Atnaujinta", "UpgradeToGetMore_engagement-dashboard_Title": "\"Analytics\"", "Upload_Folder_Path": "Įkelti aplanko kelią", - "Upload_file_description": "Failo aprašymas", "Upload_file_name": "Failo pavadinimas", "Upload_file_question": "Įkelti failą?", "Upload_user_avatar": "Įkelti avatarą", diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index fbf1c9d03bd66..99161e3f78d6b 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -2185,7 +2185,6 @@ "Updated_at": "Atjaunināts uz", "UpgradeToGetMore_engagement-dashboard_Title": "Analītika", "Upload_Folder_Path": "Augšupielādēt mapes ceļu", - "Upload_file_description": "Faila apraksts", "Upload_file_name": "Faila nosaukums", "Upload_file_question": "Vai augšupielādēt failu?", "Upload_user_avatar": "Augšupielādēt avataru", diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index 72c04332a5c36..0038b53ac8705 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -2171,7 +2171,6 @@ "Updated_at": "Дээр шинэчилсэн", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитик", "Upload_Folder_Path": "Folder Path-г оруулна уу", - "Upload_file_description": "Файлын тайлбар", "Upload_file_name": "Файлын нэр", "Upload_file_question": "Файл оруулах уу?", "Upload_user_avatar": "Зургийг байршуулна уу", diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index 1aadb5f2e7dd0..1350515fab24c 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -2177,7 +2177,6 @@ "Updated_at": "Dikemaskini di", "UpgradeToGetMore_engagement-dashboard_Title": "Analisis", "Upload_Folder_Path": "Muatkan Laluan Folder", - "Upload_file_description": "Penerangan fail", "Upload_file_name": "Nama fail", "Upload_file_question": "Muat naik fail?", "Upload_user_avatar": "Muat naik avatar", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index caa6d826b9277..ec3ed6cfa82de 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -940,7 +940,6 @@ "Cannot_invite_users_to_direct_rooms": "Kan ikke invitere brukere til direkterom", "Cannot_open_conversation_with_yourself": "Kan ikke sende direkte-melding til deg selv", "Cannot_share_your_location": "Kan ikke dele din posisjonen...", - "Cannot_upload_file_character_limit": "Kan ikke laste opp filen, beskrivelsen overskrider {{count}} tegn", "Cant_join": "Kan ikke bli med", "Categories": "Kategorier", "Categories*": "Kategorier*", @@ -5382,13 +5381,11 @@ "Upload_anyway": "Last opp allikevel", "Upload_app": "Last opp app", "Upload_file": "Last opp fil", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Laste opp fil?", "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/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index 814f4045fc636..46f1cc2e4dfcb 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -3803,7 +3803,6 @@ "Upload_Folder_Path": "Upload mappad", "Upload_From": "Uploaden van {{name}}", "Upload_app": "App uploaden", - "Upload_file_description": "Bestandsomschrijving", "Upload_file_name": "Bestandsnaam", "Upload_file_question": "Upload bestand?", "Upload_user_avatar": "Upload avatar", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index cf5a888534793..0b5616ff6785d 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -914,7 +914,6 @@ "Cannot_invite_users_to_direct_rooms": "Kan ikke invitere brukere til å lede rom", "Cannot_open_conversation_with_yourself": "Kan ikke sende melding til deg selv", "Cannot_share_your_location": "Kan ikke dele din posisjonen...", - "Cannot_upload_file_character_limit": "Kan ikke laste opp filen, beskrivelsen overskrider {{count}} tegn", "Cant_join": "Kan ikke bli med", "Categories": "Kategorier", "Categories*": "Kategorier*", @@ -4923,7 +4922,6 @@ "Upload_anyway": "Last opp allikevel", "Upload_app": "Last opp app", "Upload_file": "Last opp fil", - "Upload_file_description": "Filbeskrivelse", "Upload_file_name": "Filnavn", "Upload_file_question": "Last opp fil?", "Upload_private_app": "Last opp privat app", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index f92eb518535ad..2e6516f49a60e 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -4124,7 +4124,6 @@ "Upload_Folder_Path": "Prześlij ścieżkę folderu", "Upload_From": "Prześlij z {{name}}", "Upload_app": "Prześlij aplikację", - "Upload_file_description": "Opis pliku", "Upload_file_name": "Nazwa pliku", "Upload_file_question": "Przesłać plik?", "Upload_user_avatar": "Załaduj awatar", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index bb0fcd949a94c..76c6617598bae 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -933,7 +933,6 @@ "Cannot_invite_users_to_direct_rooms": "Não é possível convidar pessoas para salas diretas", "Cannot_open_conversation_with_yourself": "Não é possível dirigir a mensagem com você mesmo", "Cannot_share_your_location": "Não foi possível compartilhar sua localização...", - "Cannot_upload_file_character_limit": "Não é possível fazer upload de arquivo, a descrição está acima do limite de caracteres {{count}} ", "Cant_join": "Não é possível participar", "Categories": "Categorias", "Categories*": "Categorias*", @@ -5261,7 +5260,6 @@ "Upload_anyway": "Fazer upload de qualquer forma", "Upload_app": "Upload de aplicativo", "Upload_file": "Carregar arquivo", - "Upload_file_description": "Descrição do arquivo", "Upload_file_name": "Nome do arquivo", "Upload_file_question": "Fazer upload de arquivo?", "Upload_private_app": "Carregar aplicativo privado", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index d77fa3919e2d3..747663407d5fa 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -2515,7 +2515,6 @@ "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Carregar caminho da pasta", "Upload_From": "Upload de {{name}}", - "Upload_file_description": "Descrição do ficheiro", "Upload_file_name": "Nome do ficheiro", "Upload_file_question": "Carregar ficheiro?", "Upload_user_avatar": "Carregar Avatar", diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index 5a9b0e35be816..8f0ac8dba714f 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -2174,7 +2174,6 @@ "Updated_at": "Actualizat la", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "Încărcați calea folderelor", - "Upload_file_description": "Descrierea fisierului", "Upload_file_name": "Nume de fișier", "Upload_file_question": "Încarcă fișier?", "Upload_user_avatar": "Încărcați avatar", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 7f19415d8c359..902658f65a3df 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -3957,7 +3957,6 @@ "Upload_Folder_Path": "Путь к папке загрузки", "Upload_From": "Загрузить с {{name}}", "Upload_app": "Загрузить приложение", - "Upload_file_description": "Описание файла", "Upload_file_name": "Имя файла", "Upload_file_question": "Загрузить файл?", "Upload_user_avatar": "Загруженный аватар", diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index 68a9eb0284f57..bc4ad81b8ba2a 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -2179,7 +2179,6 @@ "Updated_at": "Aktualizované na", "UpgradeToGetMore_engagement-dashboard_Title": "Analytika", "Upload_Folder_Path": "Nahrať cestu priečinka", - "Upload_file_description": "Popis súboru", "Upload_file_name": "Názov súboru", "Upload_file_question": "Nahrajte súbor?", "Upload_user_avatar": "Nahrať avatar", diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index d4e3e956dda58..fb738e0282d44 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -2171,7 +2171,6 @@ "Updated_at": "Posodobljeno ob", "UpgradeToGetMore_engagement-dashboard_Title": "Analiza", "Upload_Folder_Path": "Naloži pot do mape", - "Upload_file_description": "Opis datoteke", "Upload_file_name": "Ime datoteke", "Upload_file_question": "Želite naložiti datoteko?", "Upload_user_avatar": "Naloži avatar", diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index e9282747915d6..79d83c53c7664 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -2175,7 +2175,6 @@ "Updated_at": "Përditësuar në", "UpgradeToGetMore_engagement-dashboard_Title": "Analitikë", "Upload_Folder_Path": "Ngarko dosjen e dosjes", - "Upload_file_description": "Përshkrimi i skedarit", "Upload_file_name": "Emri i skedarit", "Upload_file_question": "Ngarko skedar?", "Upload_user_avatar": "Ngarko avatar", diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index 931fba1cb698c..323697438cab3 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -2009,7 +2009,6 @@ "Updated_at": "Ажурирано у", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитика", "Upload_Folder_Path": "Путања фолдера", - "Upload_file_description": "Опис фајла", "Upload_file_name": "Назив документа", "Upload_file_question": "Отпреми датотеку?", "Upload_user_avatar": "Уплоад аватар", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 096c327993124..e793c962f4a35 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -932,7 +932,6 @@ "Cannot_invite_users_to_direct_rooms": "Det går inte att bjuda in användare att styra rum", "Cannot_open_conversation_with_yourself": "Kan inte skicka direktmeddelande till dig själv", "Cannot_share_your_location": "Kan inte dela din plats...", - "Cannot_upload_file_character_limit": "Det går inte att ladda upp filen, beskrivningen överskrider teckengränsen på {{count}} ", "Cant_join": "Kan inte gå med", "Categories": "Kategorier", "Categories*": "Kategorier*", @@ -5290,13 +5289,11 @@ "Upload_anyway": "Ladda upp ändå", "Upload_app": "Ladda upp app", "Upload_file": "Ladda upp fil", - "Upload_file_description": "Filbeskrivning", "Upload_file_name": "Filnamn", "Upload_file_question": "Ladda upp fil?", "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", diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index 7d9181f76cafe..88bd24cfe150d 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -2174,7 +2174,6 @@ "Updated_at": "புதுப்பிக்கப்பட்டது", "UpgradeToGetMore_engagement-dashboard_Title": "அனலிட்டிக்ஸ்", "Upload_Folder_Path": "கோப்புறை பாதை பதிவேற்றவும்", - "Upload_file_description": "கோப்பு விளக்கம்", "Upload_file_name": "கோப்பு பெயர்", "Upload_file_question": "கோப்பை பதிவேற்ற?", "Upload_user_avatar": "பதிவைப் பதிவேற்று", diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index 43beb1d1fa398..ed4ddcbff76c2 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -2167,7 +2167,6 @@ "Updated_at": "อัปเดตเมื่อวันที่", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Upload_Folder_Path": "อัปโหลดเส้นทางโฟลเดอร์", - "Upload_file_description": "คำอธิบายไฟล์", "Upload_file_name": "ชื่อไฟล์", "Upload_file_question": "อัปโหลดไฟล์หรือไม่?", "Upload_user_avatar": "อัปโหลดภาพอวตาร", diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 06e8d3364cdea..154fcd48cc378 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -2581,7 +2581,6 @@ "Upload_Folder_Path": "Dosya yükleme konumu", "Upload_From": "{{name}} den yükle", "Upload_app": "Uygulamayı Yükle", - "Upload_file_description": "Dosya açıklaması", "Upload_file_name": "Dosya adı", "Upload_file_question": "Dosya yükle?", "Upload_user_avatar": "Avatarı yükle", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 5d79695872f3c..a917a483a5b05 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -2667,7 +2667,6 @@ "UpgradeToGetMore_auditing_Title": "Аудит повідомлень", "UpgradeToGetMore_engagement-dashboard_Title": "Аналітика", "Upload_Folder_Path": "Завантажте шлях до папки", - "Upload_file_description": "Опис файлу", "Upload_file_name": "Ім'я файлу", "Upload_file_question": "Завантажити файл?", "Upload_user_avatar": "Завантажити аватар", diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index 72742c0e0f6ab..1cbf7609bbac1 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -2268,7 +2268,6 @@ "Updated_at": "Cập nhật tại", "UpgradeToGetMore_engagement-dashboard_Title": "phân tích", "Upload_Folder_Path": "Tải lên đường dẫn thư mục", - "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_user_avatar": "Tải lên hình đại diện", diff --git a/packages/i18n/src/locales/zh-HK.i18n.json b/packages/i18n/src/locales/zh-HK.i18n.json index c95a888279ae1..547d0fad75e49 100644 --- a/packages/i18n/src/locales/zh-HK.i18n.json +++ b/packages/i18n/src/locales/zh-HK.i18n.json @@ -2201,7 +2201,6 @@ "Updated_at": "更新于", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "Upload_Folder_Path": "上传文件夹路径", - "Upload_file_description": "文件描述", "Upload_file_name": "文件名", "Upload_file_question": "上传文件?", "Upload_user_avatar": "上传头像", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index d5d9768f7964e..560bfbfe7fd26 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -3602,7 +3602,6 @@ "Upload_Folder_Path": "上傳資料夾路徑", "Upload_From": "從 {{name}} 上傳", "Upload_app": "上傳應用程式", - "Upload_file_description": "檔案敘述", "Upload_file_name": "檔案名稱", "Upload_file_question": "是否上傳檔案?", "Upload_user_avatar": "上傳頭像", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index a84225ff34232..9f336a437edb4 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1030,7 +1030,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*": "类别*", @@ -5513,13 +5512,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": "使用情况", diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 97b8d483f2212..3aa1a850a746b 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -794,10 +794,14 @@ export type RoomsEndpoints = { groupable?: boolean; msg?: string; tmid?: string; - customFields?: string; + customFields?: Record; t?: IMessage['t']; content?: IE2EEMessage['content']; - }) => { message: IMessage | null }; + fileName?: string; + fileContent?: IE2EEMessage['content']; + }) => { + message: IMessage | null; + }; }; '/v1/rooms.nameExists': { diff --git a/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx b/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx index 3c03bed370ea0..0b6b511715cd8 100644 --- a/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx +++ b/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx @@ -1,4 +1,5 @@ -import { Button } from '@rocket.chat/fuselage'; +import { Button, IconButton } from '@rocket.chat/fuselage'; +import { action } from '@storybook/addon-actions'; import type { Meta, StoryFn } from '@storybook/react'; import '@rocket.chat/icons/dist/rocketchat.css'; @@ -13,6 +14,9 @@ import { MessageComposerSkeleton, MessageComposerHint, MessageComposerInputExpandable, + MessageComposerFile, + MessageComposerFileGroup, + MessageComposerFileError, } from '.'; export default { @@ -105,4 +109,40 @@ export const WithSubmit: StoryFn = () => ( ); +export const WithFiles: StoryFn = () => ( + + + + } + onClick={action('click')} + /> + } + onClick={action('click')} + /> + } + onClick={action('click')} + /> + + + + + + + + + +); + export const Loading: StoryFn = () => ; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFile.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFile.tsx new file mode 100644 index 0000000000000..c812d1807c665 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFile.tsx @@ -0,0 +1,96 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import { useButtonPattern } from '@rocket.chat/fuselage-hooks'; +import { useMemo, type KeyboardEvent, type MouseEvent, type AllHTMLAttributes, type ReactElement } from 'react'; + +type MessageComposerFileProps = { + fileTitle: string; + fileSubtitle: string; + actionIcon: ReactElement; + error?: boolean; + disabled?: boolean; + onClick: () => void; +} & Omit, 'is'>; + +const MessageComposerFile = ({ + fileTitle, + fileSubtitle, + actionIcon, + error, + disabled, + onClick, + className, + ...props +}: MessageComposerFileProps) => { + const closeWrapperStyle = css` + position: absolute; + right: 0.5rem; + top: 0.5rem; + `; + + const previewWrapperStyle = css` + background-color: ${Palette.surface['surface-tint']}; + cursor: ${error || disabled ? 'not-allowed' : 'pointer'}; + + &:hover { + background-color: ${!error && !disabled ? Palette.surface['surface-hover'] : Palette.surface['surface-tint']}; + } + `; + + const handleClick = (e: MouseEvent | KeyboardEvent) => { + e.stopPropagation(); + if (!error && !disabled) { + onClick(); + } + }; + + const buttonProps = useButtonPattern(handleClick); + + const subtitleColor = useMemo(() => { + if (error) { + return 'danger'; + } + + if (disabled) { + return 'disabled'; + } + + return 'hint'; + }, [disabled, error]); + + return ( + + + + {fileTitle} + + + {fileSubtitle} + + + {!disabled && {actionIcon}} + + ); +}; + +export default MessageComposerFile; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileError.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileError.tsx new file mode 100644 index 0000000000000..6fc5f180f758a --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileError.tsx @@ -0,0 +1,29 @@ +import type { AllHTMLAttributes, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import MessageComposerFile from './MessageComposerFile'; + +type MessageComposerFileErrorProps = { + fileTitle: string; + error: Error; + actionIcon: ReactElement; + onClick: () => void; +} & AllHTMLAttributes; + +const MessageComposerFileError = ({ fileTitle, error, actionIcon, onClick, ...props }: MessageComposerFileErrorProps) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default MessageComposerFileError; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileGroup.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileGroup.tsx new file mode 100644 index 0000000000000..c745b60a1bcf2 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileGroup.tsx @@ -0,0 +1,23 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +const MessageComposerFileGroup = ({ children, style, ...props }: ComponentProps) => { + return ( + + {children} + + ); +}; + +export default MessageComposerFileGroup; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileLoader.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileLoader.tsx new file mode 100644 index 0000000000000..ca515e3a534b8 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerFile/MessageComposerFileLoader.tsx @@ -0,0 +1,41 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { AllHTMLAttributes } from 'react'; + +const MessageComposerFileLoader = ({ className, ...props }: Omit, 'is'>) => { + 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; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerFile/index.ts b/packages/ui-composer/src/MessageComposer/MessageComposerFile/index.ts new file mode 100644 index 0000000000000..6414906325355 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerFile/index.ts @@ -0,0 +1,4 @@ +export { default as MessageComposerFile } from './MessageComposerFile'; +export { default as MessageComposerFileError } from './MessageComposerFileError'; +export { default as MessageComposerFileLoader } from './MessageComposerFileLoader'; +export { default as MessageComposerFileGroup } from './MessageComposerFileGroup'; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerHint.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerHint.tsx index fbb282583a6be..61f4abba70662 100644 --- a/packages/ui-composer/src/MessageComposer/MessageComposerHint.tsx +++ b/packages/ui-composer/src/MessageComposer/MessageComposerHint.tsx @@ -12,7 +12,7 @@ const MessageComposerHint = ({ icon, children, helperText }: MessageComposerHint : undefined}>{children} {helperText && ( - + {helperText} )} diff --git a/packages/ui-composer/src/MessageComposer/__snapshots__/MessageComposer.spec.tsx.snap b/packages/ui-composer/src/MessageComposer/__snapshots__/MessageComposer.spec.tsx.snap index a6b54b51ea279..d2fdb846060ac 100644 --- a/packages/ui-composer/src/MessageComposer/__snapshots__/MessageComposer.spec.tsx.snap +++ b/packages/ui-composer/src/MessageComposer/__snapshots__/MessageComposer.spec.tsx.snap @@ -591,6 +591,326 @@ exports[`renders ToolbarActions without crashing 1`] = ` `; +exports[`renders WithFiles without crashing 1`] = ` + +
+
+