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",
|
"colors": "^1.4.0",
|
||||||
"file-type": "^16.5.3",
|
"file-type": "^16.5.3",
|
||||||
"image-size": "^1.0.0",
|
"image-size": "^1.0.0",
|
||||||
|
"markdown": "^0.5.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
|
"react-contenteditable": "^3.3.6",
|
||||||
"sass": "^1.43.4",
|
"sass": "^1.43.4",
|
||||||
"sharp": "^0.29.2",
|
"sharp": "^0.29.2",
|
||||||
"socket.io": "^4.3.1",
|
"socket.io": "^4.3.1",
|
||||||
|
@ -15,4 +15,4 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import ConnectionInfo from './sections/connection-info';
|
|||||||
import GuildTitle from './sections/guild-title';
|
import GuildTitle from './sections/guild-title';
|
||||||
import UI from '../ui';
|
import UI from '../ui';
|
||||||
import ChannelList from './lists/channel-list';
|
import ChannelList from './lists/channel-list';
|
||||||
|
import SendMessage from './sections/send-message';
|
||||||
|
|
||||||
export function mountBaseComponents() {
|
export function mountBaseComponents() {
|
||||||
// guild-list
|
// guild-list
|
||||||
@ -37,10 +38,13 @@ export function mountGuildComponents(q: Q, ui: UI, guild: CombinedGuild) {
|
|||||||
export function mountGuildChannelComponents(q: Q, guild: CombinedGuild, channel: Channel) {
|
export function mountGuildChannelComponents(q: Q, guild: CombinedGuild, channel: Channel) {
|
||||||
// channel-title pieces
|
// channel-title pieces
|
||||||
ElementsUtil.unmountReactComponent(q.$('.channel-title-anchor'));
|
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
|
// message-list
|
||||||
// Up Next: Figure out why this is empty
|
|
||||||
ElementsUtil.unmountReactComponent(q.$('.message-list-anchor'));
|
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` }
|
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 {
|
static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf {
|
||||||
const element = ReactHelper.createElementFromJSX(
|
const element = ReactHelper.createElementFromJSX(
|
||||||
<div className="context">
|
<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-content">
|
||||||
<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 id="channel-feed-input-wrapper">
|
<div id="channel-feed-input-wrapper">
|
||||||
<div id="channel-feed-input">
|
<div id="channel-feed-input">
|
||||||
<div id="input-bar">
|
<div id="input-bar">
|
||||||
|
@ -3,6 +3,48 @@
|
|||||||
$scrollbarBottom: 4px;
|
$scrollbarBottom: 4px;
|
||||||
$borderRadius: 8px;
|
$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 {
|
#channel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -16,6 +58,7 @@ $borderRadius: 8px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
#channel-feed-input-wrapper {
|
#channel-feed-input-wrapper {
|
||||||
|
display: none; // for testing
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
@ -34,3 +34,7 @@ body {
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[contenteditable=plaintext-only]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user