diff --git a/figma/server-settings.fig b/figma/server-settings.fig new file mode 100644 index 0000000..8860553 Binary files /dev/null and b/figma/server-settings.fig differ diff --git a/src/client/webapp/elements/components/components.scss b/src/client/webapp/elements/components/components.scss index 6b75481..54bce48 100644 --- a/src/client/webapp/elements/components/components.scss +++ b/src/client/webapp/elements/components/components.scss @@ -238,3 +238,38 @@ font-weight: 600; } } + +.file-drop { + position: fixed; + width: 100vw; + height: calc(100vh - 22px); + top: 22px; + left: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: $background-overlay; + + .panel { + // TODO: make this nicer + color: $text-normal; + background-color: $background-primary; + padding: 16px; + border-radius: 8px; + + display: flex; + align-items: center; + + img.drop-icon { + display: block; + height: 128px; + max-width: 128px; + margin-right: 16px; + } + + .drop-message { + font-size: 2em; + color: $header-primary; + } + } +} diff --git a/src/client/webapp/elements/components/file-drop-target.tsx b/src/client/webapp/elements/components/file-drop-target.tsx new file mode 100644 index 0000000..b45fb18 --- /dev/null +++ b/src/client/webapp/elements/components/file-drop-target.tsx @@ -0,0 +1,66 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { Dispatch, DragEvent, FC, SetStateAction, useCallback, useRef } from 'react'; + +export interface FileDropTargetProps { + message: string; + setBuff: Dispatch>; + setName: Dispatch>; + close: () => void; +} + +const FileDropTarget: FC = (props: FileDropTargetProps) => { + const { setBuff, setName, close, message } = props; + + const rootRef = useRef(null); + + const onDrop = useCallback(async (event: DragEvent) => { + event.preventDefault(); + + for (const item of event.dataTransfer.items) { + if (item.kind === 'file') { + // We only care about the first file + const file = item.getAsFile(); + if (!file) { // should never happen + close(); + return; + } + const buff = Buffer.from(await file.arrayBuffer()); + const name = file.name; + setBuff(buff); + setName(name); + close(); + return; + } + } + + // No files + close(); + }, [ setBuff, setName, close ]); + + const onDragOver = useCallback((event: DragEvent) => { + // Required for some reason... + event.preventDefault(); + }, []); + + const onDragLeave = useCallback((event: DragEvent) => { + if (!rootRef.current) return; + if (!rootRef.current.contains(event.relatedTarget as Node | null)) { + close(); + } + }, [ close ]); + + return ( +
+
+ file +
{message}
+
+
+ ); +} + +export default FileDropTarget; diff --git a/src/client/webapp/elements/components/overlay.tsx b/src/client/webapp/elements/components/overlay.tsx index b25b9bd..bfbc542 100644 --- a/src/client/webapp/elements/components/overlay.tsx +++ b/src/client/webapp/elements/components/overlay.tsx @@ -8,7 +8,7 @@ import ElementsUtil from '../require/elements-util'; import ReactHelper from '../require/react-helper'; interface OverlayProps { - childRootRef: RefObject; // clicks outside this ref will close the overlay + childRootRef?: RefObject; // clicks outside this ref will close the overlay close?: () => void; children: React.ReactNode; } @@ -24,7 +24,9 @@ const Overlay: FC = (props: OverlayProps) => { } }, []); - ReactHelper.useCloseWhenClickedOutsideEffect(childRootRef, () => { removeSelf(); }); + if (childRootRef) { + ReactHelper.useCloseWhenClickedOutsideEffect(childRootRef, () => { removeSelf(); }); + } const keyDownHandler = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') { diff --git a/src/client/webapp/elements/events-add-guild.tsx b/src/client/webapp/elements/events-add-guild.tsx index fecb95e..d933514 100644 --- a/src/client/webapp/elements/events-add-guild.tsx +++ b/src/client/webapp/elements/events-add-guild.tsx @@ -83,4 +83,4 @@ export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, gui contextElement.parentElement.removeChild(contextElement); } }); -} \ No newline at end of file +} diff --git a/src/client/webapp/elements/lists/components/message-element.scss b/src/client/webapp/elements/lists/components/message-element.scss index 3aa30d4..13bf0c0 100644 --- a/src/client/webapp/elements/lists/components/message-element.scss +++ b/src/client/webapp/elements/lists/components/message-element.scss @@ -109,7 +109,7 @@ display: flex; align-items: center; background-color: $background-secondary-alt; - padding: 8px; + padding: 8px 12px; border-radius: 8px; cursor: pointer; @@ -118,12 +118,12 @@ } > :not(:last-child) { - margin-right: 8px; + margin-right: 12px; } .icon { - width: 26px; - height: 26px; + max-width: 32px; + height: 32px; -webkit-user-select: none; user-select: none; } diff --git a/src/client/webapp/elements/lists/components/message-element.tsx b/src/client/webapp/elements/lists/components/message-element.tsx index 5125dfd..1b27019 100644 --- a/src/client/webapp/elements/lists/components/message-element.tsx +++ b/src/client/webapp/elements/lists/components/message-element.tsx @@ -30,7 +30,7 @@ const ResourceElement: FC = (props: ResourceElementProps) return (
- file + file
{resourceName}
{text}
diff --git a/src/client/webapp/elements/require/base-elements.tsx b/src/client/webapp/elements/require/base-elements.tsx index e805727..a94f797 100644 --- a/src/client/webapp/elements/require/base-elements.tsx +++ b/src/client/webapp/elements/require/base-elements.tsx @@ -444,10 +444,10 @@ export default class BaseElements { (q.$$$(element, '.title img') as HTMLImageElement).src = resourceSrc; } catch (e) { LOG.error('Error loading image resource', e); - (q.$$$(element, 'img.avatar') as HTMLImageElement).src = './img/file-icon.png'; // not the error icon here + (q.$$$(element, 'img.avatar') as HTMLImageElement).src = './img/file-improved.svg'; // not the error icon here } } else { - (q.$$$(element, '.title img') as HTMLImageElement).src = './img/file-icon.png'; + (q.$$$(element, '.title img') as HTMLImageElement).src = './img/file-improved.svg'; } })(); return element; diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.tsx similarity index 93% rename from src/client/webapp/elements/require/react-helper.ts rename to src/client/webapp/elements/require/react-helper.tsx index d4e82a7..b436a55 100644 --- a/src/client/webapp/elements/require/react-helper.ts +++ b/src/client/webapp/elements/require/react-helper.tsx @@ -13,6 +13,8 @@ import path from 'path'; import fs from 'fs/promises'; import electron from 'electron'; import ElementsUtil, { IAlignment } from './elements-util'; +import React from 'react'; +import FileDropTarget from '../components/file-drop-target'; // Helper function so we can use JSX before fully committing to React @@ -475,4 +477,40 @@ export default class ReactHelper { return [ isOpen ? contextHover : null, mouseEnterCallable, mouseLeaveCallable ]; } -} \ No newline at end of file + + static useDocumentDropTarget( + message: string, + setBuff: Dispatch>, + setName: Dispatch> + ): [ dropTarget: ReactNode ] { + const [ dragEnabled, setDragEnabled ] = useState(false); + + // Drag overlay + const closeDragOverlay = useCallback(() => { setDragEnabled(false); }, []); + + const onDragEnter = useCallback((event: DragEvent) => { + event.preventDefault(); + setDragEnabled(true); + }, []); + + useEffect(() => { + document.addEventListener('dragenter', onDragEnter); + + return () => { + document.removeEventListener('dragenter', onDragEnter); + } + }, [ closeDragOverlay, onDragEnter ]); + + const dropTarget = useMemo(() => { + if (!dragEnabled) return null; + return ( + + ); + }, [ dragEnabled, message, setBuff, setName ]); + + return [ dropTarget ]; + } +} diff --git a/src/client/webapp/elements/sections/send-message.tsx b/src/client/webapp/elements/sections/send-message.tsx index 6ad2317..92826fb 100644 --- a/src/client/webapp/elements/sections/send-message.tsx +++ b/src/client/webapp/elements/sections/send-message.tsx @@ -24,12 +24,12 @@ const AttachmentPreview: FC = (props: AttachmentPreviewP const [ attachmentImgSrc ] = ReactHelper.useOneTimeAsyncAction( async () => { const type = await FileType.fromBuffer(attachmentBuff); - if (!type) return './img/file-icon.png'; + if (!type) return './img/file-improved.svg'; if (type.mime === 'image/gif' || type.mime === 'image/jpeg' || type.mime === 'image/png') { // show a preview of the image return await ElementsUtil.getImageSrcFromBufferFailSoftly(attachmentBuff); } else { - return './img/file-icon.png'; + return './img/file-improved.svg'; } }, './img/loading.svg', @@ -102,6 +102,8 @@ const SendMessage: FC = (props: SendMessageProps) => { setAttachmentName(null); }, []); + const [ attachmentDropTarget ] = ReactHelper.useDocumentDropTarget('Upload to #' + channel.name, setAttachmentBuff, setAttachmentName); + const attachmentPreview = useMemo(() => { if (!attachmentBuff || !attachmentName) return null; return @@ -136,6 +138,7 @@ const SendMessage: FC = (props: SendMessageProps) => { />
+ {attachmentDropTarget} ); } diff --git a/src/client/webapp/img/file-improved.svg b/src/client/webapp/img/file-improved.svg new file mode 100644 index 0000000..ada240e --- /dev/null +++ b/src/client/webapp/img/file-improved.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/client/webapp/styles/channel-feed.scss b/src/client/webapp/styles/channel-feed.scss index 0c52cfc..9f7edbe 100644 --- a/src/client/webapp/styles/channel-feed.scss +++ b/src/client/webapp/styles/channel-feed.scss @@ -74,6 +74,7 @@ $borderRadius: 8px; } .send-message-input-row { + width: 100%; display: flex; align-items: flex-start;