remove non-react send message pieces
This commit is contained in:
parent
ccba0f1057
commit
f39c3f0a51
2
.vscode/cordis.code-snippets
vendored
2
.vscode/cordis.code-snippets
vendored
@ -19,7 +19,7 @@
|
||||
"description": "Generally you'll want this when you create a TSX file",
|
||||
"prefix": [ "tsx" ],
|
||||
"body": [
|
||||
"import React, { FC } from 'react'",
|
||||
"import React, { FC } from 'react';",
|
||||
"",
|
||||
"export interface ${1:Element}Props {",
|
||||
"\ttext: string;",
|
||||
|
4
makefile
4
makefile
@ -22,8 +22,8 @@ test:
|
||||
node ./node_modules/ts-jest/cli.js
|
||||
|
||||
move:
|
||||
cp -r ./src/client/webapp/font ./dist/client/webapp/font
|
||||
cp -r ./src/client/webapp/img ./dist/client/webapp/img
|
||||
cp -r ./src/client/webapp/font/* ./dist/client/webapp/font
|
||||
cp -r ./src/client/webapp/img/* ./dist/client/webapp/img
|
||||
cp ./src/client/webapp/index.html ./dist/client/webapp/index.html
|
||||
cp -r ./src/server/scripts/resources ./dist/server/scripts/resources
|
||||
cp -r ./src/server/ssl ./dist/server/ssl
|
||||
|
39
src/client/webapp/elements/components/input-file.tsx
Normal file
39
src/client/webapp/elements/components/input-file.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
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, { FC, ReactNode } from 'react';
|
||||
|
||||
interface FileInputProps {
|
||||
setBuff: React.Dispatch<React.SetStateAction<Buffer | null>>;
|
||||
setName: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const FileInput: FC<FileInputProps> = (props: FileInputProps) => {
|
||||
const { setBuff, setName, children } = props;
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
LOG.debug('no files');
|
||||
return;
|
||||
}
|
||||
const file = files[0] as File;
|
||||
const buff = Buffer.from(await file.arrayBuffer());
|
||||
setBuff(buff);
|
||||
setName(file.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-edit-input-react">
|
||||
<label className="label">
|
||||
{children}
|
||||
<input type="file" onChange={handleFileChange}></input>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileInput;
|
@ -9,13 +9,19 @@ import ReactHelper from '../require/react-helper';
|
||||
|
||||
interface OverlayProps {
|
||||
childRootRef: RefObject<HTMLElement>; // clicks outside this ref will close the overlay
|
||||
close?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
||||
const { childRootRef, children } = props;
|
||||
const { childRootRef, close, children } = props;
|
||||
|
||||
const removeSelf = useCallback(() => {
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
if (close) {
|
||||
close();
|
||||
} else {
|
||||
LOG.warn('closing react overlay with ElementsUtil (deprecated)');
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
}
|
||||
}, []);
|
||||
|
||||
ReactHelper.useCloseWhenClickedOutsideEffect(childRootRef, () => { removeSelf(); });
|
||||
|
@ -1,146 +0,0 @@
|
||||
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 ElementsUtil from './require/elements-util.js';
|
||||
import Globals from '../globals';
|
||||
|
||||
import { Channel } from '../data-types';
|
||||
import Q from '../q-module';
|
||||
import UI from '../ui';
|
||||
import createUploadOverlayFromPath from './overlay-upload-path';
|
||||
import createUploadOverlayFromDataTransferItem from './overlay-upload-datatransfer';
|
||||
import createUploadDropTarget from './overlay-upload-drop-target';
|
||||
import CombinedGuild from '../guild-combined';
|
||||
|
||||
export default function bindTextInputEvents(document: Document, q: Q, ui: UI): void {
|
||||
// Send Current Channel Messages
|
||||
let sendingMessage = false;
|
||||
async function sendCurrentTextMessage() {
|
||||
if (sendingMessage) return;
|
||||
if (ui.activeGuild === null) return;
|
||||
if (ui.activeChannel === null) return;
|
||||
|
||||
const text = q.$('#text-input').innerText.trim(); // trimming is not done server-side, just a client-side 'feature'
|
||||
if (text == '') return;
|
||||
|
||||
sendingMessage = true;
|
||||
|
||||
const guild = ui.activeGuild as CombinedGuild;
|
||||
const channel = ui.activeChannel as Channel;
|
||||
|
||||
if (!guild.isSocketVerified()) {
|
||||
LOG.warn('client attempted to send message while not verified');
|
||||
q.$('#send-error').innerText = 'Not Connected to Server';
|
||||
await ElementsUtil.shakeElement(q.$('#send-error'), 400);
|
||||
sendingMessage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > Globals.MAX_TEXT_MESSAGE_LENGTH) {
|
||||
LOG.warn('skipping sending oversized message: ' + text.length + ' > ' + Globals.MAX_TEXT_MESSAGE_LENGTH + ' characters');
|
||||
q.$('#send-error').innerText = 'Message too long: ' + text.length + ' > ' + Globals.MAX_TEXT_MESSAGE_LENGTH + ' characters';
|
||||
await ElementsUtil.shakeElement(q.$('#send-error'), 400);
|
||||
sendingMessage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await ui.lockMessages(guild, channel, async () => {
|
||||
q.$('#text-input').removeAttribute('contenteditable');
|
||||
q.$('#text-input').classList.add('sending');
|
||||
try {
|
||||
await guild.requestSendMessage(channel.id, text);
|
||||
|
||||
q.$('#send-error').innerText = '';
|
||||
q.$('#text-input').innerText = '';
|
||||
} catch (e) {
|
||||
LOG.error('Error sending message', e);
|
||||
q.$('#send-error').innerText = 'Error sending message';
|
||||
await ElementsUtil.shakeElement(q.$('#send-error'), 400);
|
||||
}
|
||||
q.$('#text-input').classList.remove('sending');
|
||||
q.$('#text-input').setAttribute('contenteditable', 'plaintext-only');
|
||||
q.$('#text-input').focus();
|
||||
});
|
||||
|
||||
sendingMessage = false;
|
||||
}
|
||||
|
||||
q.$('#text-input').addEventListener('keydown', async (e) => {
|
||||
if (!sendingMessage) {
|
||||
q.$('#send-error').innerText = ''; // clear out the sending error if the message changes
|
||||
}
|
||||
if (e.key == 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
await sendCurrentTextMessage();
|
||||
}
|
||||
});
|
||||
|
||||
q.$('#text-input').addEventListener('keyup', (e) => {
|
||||
if (e.key == 'Backspace') {
|
||||
if (q.$('#text-input').innerText == '\n') { // sometimes, a \n gets left behind
|
||||
q.$('#text-input').innerText = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
q.$('#send-error').addEventListener('click', () => {
|
||||
q.$('#send-error').innerText = '';
|
||||
});
|
||||
|
||||
// Open resource select dialog when resource-input-button is clicked
|
||||
let selectingResources = false;
|
||||
q.$('#resource-input-button').addEventListener('click', async () => {
|
||||
if (ui.activeGuild === null) return;
|
||||
if (ui.activeChannel === null) return;
|
||||
|
||||
if (selectingResources) {
|
||||
return;
|
||||
}
|
||||
selectingResources = true;
|
||||
const result = await electronRemote.dialog.showOpenDialog({
|
||||
title: 'Select Resource',
|
||||
defaultPath: 'D:\\development\\cordis\\client-server\\server\\data', // TODO: not hardcoded
|
||||
properties: [ 'openFile' ]
|
||||
});
|
||||
// TODO: multiple files do consecutive overlays?
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
const element = createUploadOverlayFromPath(document, ui.activeGuild, ui.activeChannel, result.filePaths[0] as string);
|
||||
document.body.appendChild(element);
|
||||
q.$$$(element, '.text-input').focus();
|
||||
}
|
||||
selectingResources = false;
|
||||
});
|
||||
|
||||
// Open upload resource dialog when an image is pasted
|
||||
window.addEventListener('paste', (e) => {
|
||||
if (ui.activeGuild === null) return;
|
||||
if (ui.activeChannel === null) return;
|
||||
|
||||
let fileTransferItem: DataTransferItem | null = null;
|
||||
for (const item of (e as ClipboardEvent).clipboardData?.items ?? []) {
|
||||
if (item.kind == 'file') {
|
||||
e.preventDefault(); // don't continue the paste
|
||||
fileTransferItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fileTransferItem) {
|
||||
const element = createUploadOverlayFromDataTransferItem(document, ui.activeGuild, ui.activeChannel, fileTransferItem);
|
||||
document.body.appendChild(element);
|
||||
q.$$$(element, '.text-input').focus();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: drag+drop new server files?
|
||||
document.addEventListener('dragenter', () => {
|
||||
if (ui.activeGuild === null) return;
|
||||
if (ui.activeChannel === null) return;
|
||||
|
||||
if (q.$('.overlay .drop-target')) return;
|
||||
const element = createUploadDropTarget(document, q, ui.activeGuild, ui.activeChannel);
|
||||
if (!element) return;
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
}
|
@ -252,6 +252,24 @@ export default class BaseElements {
|
||||
</svg>
|
||||
);
|
||||
|
||||
static REMOVE_ATTACHMENT_X = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<circle fill="currentColor" cx="8" cy="8" r="8" />
|
||||
<rect fill="#ffffff"
|
||||
width="2"
|
||||
height="10"
|
||||
x="-1"
|
||||
y="6.3137083"
|
||||
transform="rotate(-45)" />
|
||||
<rect fill="#ffffff"
|
||||
width="2"
|
||||
height="10"
|
||||
x="-12.313708"
|
||||
y="-5"
|
||||
transform="rotate(-135)" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf {
|
||||
const element = ReactHelper.createElementFromJSX(
|
||||
<div className="context">
|
||||
|
@ -3,11 +3,48 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { FC, FormEvent, KeyboardEvent, RefObject, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import React, { FC, FormEvent, KeyboardEvent, RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Channel } from '../../data-types';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import ReactHelper from '../require/react-helper';
|
||||
import * as FileType from 'file-type';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import FileInput from '../components/input-file';
|
||||
|
||||
export interface AttachmentPreviewProps {
|
||||
attachmentBuff: Buffer;
|
||||
attachmentName: string;
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
const AttachmentPreview: FC<AttachmentPreviewProps> = (props: AttachmentPreviewProps) => {
|
||||
const { attachmentBuff, attachmentName, remove } = props;
|
||||
|
||||
const [ attachmentImgSrc ] = ReactHelper.useOneTimeAsyncAction(
|
||||
async () => {
|
||||
const type = await FileType.fromBuffer(attachmentBuff);
|
||||
if (!type) return './img/file-icon.png';
|
||||
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';
|
||||
}
|
||||
},
|
||||
'./img/loading.svg',
|
||||
[ attachmentBuff ]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="attachment-preview">
|
||||
<img className="preview" src={attachmentImgSrc} alt={attachmentName} />
|
||||
<div className="name">{attachmentName}</div>
|
||||
<div className="remove" onClick={remove}>{BaseElements.REMOVE_ATTACHMENT_X}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export interface SendMessageProps {
|
||||
guild: CombinedGuild;
|
||||
@ -21,20 +58,31 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
||||
const [ text, setText ] = useState<string>('');
|
||||
const [ enabled, setEnabled ] = useState<boolean>(true);
|
||||
|
||||
const [ attachmentBuff, setAttachmentBuff ] = useState<Buffer | null>(null);
|
||||
const [ attachmentName, setAttachmentName ] = useState<string | null>(null);
|
||||
|
||||
const [ sendCallable ] = ReactHelper.useAsyncVoidCallback(
|
||||
async (isMounted: RefObject<boolean>) => {
|
||||
if (!enabled) return;
|
||||
setEnabled(false);
|
||||
|
||||
// TODO: Deal with errors (toasts are probably the best way)
|
||||
await guild.requestSendMessage(channel.id, text);
|
||||
if (!isMounted.current) return;
|
||||
if (attachmentBuff && attachmentName) {
|
||||
await guild.requestSendMessageWithResource(channel.id, text === '' ? null : text, attachmentBuff, attachmentName)
|
||||
if (!isMounted.current) return;
|
||||
setAttachmentBuff(null);
|
||||
setAttachmentName(null);
|
||||
} else if (text !== '') {
|
||||
await guild.requestSendMessage(channel.id, text);
|
||||
if (!isMounted.current) return;
|
||||
}
|
||||
|
||||
setText('');
|
||||
if (contentEditableRef.current) contentEditableRef.current.innerText = '';
|
||||
setEnabled(true);
|
||||
if (contentEditableRef.current) contentEditableRef.current.focus();
|
||||
},
|
||||
[ enabled, guild, channel, text ]
|
||||
[ enabled, guild, channel, text, attachmentBuff, attachmentName ]
|
||||
);
|
||||
|
||||
const onTextInput = useCallback((e: FormEvent<HTMLDivElement>) => {
|
||||
@ -46,7 +94,18 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
||||
e.preventDefault();
|
||||
sendCallable();
|
||||
}
|
||||
}, [ text ]);
|
||||
}, [ sendCallable ]);
|
||||
|
||||
|
||||
const removeAttachment = useCallback(() => {
|
||||
setAttachmentBuff(null);
|
||||
setAttachmentName(null);
|
||||
}, []);
|
||||
|
||||
const attachmentPreview = useMemo(() => {
|
||||
if (!attachmentBuff || !attachmentName) return null;
|
||||
return <AttachmentPreview attachmentBuff={attachmentBuff} attachmentName={attachmentName} remove={removeAttachment} />
|
||||
}, [ attachmentBuff, attachmentName ]);
|
||||
|
||||
// WARNING: The types on this are funky because of react's lack of explicit support for 'plaintext-only'
|
||||
const contentEditableType = useMemo(() => {
|
||||
@ -61,13 +120,21 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
||||
return (
|
||||
<div className="send-message-input-wrapper">
|
||||
<div className="send-message-input">
|
||||
<div className="resource-input-button">{BaseElements.SEND_MESSAGE_ATTACH}</div>
|
||||
<div
|
||||
ref={contentEditableRef}
|
||||
className={textInputClassName} contentEditable={contentEditableType}
|
||||
data-placeholder={`Message #${channel.name}`}
|
||||
onInput={onTextInput} onKeyDown={onTextKeyDown}
|
||||
/>
|
||||
{attachmentPreview}
|
||||
{attachmentPreview && <div className="divider" />}
|
||||
<div className="send-message-input-row">
|
||||
<div className="attachment-input-button">
|
||||
<FileInput setBuff={setAttachmentBuff} setName={setAttachmentName}>
|
||||
{BaseElements.SEND_MESSAGE_ATTACH}
|
||||
</FileInput>
|
||||
</div>
|
||||
<div
|
||||
ref={contentEditableRef}
|
||||
className={textInputClassName} contentEditable={contentEditableType}
|
||||
data-placeholder={`Message #${channel.name}`}
|
||||
onInput={onTextInput} onKeyDown={onTextKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -365,6 +365,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
async requestSendMessage(channelId: string, text: string): Promise<void> {
|
||||
await this.socketGuild.requestSendMessage(channelId, text);
|
||||
}
|
||||
// TODO: Change to "withAttachment"
|
||||
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
|
||||
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
|
||||
}
|
||||
@ -377,7 +378,6 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
async requestSetAvatar(avatar: Buffer): Promise<void> {
|
||||
await this.socketGuild.requestSetAvatar(avatar);
|
||||
}
|
||||
// TODO: Rename Server -> Guild
|
||||
async requestSetGuildName(guildName: string): Promise<void> {
|
||||
await this.socketGuild.requestSetGuildName(guildName);
|
||||
}
|
||||
|
66
src/client/webapp/img/close-16x16.svg
Normal file
66
src/client/webapp/img/close-16x16.svg
Normal file
@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="close-16x16.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="11.67831"
|
||||
inkscape:cy="12.518"
|
||||
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" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<circle
|
||||
style="fill:#cc302b;fill-opacity:1;stroke-width:3.0187"
|
||||
id="path2348"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="12" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.5"
|
||||
id="rect2686"
|
||||
width="3"
|
||||
height="15"
|
||||
x="-1.5000002"
|
||||
y="9.4705629"
|
||||
transform="rotate(-45)" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.5"
|
||||
id="rect2686-6"
|
||||
width="3"
|
||||
height="15"
|
||||
x="-18.470562"
|
||||
y="-7.5"
|
||||
transform="rotate(-135)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
48
src/client/webapp/img/x-10x10.svg
Normal file
48
src/client/webapp/img/x-10x10.svg
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="x-10x10.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="49.773737"
|
||||
inkscape:cx="6.7806844"
|
||||
inkscape:cy="7.9057757"
|
||||
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" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path1928"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.04939"
|
||||
d="m 1.4644661,1.4644661 a 5,5 0 0 0 0,7.0710678 5,5 0 0 0 7.0710678,0 5,5 0 0 0 0,-7.0710678 5,5 0 0 0 -7.0710678,0 z M 2.1715729,3.5857864 3.5857864,2.1715729 5,3.5857864 6.4142136,2.1715729 7.8284271,3.5857864 6.4142136,5 7.8284271,6.4142136 6.4142136,7.8284271 5,6.4142136 3.5857864,7.8284271 2.1715729,6.4142136 3.5857864,5 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
46
src/client/webapp/img/x-16x16.svg
Normal file
46
src/client/webapp/img/x-16x16.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
sodipodi:docname="x-punchout.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"
|
||||
inkscape:zoom="54.5625"
|
||||
inkscape:cx="3.0790378"
|
||||
inkscape:cy="8"
|
||||
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" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path53"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd"
|
||||
d="m 13.656854,2.3431458 a 8,8 0 0 0 -11.3137082,0 8,8 0 0 0 0,11.3137082 8,8 0 0 0 11.3137082,0 8,8 0 0 0 0,-11.3137082 z M 10.828427,3.7573593 12.242641,5.1715729 9.4142136,8 12.242641,10.828427 10.828427,12.242641 8,9.4142136 5.1715729,12.242641 3.7573593,10.828427 6.5857864,8 3.7573593,5.1715729 5.1715729,3.7573593 8,6.5857864 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -55,20 +55,6 @@
|
||||
<div id="channel-feed-wrapper">
|
||||
<div class="message-list-anchor"></div>
|
||||
<div class="send-message-input-wrapper-anchor"></div>
|
||||
<div id="channel-feed-input-wrapper">
|
||||
<div id="channel-feed-input">
|
||||
<div id="input-bar">
|
||||
<div id="resource-input-button">
|
||||
<!-- Yoinked Directly from Discord -->
|
||||
<svg viewBox="0 0 24 24"><path class="attachButtonPlus-jWVFah" fill="currentColor" d="M12 2.00098C6.486 2.00098 2 6.48698 2 12.001C2 17.515 6.486 22.001 12 22.001C17.514 22.001 22 17.515 22 12.001C22 6.48698 17.514 2.00098 12 2.00098ZM17 13.001H13V17.001H11V13.001H7V11.001H11V7.00098H13V11.001H17V13.001Z"></path></svg>
|
||||
</div>
|
||||
<div id="text-input" contenteditable="plaintext-only"></div>
|
||||
</div>
|
||||
<div id="error-bar">
|
||||
<div id="send-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-list-anchor"></div>
|
||||
</div>
|
||||
|
@ -17,7 +17,6 @@ import Actions from './actions';
|
||||
import { Changes, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
|
||||
import Q from './q-module';
|
||||
import bindWindowButtonEvents from './elements/events-window-buttons';
|
||||
import bindTextInputEvents from './elements/events-text-input';
|
||||
import bindAddGuildEvents from './elements/events-add-guild';
|
||||
import PersonalDB from './personal-db';
|
||||
import MessageRAMCache from './message-ram-cache';
|
||||
@ -71,7 +70,6 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
LOG.silly('action classes initialized');
|
||||
|
||||
bindWindowButtonEvents(q);
|
||||
bindTextInputEvents(document, q, ui);
|
||||
bindAddGuildEvents(document, q, ui, guildsManager);
|
||||
|
||||
LOG.silly('events bound');
|
||||
|
@ -8,40 +8,98 @@ $borderRadius: 8px;
|
||||
height: 0;
|
||||
|
||||
.send-message-input {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
margin: 0 16px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 32px);
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-start;
|
||||
|
||||
color: $text-normal;
|
||||
background-color: $channeltextarea-background;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
.resource-input-button {
|
||||
cursor: pointer;
|
||||
margin: 12px;
|
||||
.attachment-preview {
|
||||
position: relative;
|
||||
margin: 16px 16px 0 16px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: calc(100% - 32px);
|
||||
background-color: $background-secondary;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
img.preview {
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: -8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: $background-button-negative;
|
||||
|
||||
&:hover {
|
||||
color: $background-button-negative-hover;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 16px 0 0 0;
|
||||
height: 1px;
|
||||
background-color: $interactive-muted;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
flex: 1;
|
||||
padding: 12px 12px 12px 0;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre;
|
||||
.send-message-input-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&.disabled {
|
||||
color: $text-sending;
|
||||
}
|
||||
}
|
||||
.attachment-input-button {
|
||||
cursor: pointer;
|
||||
margin: 12px;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input {
|
||||
flex: 1;
|
||||
padding: 12px 12px 12px 0;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre;
|
||||
|
||||
&.disabled {
|
||||
color: $text-sending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,84 +114,3 @@ $borderRadius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#channel-feed-input-wrapper {
|
||||
display: none; // for testing
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#channel-feed-input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 16px;
|
||||
margin: 0 16px;
|
||||
width: calc(100% - 32px);
|
||||
display: flex;
|
||||
flex-flow: column-reverse;
|
||||
background-color: $background-primary;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#error-bar {
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#send-error:not(:empty) {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
line-height: 1;
|
||||
border: 1px solid $error;
|
||||
background-color: $background-secondary;
|
||||
color: $header-primary;
|
||||
}
|
||||
|
||||
#input-bar {
|
||||
font-family: Whitney;
|
||||
background-color: $channeltextarea-background;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#resource-input-button {
|
||||
color: $text-normal;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
#text-input {
|
||||
flex: 1;
|
||||
padding: 12px 12px 12px 0;
|
||||
color: $text-normal;
|
||||
overflow-wrap: anywhere;
|
||||
overflow-y: scroll;
|
||||
max-height: 300px;
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.sending {
|
||||
color: $text-sending;
|
||||
}
|
||||
|
||||
&::before {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
|
@ -113,9 +113,6 @@ export default class UI {
|
||||
|
||||
public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise<void> {
|
||||
await this.lockChannels(guild, () => {
|
||||
// Channel Name + Flavor Text Header + Text Input Placeholder
|
||||
this.q.$('#text-input').setAttribute('data-placeholder', 'Message #' + channel.name);
|
||||
|
||||
this.activeChannel = channel;
|
||||
mountGuildChannelComponents(this.q, guild, channel);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user