drop target for send message component

This commit is contained in:
Michael Peters 2021-12-27 21:02:02 -06:00
parent f39c3f0a51
commit 15c92c2eac
12 changed files with 273 additions and 13 deletions

BIN
figma/server-settings.fig Normal file

Binary file not shown.

View File

@ -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;
}
}
}

View File

@ -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<SetStateAction<Buffer | null>>;
setName: Dispatch<SetStateAction<string | null>>;
close: () => void;
}
const FileDropTarget: FC<FileDropTargetProps> = (props: FileDropTargetProps) => {
const { setBuff, setName, close, message } = props;
const rootRef = useRef<HTMLDivElement>(null);
const onDrop = useCallback(async (event: DragEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
// Required for some reason...
event.preventDefault();
}, []);
const onDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
if (!rootRef.current) return;
if (!rootRef.current.contains(event.relatedTarget as Node | null)) {
close();
}
}, [ close ]);
return (
<div className="file-drop" ref={rootRef} onDrop={onDrop} onDragOver={onDragOver} onDragLeave={onDragLeave}>
<div className="panel">
<img className="drop-icon" src="./img/file-improved.svg" alt="file" />
<div className="drop-message">{message}</div>
</div>
</div>
);
}
export default FileDropTarget;

View File

@ -8,7 +8,7 @@ import ElementsUtil from '../require/elements-util';
import ReactHelper from '../require/react-helper';
interface OverlayProps {
childRootRef: RefObject<HTMLElement>; // clicks outside this ref will close the overlay
childRootRef?: RefObject<HTMLElement>; // clicks outside this ref will close the overlay
close?: () => void;
children: React.ReactNode;
}
@ -24,7 +24,9 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
}
}, []);
ReactHelper.useCloseWhenClickedOutsideEffect(childRootRef, () => { removeSelf(); });
if (childRootRef) {
ReactHelper.useCloseWhenClickedOutsideEffect(childRootRef, () => { removeSelf(); });
}
const keyDownHandler = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {

View File

@ -83,4 +83,4 @@ export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, gui
contextElement.parentElement.removeChild(contextElement);
}
});
}
}

View File

@ -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;
}

View File

@ -30,7 +30,7 @@ const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps)
return (
<div className="content resource" onClick={callable}>
<img className="icon" src="./img/file-icon.png" alt="file" />
<img className="icon" src="./img/file-improved.svg" alt="file" />
<div className="text">
<div className="filename">{resourceName}</div>
<div className={shaking ? 'download-status shaking-horizontal' : 'download-status'}>{text}</div>

View File

@ -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;

View File

@ -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 ];
}
}
static useDocumentDropTarget(
message: string,
setBuff: Dispatch<SetStateAction<Buffer | null>>,
setName: Dispatch<SetStateAction<string | null>>
): [ dropTarget: ReactNode ] {
const [ dragEnabled, setDragEnabled ] = useState<boolean>(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 (
<FileDropTarget
close={closeDragOverlay} message={message}
setBuff={setBuff} setName={setName}
/>
);
}, [ dragEnabled, message, setBuff, setName ]);
return [ dropTarget ];
}
}

View File

@ -24,12 +24,12 @@ const AttachmentPreview: FC<AttachmentPreviewProps> = (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<SendMessageProps> = (props: SendMessageProps) => {
setAttachmentName(null);
}, []);
const [ attachmentDropTarget ] = ReactHelper.useDocumentDropTarget('Upload to #' + channel.name, setAttachmentBuff, setAttachmentName);
const attachmentPreview = useMemo(() => {
if (!attachmentBuff || !attachmentName) return null;
return <AttachmentPreview attachmentBuff={attachmentBuff} attachmentName={attachmentName} remove={removeAttachment} />
@ -136,6 +138,7 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
/>
</div>
</div>
{attachmentDropTarget}
</div>
);
}

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="12.235"
height="16"
viewBox="0 0 12.235 16"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
sodipodi:docname="file-improved.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
units="px"
width="16px"
inkscape:zoom="45.254834"
inkscape:cx="9.6674755"
inkscape:cy="9.7779609"
inkscape:window-width="2560"
inkscape:window-height="1375"
inkscape:window-x="2560"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
id="linearGradient3040"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3038" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g5768"
transform="matrix(0.94115305,0,0,0.94117712,-1.4117296,0.47058348)">
<path
id="rect924"
style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="M 2,0 V 16 H 14 V 4.6660156 L 9.3339844,0 Z" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.08627;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect5337"
width="0.5"
height="5.2157302"
x="8.8339844"
y="-0.077339806" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.35848;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect5581"
width="5.3057127"
height="0.5"
x="8.8339844"
y="4.6383905" />
</g>
<g
id="g6420"
transform="translate(-1.8374567,0.29916521)">
<rect
style="fill:#999999;fill-opacity:1;stroke:none;stroke-width:1.333;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect5984"
width="8.8609314"
height="1.5026019"
x="3.5244858"
y="6.279469"
rx="0.30000001"
ry="0.30000001" />
<rect
style="fill:#999999;fill-opacity:1;stroke:none;stroke-width:1.333;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect5984-7"
width="8.8609314"
height="1.5026019"
x="3.5244858"
y="9.1169968"
rx="0.30000001"
ry="0.30000001" />
<rect
style="fill:#999999;fill-opacity:1;stroke:none;stroke-width:1.333;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect5984-5"
width="8.8609314"
height="1.5026019"
x="3.5244858"
y="11.954525"
rx="0.30000001"
ry="0.30000001" />
<rect
style="fill:#999999;fill-opacity:1;stroke:none;stroke-width:1.333;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect6232"
width="3"
height="3"
x="3.5244858"
y="1.9445436"
rx="0.30000001"
ry="0.30000001" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -74,6 +74,7 @@ $borderRadius: 8px;
}
.send-message-input-row {
width: 100%;
display: flex;
align-items: flex-start;