diff --git a/package.json b/package.json index 0b50a4bd..4d25dcdc 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05aa4c55..2c41c362 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index fff87fbc..4eda7575 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -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(null); - const placeholderRef = React.useRef(null); - const inputRef = React.useRef(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) => { + 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) { - const target = e.target as HTMLDivElement; - const text = target.innerText; - - setContent(text); - adjustInputHeight(); - } - - function onKeyDown(e: React.KeyboardEvent) { - 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 (
@@ -142,44 +123,22 @@ function MessageInput(props: Props) { position: "relative", }} > -
-
- - Message #{props.channel?.name} - - -
-
+ + +
diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx index 9bee3eb1..4ec54dc7 100644 --- a/src/components/UserPanel.tsx +++ b/src/components/UserPanel.tsx @@ -71,7 +71,7 @@ function UserPanel() { - +