web/composer: store drafts in localStorage

This commit is contained in:
Tulir Asokan 2024-10-15 00:06:00 +03:00
parent 3536aa1569
commit ce43c6946c

View file

@ -13,9 +13,9 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use, useCallback, useRef, useState } from "react" import React, { use, useCallback, useLayoutEffect, useRef, useState } from "react"
import { RoomStateStore } from "@/api/statestore" import { RoomStateStore } from "@/api/statestore"
import { MemDBEvent, Mentions } from "@/api/types" import { MemDBEvent, Mentions, RoomID } from "@/api/types"
import { ClientContext } from "./ClientContext.ts" import { ClientContext } from "./ClientContext.ts"
import { ReplyBody } from "./timeline/ReplyBody.tsx" import { ReplyBody } from "./timeline/ReplyBody.tsx"
import "./MessageComposer.css" import "./MessageComposer.css"
@ -27,18 +27,34 @@ interface MessageComposerProps {
closeReply: () => void closeReply: () => void
} }
const draftStore = {
get: (roomID: RoomID) => localStorage.getItem(`draft-${roomID}`) ?? "",
set: (roomID: RoomID, text: string) => localStorage.setItem(`draft-${roomID}`, text),
clear: (roomID: RoomID)=> localStorage.removeItem(`draft-${roomID}`),
}
const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComposerProps) => { const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComposerProps) => {
const client = use(ClientContext)! const client = use(ClientContext)!
const [text, setText] = useState("") const [text, setText] = useState("")
const textRows = useRef(1) const textRows = useRef(1)
const fullSetText = useCallback((text: string, setDraft: boolean) => {
setText(text)
textRows.current = text === "" ? 1 : text.split("\n").length
setTextRows(textRows.current)
if (setDraft) {
if (text === "") {
draftStore.clear(room.roomID)
} else {
draftStore.set(room.roomID, text)
}
}
}, [setTextRows, room.roomID])
const sendMessage = useCallback((evt: React.FormEvent) => { const sendMessage = useCallback((evt: React.FormEvent) => {
evt.preventDefault() evt.preventDefault()
if (text === "") { if (text === "") {
return return
} }
setText("") fullSetText("", true)
setTextRows(1)
textRows.current = 1
closeReply() closeReply()
const room_id = room.roomID const room_id = room.roomID
const mentions: Mentions = { const mentions: Mentions = {
@ -50,17 +66,20 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
} }
client.sendMessage({ room_id, text, reply_to: replyTo?.event_id, mentions }) client.sendMessage({ room_id, text, reply_to: replyTo?.event_id, mentions })
.catch(err => window.alert("Failed to send message: " + err)) .catch(err => window.alert("Failed to send message: " + err))
}, [setTextRows, closeReply, replyTo, text, room, client]) }, [fullSetText, closeReply, replyTo, text, room, client])
const onKeyDown = useCallback((evt: React.KeyboardEvent) => { const onKeyDown = useCallback((evt: React.KeyboardEvent) => {
if (evt.key === "Enter" && !evt.shiftKey) { if (evt.key === "Enter" && !evt.shiftKey) {
sendMessage(evt) sendMessage(evt)
} }
}, [sendMessage]) }, [sendMessage])
const onChange = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => { const onChange = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
setText(evt.target.value) fullSetText(evt.target.value, true)
textRows.current = evt.target.value.split("\n").length }, [fullSetText])
setTextRows(textRows.current) // To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState
}, [setTextRows]) // To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect
useLayoutEffect(() => {
fullSetText(draftStore.get(room.roomID), false)
}, [room.roomID, fullSetText])
return <div className="message-composer"> return <div className="message-composer">
{replyTo && <ReplyBody room={room} event={replyTo} onClose={closeReply}/>} {replyTo && <ReplyBody room={room} event={replyTo} onClose={closeReply}/>}
<div className="input-area"> <div className="input-area">