remove non-react send message pieces

This commit is contained in:
Michael Peters 2021-12-26 22:09:55 -06:00
parent ccba0f1057
commit f39c3f0a51
15 changed files with 386 additions and 284 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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