Skip to content

Commit

Permalink
switch to slate for chat input
Browse files Browse the repository at this point in the history
  • Loading branch information
Puyodead1 committed Aug 14, 2023
1 parent 270c6c9 commit 02e215c
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 125 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
30 changes: 21 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

185 changes: 72 additions & 113 deletions src/components/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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("");

React.useEffect(() => {
// ensure the content is not just a new line
if (content === "\n") {
setContent("");
return;
}

// 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 editor = useMemo(() => withHistory(withReact(createEditor())), []);
const [content, setContent] = useState("");

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

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);
Expand All @@ -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",
}}
>
Expand All @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion src/components/UserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 02e215c

Please sign in to comment.