From d2d70a19521cda398b748abe17129c23121ecad3 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 2 Feb 2022 22:48:29 +0100 Subject: [PATCH] Composer: Support uploading folders (#1682) --- src/components/middle/MiddleColumn.tsx | 2 +- src/components/middle/composer/DropArea.tsx | 17 ++++++-- .../helpers/getFilesFromDataTransferItems.ts | 41 +++++++++++++++++++ .../composer/hooks/useClipboardPaste.ts | 28 +++++++------ .../middle/message/_message-content.scss | 6 ++- 5 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 src/components/middle/composer/helpers/getFilesFromDataTransferItems.ts diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 5c3b692c0..b0b4544c6 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -254,7 +254,7 @@ const MiddleColumn: FC = ({ } const { items } = e.dataTransfer || {}; - const shouldDrawQuick = items && Array.from(items) + const shouldDrawQuick = items && items.length > 0 && Array.from(items) // Filter unnecessary element for drag and drop images in Firefox (https://github.com/Ajaxy/telegram-tt/issues/49) // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#image .filter((item) => item.type !== 'text/uri-list') diff --git a/src/components/middle/composer/DropArea.tsx b/src/components/middle/composer/DropArea.tsx index 55c6193be..3ae1473de 100644 --- a/src/components/middle/composer/DropArea.tsx +++ b/src/components/middle/composer/DropArea.tsx @@ -4,6 +4,7 @@ import React, { import useShowTransition from '../../../hooks/useShowTransition'; import buildClassName from '../../../util/buildClassName'; +import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import usePrevious from '../../../hooks/usePrevious'; @@ -38,13 +39,23 @@ const DropArea: FC = ({ useEffect(() => (isOpen ? captureEscKeyListener(onHide) : undefined), [isOpen, onHide]); - const handleFilesDrop = useCallback((e: React.DragEvent) => { + const handleFilesDrop = useCallback(async (e: React.DragEvent) => { const { dataTransfer: dt } = e; + let files: File[] = []; + + if (dt.items && dt.items.length > 0) { + const folderFiles = await getFilesFromDataTransferItems(dt.items); + if (folderFiles.length) { + files = files.concat(folderFiles); + } + } if (dt.files && dt.files.length > 0) { - onHide(); - onFileSelect(Array.from(dt.files), false); + files = files.concat(Array.from(dt.files)); } + + onHide(); + onFileSelect(files, false); }, [onFileSelect, onHide]); const handleQuickFilesDrop = useCallback((e: React.DragEvent) => { diff --git a/src/components/middle/composer/helpers/getFilesFromDataTransferItems.ts b/src/components/middle/composer/helpers/getFilesFromDataTransferItems.ts new file mode 100644 index 000000000..f6c16e50d --- /dev/null +++ b/src/components/middle/composer/helpers/getFilesFromDataTransferItems.ts @@ -0,0 +1,41 @@ +export default async function getFilesFromDataTransferItems(dataTransferItems: DataTransferItemList) { + const files: File[] = []; + + function traverseFileTreePromise(entry: FileSystemEntry | File) { + return new Promise(resolve => { + if (entry instanceof File) { + files.push(entry); + resolve(entry); + } else if (entry.isFile) { + (entry as FileSystemFileEntry).file((file) => { + files.push(file); + resolve(file); + }); + } else if (entry.isDirectory) { + let dirReader = (entry as FileSystemDirectoryEntry).createReader(); + dirReader.readEntries((entries) => { + let entriesPromises = []; + for (let entr of entries) { + entriesPromises.push(traverseFileTreePromise(entr)); + } + resolve(Promise.all(entriesPromises)); + }); + } + }); + } + + let entriesPromises = []; + for (let item of dataTransferItems) { + if (item.kind === 'file') { + const entry = item.webkitGetAsEntry() || item.getAsFile(); + if (entry) { + entriesPromises.push(traverseFileTreePromise(entry)); + } + } + } + + await Promise.all(entriesPromises); + + return files; +} + diff --git a/src/components/middle/composer/hooks/useClipboardPaste.ts b/src/components/middle/composer/hooks/useClipboardPaste.ts index 0067ddba0..634fb3f41 100644 --- a/src/components/middle/composer/hooks/useClipboardPaste.ts +++ b/src/components/middle/composer/hooks/useClipboardPaste.ts @@ -3,6 +3,7 @@ import { ApiAttachment, ApiMessage } from '../../../../api/types'; import buildAttachment from '../helpers/buildAttachment'; import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../../config'; +import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems'; const CLIPBOARD_ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/gif']; const MAX_MESSAGE_LENGTH = 4096; @@ -23,24 +24,25 @@ const useClipboardPaste = ( return; } - const { items } = e.clipboardData; - const media = Array.from(items) - .find((item) => CLIPBOARD_ACCEPTED_TYPES.includes(item.type) && item.kind === 'file'); - const file = media && media.getAsFile(); - const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH); - e.preventDefault(); - if (!file && !pastedText) { + const { items } = e.clipboardData; + let files: File[] = []; + + if (items.length > 0) { + files = await getFilesFromDataTransferItems(items); + } + const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH); + + if (files.length === 0 && !pastedText) { return; } - if (file && !editedMessage) { - const attachment = await buildAttachment(file.name, file, true); - setAttachments((attachments) => [ - ...attachments, - attachment, - ]); + if (files.length > 0 && !editedMessage) { + const newAttachments = await Promise.all(files.map((file) => { + return buildAttachment(file.name, file, files.length === 1 && CLIPBOARD_ACCEPTED_TYPES.includes(file.type)); + })); + setAttachments((attachments) => attachments.concat(newAttachments)); } if (pastedText) { diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index d4b061128..6ee09a460 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -482,7 +482,7 @@ &.is-reply .media-inner, &.force-sender-name .Album, &.is-reply .Album, - .message-title ~ .media-inner:not(.RoundVideo) { + .message-title ~ .media-inner { margin-top: 0.375rem; margin-bottom: -0.375rem; @@ -491,6 +491,10 @@ } } + &:not(.text) .RoundVideo { + margin-bottom: 0; + } + // Moved below .is-reply to overwrite its styles &.text .media-inner, &.text .Album {