From 5b842081a31d566512d8b6260de75a9e96bef54f Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Thu, 6 Jun 2024 08:25:48 -0500 Subject: [PATCH] save reply and DM drafts --- package.json | 2 +- src/components/post-modal/index.tsx | 2 +- src/hooks/use-cache-form.ts | 87 +++++++++++-------- .../channels/components/send-message-form.tsx | 2 +- .../dms/components/send-message-form.tsx | 10 ++- src/views/thread/components/reply-form.tsx | 7 +- src/views/wiki/create.tsx | 14 +-- src/views/wiki/edit.tsx | 2 +- yarn.lock | 8 +- 9 files changed, 81 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index d85a9d379..e1af426f9 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "react-error-boundary": "^4.0.11", "react-force-graph-2d": "^1.25.4", "react-force-graph-3d": "^1.24.2", - "react-hook-form": "^7.45.4", + "react-hook-form": "^7.51.5", "react-markdown": "^9.0.1", "react-mosaic-component": "^6.1.0", "react-photo-album": "^2.3.0", diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index 4cf1eb416..e2771fb4a 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -111,7 +111,7 @@ export default function PostModal({ watch("difficulty"); // cache form to localStorage - useCacheForm(cacheFormKey, getValues, setValue, formState); + useCacheForm(cacheFormKey, getValues, reset, formState); const imageUploadRef = useRef(null); diff --git a/src/hooks/use-cache-form.ts b/src/hooks/use-cache-form.ts index d915026b0..43865520f 100644 --- a/src/hooks/use-cache-form.ts +++ b/src/hooks/use-cache-form.ts @@ -1,63 +1,78 @@ -import { useCallback, useMemo, useRef } from "react"; -import { FieldValues, UseFormGetValues, UseFormSetValue, UseFormStateReturn } from "react-hook-form"; -import { useMount, useUnmount } from "react-use"; -import { logger } from "../helpers/debug"; -import { useTimeout } from "@chakra-ui/react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { FieldValues, UseFormGetValues, UseFormReset, UseFormStateReturn } from "react-hook-form"; +import { useBeforeUnload } from "react-router-dom"; + +import { logger } from "../helpers/debug"; -// TODO: make these caches expire export default function useCacheForm( key: string | null, getValues: UseFormGetValues, - setValue: UseFormSetValue, + reset: UseFormReset, state: UseFormStateReturn, + opts?: { clearOnKeyChange: boolean }, ) { const log = useMemo(() => (key ? logger.extend(`CachedForm:${key}`) : () => {}), [key]); const storageKey = key && "cached-form-" + key; - useMount(() => { - if (storageKey === null) return; + const stateRef = useRef>(state); + stateRef.current = state; + + // NOTE: this watches the dirty state + state.isDirty; + state.isSubmitted; + + useEffect(() => { + if (!storageKey) return; + + // restore form on key change or mount try { const cached = localStorage.getItem(storageKey); + + // remove the item and keep it in memory localStorage.removeItem(storageKey); if (cached) { - log("Restoring form"); const values = JSON.parse(cached) as TFieldValues; - for (const [key, value] of Object.entries(values)) { - // @ts-ignore - setValue(key, value, { shouldDirty: true }); - } + + log("Restoring form"); + reset(values, { keepDefaultValues: true }); + } else if (opts?.clearOnKeyChange) { + log("Clearing form"); + reset(); } } catch (e) {} - }); - const stateRef = useRef>(state); - stateRef.current = state; - useUnmount(() => { - if (storageKey === null) return; - if (!stateRef.current.isDirty) return; + // save previous key on change or unmount + return () => { + if (stateRef.current.isSubmitted) { + log("Removing because submitted"); + localStorage.removeItem(storageKey); + } else if (stateRef.current.isDirty) { + const values = getValues(); + log("Saving form", values); + localStorage.setItem(storageKey, JSON.stringify(values)); + } + }; + }, [storageKey, log, opts?.clearOnKeyChange]); - if (!stateRef.current.isSubmitted) { - log("Saving form", getValues()); - localStorage.setItem(storageKey, JSON.stringify(getValues())); - } else if (localStorage.getItem(storageKey)) { - log("Removing cache because form was submitted"); + const saveOnClose = useCallback(() => { + if (!storageKey) return; + + if (stateRef.current.isSubmitted) { + log("Removing because submitted"); localStorage.removeItem(storageKey); + } else if (stateRef.current.isDirty) { + const values = getValues(); + log("Saving form", values); + localStorage.setItem(storageKey, JSON.stringify(values)); } - }); + }, [log, getValues, storageKey]); - const autoSave = useCallback(() => { - if (storageKey === null) return; - if (!stateRef.current.isSubmitted) { - log("Autosave", getValues()); - localStorage.setItem(storageKey, JSON.stringify(getValues())); - } - }, [storageKey]); - - useTimeout(autoSave, 5_000); + useBeforeUnload(saveOnClose); return useCallback(() => { - if (storageKey === null) return; + if (!storageKey) return; + localStorage.removeItem(storageKey); }, [storageKey]); } diff --git a/src/views/channels/components/send-message-form.tsx b/src/views/channels/components/send-message-form.tsx index 874b8d57b..1d94226c5 100644 --- a/src/views/channels/components/send-message-form.tsx +++ b/src/views/channels/components/send-message-form.tsx @@ -49,7 +49,7 @@ export default function ChannelMessageForm({ setLoadingMessage("Signing..."); await publish("Send DM", draft, undefined, false); - reset(); + reset({ content: "" }); // refocus input setTimeout(() => textAreaRef.current?.focus(), 50); diff --git a/src/views/dms/components/send-message-form.tsx b/src/views/dms/components/send-message-form.tsx index a89514044..e7326ae25 100644 --- a/src/views/dms/components/send-message-form.tsx +++ b/src/views/dms/components/send-message-form.tsx @@ -11,6 +11,7 @@ import { DraftNostrEvent } from "../../../types/nostr-event"; import { useDecryptionContext } from "../../../providers/global/decryption-provider"; import useUserMailboxes from "../../../hooks/use-user-mailboxes"; import { usePublishEvent } from "../../../providers/global/publish-provider"; +import useCacheForm from "../../../hooks/use-cache-form"; export default function SendMessageForm({ pubkey, @@ -30,6 +31,10 @@ export default function SendMessageForm({ }); watch("content"); + const clearCache = useCacheForm<{ content: string }>(`dm-${pubkey}`, getValues, reset, formState, { + clearOnKeyChange: true, + }); + const autocompleteRef = useRef(null); const textAreaRef = useRef(null); const { onPaste } = useTextAreaUploadFileWithForm(autocompleteRef, getValues, setValue); @@ -55,7 +60,8 @@ export default function SendMessageForm({ const pub = await publish("Send DM", draft, userMailboxes?.inbox); if (pub) { - reset(); + clearCache(); + reset({ content: "" }); // add plaintext to decryption context getOrCreateContainer(pubkey, encrypted).plaintext.next(values.content); @@ -79,7 +85,7 @@ export default function SendMessageForm({ setValue("content", e.target.value, { shouldDirty: true })} + onChange={(e) => setValue("content", e.target.value, { shouldDirty: true, shouldTouch: true })} rows={2} isRequired instanceRef={(inst) => (autocompleteRef.current = inst)} diff --git a/src/views/thread/components/reply-form.tsx b/src/views/thread/components/reply-form.tsx index 8e6fba42a..2150af146 100644 --- a/src/views/thread/components/reply-form.tsx +++ b/src/views/thread/components/reply-form.tsx @@ -25,6 +25,7 @@ import { UploadImageIcon } from "../../../components/icons"; import { unique } from "../../../helpers/array"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; +import useCacheForm from "../../../hooks/use-cache-form"; export type ReplyFormProps = { item: ThreadItem; @@ -41,11 +42,14 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin const { requestSignature } = useSigningContext(); const threadMembers = useMemo(() => getThreadMembers(item, account?.pubkey), [item, account?.pubkey]); - const { setValue, getValues, watch, handleSubmit } = useForm({ + const { setValue, getValues, watch, handleSubmit, formState, reset } = useForm({ defaultValues: { content: "", }, + mode: "all", }); + const clearCache = useCacheForm<{ content: string }>(`reply-${item.event.id}`, getValues, reset, formState); + const contentMentions = getPubkeysMentionedInContent(getValues().content); const notifyPubkeys = unique([...threadMembers, ...contentMentions]); @@ -88,6 +92,7 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin const pub = await publish("Reply", { ...draft, created_at: dayjs().unix() }); if (pub && onSubmitted) onSubmitted(pub.event); + clearCache(); }); const formRef = useRef(null); diff --git a/src/views/wiki/create.tsx b/src/views/wiki/create.tsx index cfba9df3b..87c5c345e 100644 --- a/src/views/wiki/create.tsx +++ b/src/views/wiki/create.tsx @@ -61,11 +61,13 @@ export default function CreateWikiPageView() { useEffect(() => { if (!fork) return; - setValue("topic", getPageTopic(fork)); - setValue("title", getPageTitle(fork) ?? ""); - setValue("summary", getPageSummary(fork)); - setValue("content", fork.content); - }, [fork, setValue]); + reset({ + topic: getPageTopic(fork), + title: getPageTitle(fork) ?? "", + summary: getPageSummary(fork, false) ?? "", + content: fork.content, + }); + }, [fork, reset]); const cacheKey = forkAddress ? "wiki-" + forkAddress.identifier + ":" + forkAddress.pubkey + "-fork" @@ -77,7 +79,7 @@ export default function CreateWikiPageView() { cacheKey, // @ts-expect-error getValues, - setValue, + reset, formState, ); diff --git a/src/views/wiki/edit.tsx b/src/views/wiki/edit.tsx index 8f8dc3527..400d0b562 100644 --- a/src/views/wiki/edit.tsx +++ b/src/views/wiki/edit.tsx @@ -44,7 +44,7 @@ function EditWikiPagePage({ page }: { page: NostrEvent }) { "wiki-" + topic, // @ts-expect-error getValues, - setValue, + reset, formState, ); diff --git a/yarn.lock b/yarn.lock index 4d7a794de..5f6c11c1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6702,10 +6702,10 @@ react-force-graph-3d@^1.24.2: prop-types "15" react-kapsule "2" -react-hook-form@^7.45.4: - version "7.50.1" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.50.1.tgz#f6aeb17a863327e5a0252de8b35b4fc8990377ed" - integrity sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ== +react-hook-form@^7.51.5: + version "7.51.5" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.5.tgz#4afbfb819312db9fea23e8237a3a0d097e128b43" + integrity sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1"