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",
|
"description": "Generally you'll want this when you create a TSX file",
|
||||||
"prefix": [ "tsx" ],
|
"prefix": [ "tsx" ],
|
||||||
"body": [
|
"body": [
|
||||||
"import React, { FC } from 'react'",
|
"import React, { FC } from 'react';",
|
||||||
"",
|
"",
|
||||||
"export interface ${1:Element}Props {",
|
"export interface ${1:Element}Props {",
|
||||||
"\ttext: string;",
|
"\ttext: string;",
|
||||||
|
4
makefile
4
makefile
@ -22,8 +22,8 @@ test:
|
|||||||
node ./node_modules/ts-jest/cli.js
|
node ./node_modules/ts-jest/cli.js
|
||||||
|
|
||||||
move:
|
move:
|
||||||
cp -r ./src/client/webapp/font ./dist/client/webapp/font
|
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/img/* ./dist/client/webapp/img
|
||||||
cp ./src/client/webapp/index.html ./dist/client/webapp/index.html
|
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/scripts/resources ./dist/server/scripts/resources
|
||||||
cp -r ./src/server/ssl ./dist/server/ssl
|
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 {
|
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;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
||||||
const { childRootRef, children } = props;
|
const { childRootRef, close, children } = props;
|
||||||
|
|
||||||
const removeSelf = useCallback(() => {
|
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(); });
|
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>
|
</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 {
|
static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf {
|
||||||
const element = ReactHelper.createElementFromJSX(
|
const element = ReactHelper.createElementFromJSX(
|
||||||
<div className="context">
|
<div className="context">
|
||||||
|
@ -3,11 +3,48 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
|||||||
import Logger from '../../../../logger/logger';
|
import Logger from '../../../../logger/logger';
|
||||||
const LOG = Logger.create(__filename, electronConsole);
|
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 { Channel } from '../../data-types';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
import BaseElements from '../require/base-elements';
|
import BaseElements from '../require/base-elements';
|
||||||
import ReactHelper from '../require/react-helper';
|
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 {
|
export interface SendMessageProps {
|
||||||
guild: CombinedGuild;
|
guild: CombinedGuild;
|
||||||
@ -21,20 +58,31 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
|||||||
const [ text, setText ] = useState<string>('');
|
const [ text, setText ] = useState<string>('');
|
||||||
const [ enabled, setEnabled ] = useState<boolean>(true);
|
const [ enabled, setEnabled ] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const [ attachmentBuff, setAttachmentBuff ] = useState<Buffer | null>(null);
|
||||||
|
const [ attachmentName, setAttachmentName ] = useState<string | null>(null);
|
||||||
|
|
||||||
const [ sendCallable ] = ReactHelper.useAsyncVoidCallback(
|
const [ sendCallable ] = ReactHelper.useAsyncVoidCallback(
|
||||||
async (isMounted: RefObject<boolean>) => {
|
async (isMounted: RefObject<boolean>) => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
|
|
||||||
// TODO: Deal with errors (toasts are probably the best way)
|
// TODO: Deal with errors (toasts are probably the best way)
|
||||||
await guild.requestSendMessage(channel.id, text);
|
if (attachmentBuff && attachmentName) {
|
||||||
if (!isMounted.current) return;
|
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 = '';
|
if (contentEditableRef.current) contentEditableRef.current.innerText = '';
|
||||||
setEnabled(true);
|
setEnabled(true);
|
||||||
if (contentEditableRef.current) contentEditableRef.current.focus();
|
if (contentEditableRef.current) contentEditableRef.current.focus();
|
||||||
},
|
},
|
||||||
[ enabled, guild, channel, text ]
|
[ enabled, guild, channel, text, attachmentBuff, attachmentName ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTextInput = useCallback((e: FormEvent<HTMLDivElement>) => {
|
const onTextInput = useCallback((e: FormEvent<HTMLDivElement>) => {
|
||||||
@ -46,7 +94,18 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendCallable();
|
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'
|
// WARNING: The types on this are funky because of react's lack of explicit support for 'plaintext-only'
|
||||||
const contentEditableType = useMemo(() => {
|
const contentEditableType = useMemo(() => {
|
||||||
@ -61,13 +120,21 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="send-message-input-wrapper">
|
<div className="send-message-input-wrapper">
|
||||||
<div className="send-message-input">
|
<div className="send-message-input">
|
||||||
<div className="resource-input-button">{BaseElements.SEND_MESSAGE_ATTACH}</div>
|
{attachmentPreview}
|
||||||
<div
|
{attachmentPreview && <div className="divider" />}
|
||||||
ref={contentEditableRef}
|
<div className="send-message-input-row">
|
||||||
className={textInputClassName} contentEditable={contentEditableType}
|
<div className="attachment-input-button">
|
||||||
data-placeholder={`Message #${channel.name}`}
|
<FileInput setBuff={setAttachmentBuff} setName={setAttachmentName}>
|
||||||
onInput={onTextInput} onKeyDown={onTextKeyDown}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -365,6 +365,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
async requestSendMessage(channelId: string, text: string): Promise<void> {
|
async requestSendMessage(channelId: string, text: string): Promise<void> {
|
||||||
await this.socketGuild.requestSendMessage(channelId, text);
|
await this.socketGuild.requestSendMessage(channelId, text);
|
||||||
}
|
}
|
||||||
|
// TODO: Change to "withAttachment"
|
||||||
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
|
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
|
||||||
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
|
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> {
|
async requestSetAvatar(avatar: Buffer): Promise<void> {
|
||||||
await this.socketGuild.requestSetAvatar(avatar);
|
await this.socketGuild.requestSetAvatar(avatar);
|
||||||
}
|
}
|
||||||
// TODO: Rename Server -> Guild
|
|
||||||
async requestSetGuildName(guildName: string): Promise<void> {
|
async requestSetGuildName(guildName: string): Promise<void> {
|
||||||
await this.socketGuild.requestSetGuildName(guildName);
|
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 id="channel-feed-wrapper">
|
||||||
<div class="message-list-anchor"></div>
|
<div class="message-list-anchor"></div>
|
||||||
<div class="send-message-input-wrapper-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>
|
||||||
<div class="member-list-anchor"></div>
|
<div class="member-list-anchor"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,6 @@ import Actions from './actions';
|
|||||||
import { Changes, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
|
import { Changes, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
|
||||||
import Q from './q-module';
|
import Q from './q-module';
|
||||||
import bindWindowButtonEvents from './elements/events-window-buttons';
|
import bindWindowButtonEvents from './elements/events-window-buttons';
|
||||||
import bindTextInputEvents from './elements/events-text-input';
|
|
||||||
import bindAddGuildEvents from './elements/events-add-guild';
|
import bindAddGuildEvents from './elements/events-add-guild';
|
||||||
import PersonalDB from './personal-db';
|
import PersonalDB from './personal-db';
|
||||||
import MessageRAMCache from './message-ram-cache';
|
import MessageRAMCache from './message-ram-cache';
|
||||||
@ -71,7 +70,6 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
LOG.silly('action classes initialized');
|
LOG.silly('action classes initialized');
|
||||||
|
|
||||||
bindWindowButtonEvents(q);
|
bindWindowButtonEvents(q);
|
||||||
bindTextInputEvents(document, q, ui);
|
|
||||||
bindAddGuildEvents(document, q, ui, guildsManager);
|
bindAddGuildEvents(document, q, ui, guildsManager);
|
||||||
|
|
||||||
LOG.silly('events bound');
|
LOG.silly('events bound');
|
||||||
|
@ -8,40 +8,98 @@ $borderRadius: 8px;
|
|||||||
height: 0;
|
height: 0;
|
||||||
|
|
||||||
.send-message-input {
|
.send-message-input {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 0 16px 16px 16px;
|
margin: 0 16px 16px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
color: $text-normal;
|
color: $text-normal;
|
||||||
background-color: $channeltextarea-background;
|
background-color: $channeltextarea-background;
|
||||||
border-radius: $borderRadius;
|
border-radius: $borderRadius;
|
||||||
|
|
||||||
.resource-input-button {
|
.attachment-preview {
|
||||||
cursor: pointer;
|
position: relative;
|
||||||
margin: 12px;
|
margin: 16px 16px 0 16px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: calc(100% - 32px);
|
||||||
|
background-color: $background-secondary;
|
||||||
|
|
||||||
svg {
|
img.preview {
|
||||||
width: 24px;
|
max-width: 128px;
|
||||||
height: 24px;
|
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 {
|
.send-message-input-row {
|
||||||
flex: 1;
|
display: flex;
|
||||||
padding: 12px 12px 12px 0;
|
align-items: flex-start;
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
white-space: pre;
|
|
||||||
|
|
||||||
&.disabled {
|
.attachment-input-button {
|
||||||
color: $text-sending;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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> {
|
public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise<void> {
|
||||||
await this.lockChannels(guild, () => {
|
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;
|
this.activeChannel = channel;
|
||||||
mountGuildChannelComponents(this.q, guild, channel);
|
mountGuildChannelComponents(this.q, guild, channel);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user