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:
parent
270c6c9083
commit
02e215c54b
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user