react text-based send message input
This commit is contained in:
parent
26139af512
commit
ccba0f1057
4471
package-lock.json
generated
4471
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,8 +12,10 @@
|
||||
"colors": "^1.4.0",
|
||||
"file-type": "^16.5.3",
|
||||
"image-size": "^1.0.0",
|
||||
"markdown": "^0.5.0",
|
||||
"moment": "^2.29.1",
|
||||
"pg": "^8.7.1",
|
||||
"react-contenteditable": "^3.3.6",
|
||||
"sass": "^1.43.4",
|
||||
"sharp": "^0.29.2",
|
||||
"socket.io": "^4.3.1",
|
||||
|
@ -15,4 +15,4 @@
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import ConnectionInfo from './sections/connection-info';
|
||||
import GuildTitle from './sections/guild-title';
|
||||
import UI from '../ui';
|
||||
import ChannelList from './lists/channel-list';
|
||||
import SendMessage from './sections/send-message';
|
||||
|
||||
export function mountBaseComponents() {
|
||||
// guild-list
|
||||
@ -37,10 +38,13 @@ export function mountGuildComponents(q: Q, ui: UI, guild: CombinedGuild) {
|
||||
export function mountGuildChannelComponents(q: Q, guild: CombinedGuild, channel: Channel) {
|
||||
// channel-title pieces
|
||||
ElementsUtil.unmountReactComponent(q.$('.channel-title-anchor'));
|
||||
ElementsUtil.mountReactComponent(q.$('.channel-title-anchor'), <ChannelTitle channel={channel} />)
|
||||
ElementsUtil.mountReactComponent(q.$('.channel-title-anchor'), <ChannelTitle channel={channel} />);
|
||||
|
||||
// message-list
|
||||
// Up Next: Figure out why this is empty
|
||||
ElementsUtil.unmountReactComponent(q.$('.message-list-anchor'));
|
||||
ElementsUtil.mountReactComponent(q.$('.message-list-anchor'), <MessageList guild={guild} channel={channel} />)
|
||||
ElementsUtil.mountReactComponent(q.$('.message-list-anchor'), <MessageList guild={guild} channel={channel} />);
|
||||
|
||||
// send-message
|
||||
ElementsUtil.unmountReactComponent(q.$('.send-message-input-wrapper-anchor'));
|
||||
ElementsUtil.mountReactComponent(q.$('.send-message-input-wrapper-anchor'), <SendMessage guild={guild} channel={channel} />);
|
||||
}
|
||||
|
@ -245,6 +245,13 @@ export default class BaseElements {
|
||||
Z` }
|
||||
}
|
||||
|
||||
static SEND_MESSAGE_ATTACH = (
|
||||
// Yoinked directly from Discord
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path 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" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf {
|
||||
const element = ReactHelper.createElementFromJSX(
|
||||
<div className="context">
|
||||
|
76
src/client/webapp/elements/sections/send-message.tsx
Normal file
76
src/client/webapp/elements/sections/send-message.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
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, 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';
|
||||
|
||||
export interface SendMessageProps {
|
||||
guild: CombinedGuild;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
|
||||
const { guild, channel } = props;
|
||||
|
||||
const contentEditableRef = useRef<HTMLDivElement>(null);
|
||||
const [ text, setText ] = useState<string>('');
|
||||
const [ enabled, setEnabled ] = useState<boolean>(true);
|
||||
|
||||
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 (contentEditableRef.current) contentEditableRef.current.innerText = '';
|
||||
setEnabled(true);
|
||||
if (contentEditableRef.current) contentEditableRef.current.focus();
|
||||
},
|
||||
[ enabled, guild, channel, text ]
|
||||
);
|
||||
|
||||
const onTextInput = useCallback((e: FormEvent<HTMLDivElement>) => {
|
||||
setText(e.currentTarget.innerText);
|
||||
}, []);
|
||||
|
||||
const onTextKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendCallable();
|
||||
}
|
||||
}, [ text ]);
|
||||
|
||||
// WARNING: The types on this are funky because of react's lack of explicit support for 'plaintext-only'
|
||||
const contentEditableType = useMemo(() => {
|
||||
if (enabled) return 'plaintext-only' as 'true'; // WARNING: we have to do this to trick react's bad typings
|
||||
else return 'false';
|
||||
}, [ enabled ]);
|
||||
|
||||
const textInputClassName = useMemo(() => {
|
||||
return enabled ? 'text-input' : 'text-input disabled';
|
||||
}, [ enabled ]);
|
||||
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SendMessage;
|
@ -54,6 +54,7 @@
|
||||
<div id="channel-content">
|
||||
<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">
|
||||
|
@ -3,6 +3,48 @@
|
||||
$scrollbarBottom: 4px;
|
||||
$borderRadius: 8px;
|
||||
|
||||
.send-message-input-wrapper {
|
||||
position: relative;
|
||||
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);
|
||||
color: $text-normal;
|
||||
background-color: $channeltextarea-background;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
.resource-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#channel-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -16,6 +58,7 @@ $borderRadius: 8px;
|
||||
}
|
||||
|
||||
#channel-feed-input-wrapper {
|
||||
display: none; // for testing
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
@ -34,3 +34,7 @@ body {
|
||||
cursor: text;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
[contenteditable=plaintext-only]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user