From 56dda66c1c559a89dee9212e1727c099363eeeac Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 22 Jun 2021 12:08:39 +0100 Subject: [PATCH] Add file pasting and drag-n-drop. --- .../common/messaging/MessageBox.tsx | 7 ++ src/context/revoltjs/FileUploads.tsx | 70 ++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index ffcc45a5..e938e73c 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -255,6 +255,13 @@ function MessageBox({ channel, draft, dispatcher }: Props) { remove={async () => setUploadState({ type: "none" })} onChange={files => setUploadState({ type: "attached", files })} cancel={() => uploadState.type === 'uploading' && uploadState.cancel.cancel("cancel")} + append={files => { + if (uploadState.type === 'none') { + setUploadState({ type: 'attached', files }); + } else if (uploadState.type === 'attached') { + setUploadState({ type: 'attached', files: [ ...uploadState.files, ...files ] }); + } + }} /> void } | - { behaviour: 'multi', onChange: (files: File[]) => void } | - { behaviour: 'upload', onUpload: (id: string) => Promise } + { behaviour: 'upload', onUpload: (id: string) => Promise } | + { behaviour: 'multi', onChange: (files: File[]) => void, append?: (files: File[]) => void } ) & ( { style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } | { style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number } @@ -107,6 +107,70 @@ export function FileUploader(props: Props) { } } + if (props.behaviour === 'multi' && props.append) { + useEffect(() => { + // File pasting. + function paste(e: ClipboardEvent) { + const items = e.clipboardData?.items; + if (typeof items === "undefined") return; + if (props.behaviour !== 'multi' || !props.append) return; + + let files = []; + for (const item of items) { + if (!item.type.startsWith("text/")) { + const blob = item.getAsFile(); + if (blob) { + if (blob.size > props.maxFileSize) { + openScreen({ id: 'error', error: 'FileTooLarge' }); + } + + files.push(blob); + } + } + } + + props.append(files); + } + + // Let the browser know we can drop files. + function dragover(e: DragEvent) { + e.stopPropagation(); + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = "copy"; + } + + // File dropping. + function drop(e: DragEvent) { + e.preventDefault(); + if (props.behaviour !== 'multi' || !props.append) return; + + const dropped = e.dataTransfer?.files; + if (dropped) { + let files = []; + for (const item of dropped) { + if (item.size > props.maxFileSize) { + openScreen({ id: 'error', error: 'FileTooLarge' }); + } + + files.push(item); + } + + props.append(files); + } + } + + document.addEventListener("paste", paste); + document.addEventListener("dragover", dragover); + document.addEventListener("drop", drop); + + return () => { + document.removeEventListener("paste", paste); + document.removeEventListener("dragover", dragover); + document.removeEventListener("drop", drop); + }; + }, [ props.append ]); + } + if (props.style === 'icon' || props.style === 'banner') { const { style, previewURL, defaultPreview, width, height } = props; return (