1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 10:22:30 +01:00

switch to slate for chat input

This commit is contained in:
Puyodead1 2023-08-14 01:47:38 -04:00
parent 270c6c9083
commit 02e215c54b
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
4 changed files with 95 additions and 123 deletions

View File

@ -47,8 +47,9 @@
"react-select-search": "^4.1.6",
"react-spinners": "^0.13.8",
"reoverlay": "^1.0.3",
"slate": "^0.91.4",
"slate-react": "^0.92.0",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.98.1",
"styled-components": "^5.3.10",
"typescript": "^4.9.5"
},

View File

@ -111,11 +111,14 @@ dependencies:
specifier: ^1.0.3
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
slate:
specifier: ^0.91.4
version: 0.91.4
specifier: ^0.94.1
version: 0.94.1
slate-history:
specifier: ^0.93.0
version: 0.93.0(slate@0.94.1)
slate-react:
specifier: ^0.92.0
version: 0.92.0(react-dom@18.2.0)(react@18.2.0)(slate@0.91.4)
specifier: ^0.98.1
version: 0.98.1(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1)
styled-components:
specifier: ^5.3.10
version: 5.3.10(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
@ -9998,8 +10001,17 @@ packages:
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
engines: {node: '>=12'}
/slate-react@0.92.0(react-dom@18.2.0)(react@18.2.0)(slate@0.91.4):
resolution: {integrity: sha512-xEDKu5RKw5f0N95l1UeNQnrB0Pxh4JPjpIZR/BVsMo0ININnLAknR99gLo46bl/Ffql4mr7LeaxQRoXxbFtJOQ==}
/slate-history@0.93.0(slate@0.94.1):
resolution: {integrity: sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g==}
peerDependencies:
slate: '>=0.65.3'
dependencies:
is-plain-object: 5.0.0
slate: 0.94.1
dev: false
/slate-react@0.98.1(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1):
resolution: {integrity: sha512-ta4TAxoHE740e5EYSjAvK2bSpvrvnTkPfwMmx7rV+z/r8sng/RaJpc5cL9Rt2sfqQonSZOnQtAIaL6g97bLgzw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
@ -10015,12 +10027,12 @@ packages:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
scroll-into-view-if-needed: 2.2.31
slate: 0.91.4
slate: 0.94.1
tiny-invariant: 1.0.6
dev: false
/slate@0.91.4:
resolution: {integrity: sha512-aUJ3rpjrdi5SbJ5G1Qjr3arytfRkEStTmHjBfWq2A2Q8MybacIzkScSvGJjQkdTk3djCK9C9SEOt39sSeZFwTw==}
/slate@0.94.1:
resolution: {integrity: sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==}
dependencies:
immer: 9.0.21
is-plain-object: 5.0.0

View File

@ -1,36 +1,50 @@
import React from "react";
import styled from "styled-components";
import useLogger from "../hooks/useLogger";
import { useAppStore } from "../stores/AppStore";
import Channel from "../stores/objects/Channel";
import { useMemo, useState } from "react";
import { BaseEditor, Descendant, Node, createEditor } from "slate";
import { HistoryEditor, withHistory } from "slate-history";
import { Editable, ReactEditor, Slate, withReact } from "slate-react";
import User from "../stores/objects/User";
import Snowflake from "../utils/Snowflake";
type CustomElement = { type: "paragraph"; children: CustomText[] };
type CustomText = { text: string; bold?: true };
declare module "slate" {
interface CustomTypes {
Editor: BaseEditor & ReactEditor & HistoryEditor;
Element: CustomElement;
Text: CustomText;
}
}
const Container = styled.div`
margin-top: -8px;
padding-left: 16px;
padding-right: 16px;
flex-shrink: 0;
position: relative;
`;
const InnerContainer = styled.div`
background-color: var(--background-primary);
margin-bottom: 24px;
position: relative;
width: 100%;
border-radius: 8px;
`;
const TextInput = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
outline: none;
word-wrap: anywhere;
padding: 12px 16px;
`;
const initialEditorValue: Descendant[] = [
{
type: "paragraph",
children: [
{
text: "",
},
],
},
];
interface Props {
channel?: Channel;
@ -39,67 +53,26 @@ interface Props {
function MessageInput(props: Props) {
const app = useAppStore();
const logger = useLogger("MessageInput");
const wrapperRef = React.useRef<HTMLDivElement>(null);
const placeholderRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLDivElement>(null);
const [content, setContent] = React.useState("");
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
const [content, setContent] = useState("");
React.useEffect(() => {
// ensure the content is not just a new line
if (content === "\n") {
setContent("");
return;
}
const serialize = (value: Descendant[]) => {
return (
value
// Return the string content of each paragraph in the value's children.
.map((n) => Node.string(n))
// Join them all with line breaks denoting paragraphs.
.join("\n")
);
};
// controls the placeholder visibility
if (!content.length) placeholderRef.current!.style.setProperty("display", "block");
else placeholderRef.current!.style.setProperty("display", "none");
// update the input content
if (inputRef.current) {
// handle empty input
if (!content.length) {
inputRef.current.innerHTML = "";
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
if (!props.channel) {
logger.warn("No channel selected, cannot send message");
return;
} else {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(inputRef.current);
range.collapse(false);
selection?.removeAllRanges();
selection?.addRange(range);
}
}
}, [content]);
// this function makes the input element grow as the user types
function adjustInputHeight() {
if (!wrapperRef.current) return;
wrapperRef.current.style.height = "44px";
wrapperRef.current.style.height = wrapperRef.current.scrollHeight + "px";
}
function resetInput() {
setContent("");
adjustInputHeight();
}
function onChange(e: React.FormEvent<HTMLDivElement>) {
const target = e.target as HTMLDivElement;
const text = target.innerText;
setContent(text);
adjustInputHeight();
}
function onKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (!props.channel) {
logger.warn("No channel selected, cannot send message");
return;
}
if (e.key === "Enter") {
e.preventDefault();
const shouldFail = app.experiments.isTreatmentEnabled("message_queue", 2);
const shouldSend = !app.experiments.isTreatmentEnabled("message_queue", 1);
@ -120,18 +93,26 @@ function MessageInput(props: Props) {
});
}
resetInput();
// reset slate editor
const point = { path: [0, 0], offset: 0 };
editor.selection = { anchor: point, focus: point };
editor.history = { redos: [], undos: [] };
editor.children = initialEditorValue;
}
}
};
const onChange = (value: Descendant[]) => {
const isAstChange = editor.operations.some((op) => "set_selection" !== op.type);
if (isAstChange) {
setContent(serialize(value));
}
};
return (
<Container>
<InnerContainer>
<div
style={{
overflowX: "hidden",
overflowY: "scroll",
maxHeight: "50vh",
borderRadius: "8px",
}}
>
@ -142,44 +123,22 @@ function MessageInput(props: Props) {
position: "relative",
}}
>
<div
style={{
padding: 0,
backgroundColor: "transparent",
resize: "none",
border: "none",
appearance: "none",
fontWeight: 400,
fontSize: "16px",
width: "100%",
height: "44px",
minHeight: "44px",
// maxHeight: "50vh",
color: "var(--text-normal)",
position: "relative",
}}
ref={wrapperRef}
>
<div>
<span
ref={placeholderRef}
style={{
padding: "12px 16px",
}}
>
Message #{props.channel?.name}
</span>
<TextInput
role="textbox"
spellCheck="true"
autoCorrect="off"
contentEditable="true"
onInput={onChange}
onKeyDown={onKeyDown}
ref={inputRef}
/>
</div>
</div>
<Slate editor={editor} initialValue={initialEditorValue} onChange={onChange}>
<Editable
onKeyDown={onKeyDown}
value={content}
style={{
width: "100%",
outline: "none",
wordBreak: "break-word",
padding: "12px 16px",
overflowY: "auto",
maxHeight: "50vh",
}}
placeholder={`Message ${props.channel?.name}`}
aria-label="Message input"
/>
</Slate>
</div>
</div>
</InnerContainer>

View File

@ -71,7 +71,7 @@ function UserPanel() {
<ActionsWrapper>
<Tooltip title="Settings">
<IconButton aria-label="settings" disabled color="#fff">
<IconButton aria-label="settings" color="#fff">
<Icon icon="mdiCog" size="20px" />
</IconButton>
</Tooltip>