drop target for send message component
This commit is contained in:
parent
f39c3f0a51
commit
15c92c2eac
BIN
figma/server-settings.fig
Normal file
BIN
figma/server-settings.fig
Normal file
Binary file not shown.
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/client/webapp/elements/components/file-drop-target.tsx
Normal file
66
src/client/webapp/elements/components/file-drop-target.tsx
Normal 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;
|
@ -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') {
|
||||
|
@ -83,4 +83,4 @@ export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, gui
|
||||
contextElement.parentElement.removeChild(contextElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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 ];
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
115
src/client/webapp/img/file-improved.svg
Normal file
115
src/client/webapp/img/file-improved.svg
Normal 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 |
@ -74,6 +74,7 @@ $borderRadius: 8px;
|
||||
}
|
||||
|
||||
.send-message-input-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user