save reply and DM drafts

This commit is contained in:
hzrd149 2024-06-06 08:25:48 -05:00
parent 8f963ec55e
commit 5b842081a3
9 changed files with 81 additions and 53 deletions

View File

@ -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",

View File

@ -111,7 +111,7 @@ export default function PostModal({
watch("difficulty");
// cache form to localStorage
useCacheForm<FormValues>(cacheFormKey, getValues, setValue, formState);
useCacheForm<FormValues>(cacheFormKey, getValues, reset, formState);
const imageUploadRef = useRef<HTMLInputElement | null>(null);

View File

@ -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<TFieldValues extends FieldValues = FieldValues>(
key: string | null,
getValues: UseFormGetValues<TFieldValues>,
setValue: UseFormSetValue<TFieldValues>,
reset: UseFormReset<TFieldValues>,
state: UseFormStateReturn<TFieldValues>,
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<UseFormStateReturn<TFieldValues>>(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<UseFormStateReturn<TFieldValues>>(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]);
}

View File

@ -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);

View File

@ -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<RefType | null>(null);
const textAreaRef = useRef<HTMLTextAreaElement | null>(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({
<MagicTextArea
mb="2"
value={getValues().content}
onChange={(e) => 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)}

View File

@ -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<HTMLFormElement | null>(null);

View File

@ -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,
);

View File

@ -44,7 +44,7 @@ function EditWikiPagePage({ page }: { page: NostrEvent }) {
"wiki-" + topic,
// @ts-expect-error
getValues,
setValue,
reset,
formState,
);

View File

@ -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"