diff --git a/src/app/components/CalendarEventForm.svelte b/src/app/components/CalendarEventForm.svelte index 6ccf1d48..b4d50999 100644 --- a/src/app/components/CalendarEventForm.svelte +++ b/src/app/components/CalendarEventForm.svelte @@ -20,6 +20,7 @@ import EditorContent from "@app/editor/EditorContent.svelte" import {PROTECTED} from "@app/core/state" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {pushToast} from "@app/util/toast" import {canEnforceNip70} from "@app/core/commands" @@ -39,6 +40,9 @@ const {url, h, header, initialValues}: Props = $props() + const draftKey = !initialValues ? `calendar:${url}:${h ?? ""}` : undefined + const draft = draftKey ? getDraft(draftKey) : {} + const shouldProtect = canEnforceNip70(url) const uploading = writable(false) @@ -95,17 +99,37 @@ pushToast({message: "Your event has been saved!"}) publishThunk({event, relays: [url]}) + if (draftKey) clearDraft(draftKey) history.back() } - const content = initialValues?.content || "" - const editor = makeEditor({url, submit, uploading, content}) + const initialContent = initialValues?.content || "" + const onChange = (json: unknown) => { + if (draftKey) setDraft(draftKey, {editorContent: json, title, location}) + } + const editor = makeEditor({ + url, + submit, + uploading, + onChange, + content: draft.editorContent ?? initialContent, + }) - let title = $state(initialValues?.title || "") - let location = $state(initialValues?.location || "") + let title = $state((draft.title as string | undefined) ?? initialValues?.title ?? "") + let location = $state((draft.location as string | undefined) ?? initialValues?.location ?? "") let start: number | undefined = $state(initialValues?.start) let end: number | undefined = $state(initialValues?.end) - let endDirty = Boolean(initialValues?.end) + let endDirty = $state(Boolean(initialValues?.end)) + + $effect(() => { + if (draftKey) { + setDraft(draftKey, { + ...getDraft(draftKey), + title, + location, + }) + } + }) $effect(() => { if (!endDirty && start) { diff --git a/src/app/components/Chat.svelte b/src/app/components/Chat.svelte index 26e84db8..a3e79e35 100644 --- a/src/app/components/Chat.svelte +++ b/src/app/components/Chat.svelte @@ -65,6 +65,7 @@ const {pubkeys, info}: Props = $props() + const draftKey = `dm:${[...pubkeys].sort().join(":")}` const chat = deriveChat(pubkeys) const others = remove($pubkey!, pubkeys) const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk))) @@ -353,6 +354,7 @@ {onEscape} {onEditPrevious} content={eventToEdit?.content} + draftKey={eventToEdit ? undefined : draftKey} disabled={Boolean(missingRelayLists.length)} /> {/key} diff --git a/src/app/components/ChatCompose.svelte b/src/app/components/ChatCompose.svelte index 68b62447..f3b9b541 100644 --- a/src/app/components/ChatCompose.svelte +++ b/src/app/components/ChatCompose.svelte @@ -10,16 +10,20 @@ import Button from "@lib/components/Button.svelte" import EditorContent from "@app/editor/EditorContent.svelte" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" type Props = { content?: string disabled?: boolean + draftKey?: string onEscape?: () => void onEditPrevious?: () => void onSubmit: (event: EventContent) => void } - const {content, disabled = false, onEscape, onEditPrevious, onSubmit}: Props = $props() + const {content, disabled = false, draftKey, onEscape, onEditPrevious, onSubmit}: Props = $props() + + const draft = draftKey && !content ? getDraft(draftKey) : {} const autofocus = !isMobile && !disabled @@ -59,14 +63,20 @@ onSubmit({content, tags}) + if (draftKey) clearDraft(draftKey) ed.chain().clearContent().run() } + const onChange = (json: unknown) => { + if (draftKey) setDraft(draftKey, {content: json}) + } + const editor = makeEditor({ - content, + content: draft.content ?? content, autofocus, submit, uploading, + onChange, aggressive: true, encryptFiles: true, }) diff --git a/src/app/components/ClassifiedForm.svelte b/src/app/components/ClassifiedForm.svelte index 4e9fcbf6..9ba04271 100644 --- a/src/app/components/ClassifiedForm.svelte +++ b/src/app/components/ClassifiedForm.svelte @@ -20,6 +20,7 @@ import {pushToast} from "@app/util/toast" import {PROTECTED} from "@app/core/state" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {canEnforceNip70, uploadFile} from "@app/core/commands" type Props = { @@ -40,6 +41,9 @@ const {url, h, header, initialValues}: Props = $props() + const draftKey = !initialValues ? `classified:${url}:${h ?? ""}` : undefined + const draft = draftKey ? getDraft(draftKey) : {} + const shouldProtect = canEnforceNip70(url) const back = () => history.back() @@ -110,22 +114,37 @@ event: makeEvent(CLASSIFIED, {content, tags}), }) + if (draftKey) clearDraft(draftKey) history.back() } finally { loading = false } } - const content = initialValues?.content || "" - const editor = makeEditor({url, submit, content}) + const initialContent = initialValues?.content || "" + const onChange = (json: unknown) => { + if (draftKey) setDraft(draftKey, {editorContent: json, title, price, currency, topics, status}) + } + const editor = makeEditor({url, submit, onChange, content: draft.editorContent ?? initialContent}) let loading = $state(false) - let title = $state(initialValues?.title || "") - let status = $state(initialValues?.status || "active") - let price = $state(Number(initialValues?.price || 0)) - let currency = $state(initialValues?.currency || "SAT") - let images = $state<(string | File)[]>(initialValues?.images || []) - let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic)))) + let title = $state((draft.title as string | undefined) ?? initialValues?.title ?? "") + let status = $state((draft.status as string | undefined) ?? initialValues?.status ?? "active") + let price = $state((draft.price as number | undefined) ?? Number(initialValues?.price ?? 0)) + let currency = $state((draft.currency as string | undefined) ?? initialValues?.currency ?? "SAT") + let images = $state<(string | File)[]>(initialValues?.images ?? []) + let topics = $state( + uniq( + removeUndefined( + ((draft.topics as string[] | undefined) ?? initialValues?.topics ?? []).map(normalizeTopic), + ), + ), + ) + + $effect(() => { + if (draftKey) + setDraft(draftKey, {...getDraft(draftKey), title, price, currency, topics, status}) + }) diff --git a/src/app/components/EventReply.svelte b/src/app/components/EventReply.svelte index 20a9826a..b7e3eaee 100644 --- a/src/app/components/EventReply.svelte +++ b/src/app/components/EventReply.svelte @@ -10,10 +10,14 @@ import {publishComment, canEnforceNip70} from "@app/core/commands" import {PROTECTED} from "@app/core/state" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {pushToast} from "@app/util/toast" const {url, event, onClose, onSubmit} = $props() + const draftKey = `reply:${event.id}` + const draft = getDraft(draftKey) + const shouldProtect = canEnforceNip70(url) const uploading = writable(false) @@ -38,10 +42,20 @@ }) } + clearDraft(draftKey) onSubmit(publishComment({event, content, tags, relays: [url]})) } - const editor = makeEditor({url, submit, uploading, autofocus: !isMobile}) + const onChange = (json: unknown) => setDraft(draftKey, {content: json}) + + const editor = makeEditor({ + url, + submit, + uploading, + autofocus: !isMobile, + content: draft.content as string | object | undefined, + onChange, + }) let form: HTMLElement let spacer: HTMLElement diff --git a/src/app/components/GoalCreate.svelte b/src/app/components/GoalCreate.svelte index 1db8ded5..9a5ea827 100644 --- a/src/app/components/GoalCreate.svelte +++ b/src/app/components/GoalCreate.svelte @@ -20,6 +20,7 @@ import {pushToast} from "@app/util/toast" import {PROTECTED} from "@app/core/state" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {canEnforceNip70} from "@app/core/commands" type Props = { @@ -29,6 +30,9 @@ const {url, h}: Props = $props() + const draftKey = `goal:${url}:${h ?? ""}` + const draft = getDraft(draftKey) + const shouldProtect = canEnforceNip70(url) const uploading = writable(false) @@ -77,13 +81,27 @@ event: makeEvent(ZAP_GOAL, {content, tags}), }) + clearDraft(draftKey) history.back() } - const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"}) + const onChange = (json: unknown) => setDraft(draftKey, {editorContent: json, content, amount}) - let content = $state("") - let amount = $state(1000) + const editor = makeEditor({ + url, + submit, + uploading, + onChange, + placeholder: "What's on your mind?", + content: draft.editorContent as string | object | undefined, + }) + + let content = $state((draft.content as string | undefined) ?? "") + let amount = $state((draft.amount as number | undefined) ?? 1000) + + $effect(() => { + setDraft(draftKey, {...getDraft(draftKey), content, amount}) + }) diff --git a/src/app/components/RoomCompose.svelte b/src/app/components/RoomCompose.svelte index 28bb18ab..7a235b3f 100644 --- a/src/app/components/RoomCompose.svelte +++ b/src/app/components/RoomCompose.svelte @@ -12,6 +12,7 @@ import ComposeMenu from "@app/components/ComposeMenu.svelte" import EditorContent from "@app/editor/EditorContent.svelte" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {onDestroy, onMount} from "svelte" type Props = { @@ -25,6 +26,9 @@ const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props() + const draftKey = url || h ? `room:${url ?? ""}:${h ?? ""}` : undefined + const draft = draftKey && !content ? getDraft(draftKey) : {} + const autofocus = !isMobile const uploading = writable(false) @@ -61,10 +65,23 @@ onSubmit({content, tags}) + if (draftKey) clearDraft(draftKey) ed.chain().clearContent().run() } - const editor = makeEditor({url, content, autofocus, submit, uploading, aggressive: true}) + const onChange = (json: unknown) => { + if (draftKey) setDraft(draftKey, {content: json}) + } + + const editor = makeEditor({ + url, + content: draft.content ?? content, + autofocus, + submit, + uploading, + onChange, + aggressive: true, + }) let popover: Instance | undefined = $state() diff --git a/src/app/components/ThreadCreate.svelte b/src/app/components/ThreadCreate.svelte index 5f947e0d..a356d81a 100644 --- a/src/app/components/ThreadCreate.svelte +++ b/src/app/components/ThreadCreate.svelte @@ -18,6 +18,7 @@ import {pushToast} from "@app/util/toast" import {PROTECTED} from "@app/core/state" import {makeEditor} from "@app/editor" + import {getDraft, setDraft, clearDraft} from "@app/util/drafts" import {canEnforceNip70} from "@app/core/commands" type Props = { @@ -27,6 +28,9 @@ const {url, h}: Props = $props() + const draftKey = `thread:${url}:${h ?? ""}` + const draft = getDraft(draftKey) + const shouldProtect = canEnforceNip70(url) const uploading = writable(false) @@ -70,12 +74,26 @@ event: makeEvent(THREAD, {content, tags}), }) + clearDraft(draftKey) history.back() } - const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"}) + const onChange = (json: unknown) => setDraft(draftKey, {content: json, title}) - let title: string = $state("") + const editor = makeEditor({ + url, + submit, + uploading, + onChange, + placeholder: "What's on your mind?", + content: draft.content as string | object | undefined, + }) + + let title: string = $state((draft.title as string | undefined) ?? "") + + $effect(() => { + setDraft(draftKey, {...getDraft(draftKey), title}) + }) diff --git a/src/app/editor/EditorContent.svelte b/src/app/editor/EditorContent.svelte index 6b56f9c6..cdf764fe 100644 --- a/src/app/editor/EditorContent.svelte +++ b/src/app/editor/EditorContent.svelte @@ -11,13 +11,17 @@ let element: HTMLElement onMount(() => { - editor.then(({options}) => { - if (options.element) { - element?.append(options.element) + editor.then(ed => { + if (ed.options.element) { + element?.append(ed.options.element) } - if (options.autofocus) { - ;(element?.querySelector("[contenteditable]") as HTMLElement)?.focus() + if ((ed as any)._shouldAutofocus) { + const hasContent = ed.getText().trim().length > 0 + + requestAnimationFrame(() => { + ed.commands.focus(hasContent ? "end" : "start") + }) } }) }) diff --git a/src/app/editor/index.ts b/src/app/editor/index.ts index 8ea78606..ba9b0589 100644 --- a/src/app/editor/index.ts +++ b/src/app/editor/index.ts @@ -28,6 +28,7 @@ export const makeEditor = async ({ autofocus = false, charCount, content = "", + onChange, placeholder = "", url, submit, @@ -38,7 +39,8 @@ export const makeEditor = async ({ aggressive?: boolean autofocus?: boolean charCount?: Writable - content?: string + content?: string | object + onChange?: (json: unknown) => void placeholder?: string url?: string submit: () => void @@ -82,9 +84,9 @@ export const makeEditor = async ({ }, ) - return new Editor({ - content: escapeHtml(content), - autofocus, + const ed = new Editor({ + content: typeof content === "string" ? escapeHtml(content) : content, + autofocus: false, editorProps, element: document.createElement("div"), extensions: [ @@ -142,6 +144,11 @@ export const makeEditor = async ({ onUpdate({editor}) { wordCount?.set(editor.storage.wordCount.words) charCount?.set(editor.storage.wordCount.chars) + onChange?.(editor.getJSON()) }, }) + + ;(ed as any)._shouldAutofocus = autofocus + + return ed } diff --git a/src/app/util/drafts.ts b/src/app/util/drafts.ts new file mode 100644 index 00000000..8943ecae --- /dev/null +++ b/src/app/util/drafts.ts @@ -0,0 +1,7 @@ +const store = new Map>() + +export const getDraft = (key: string): Record => store.get(key) ?? {} + +export const setDraft = (key: string, draft: Record) => store.set(key, draft) + +export const clearDraft = (key: string) => store.delete(key)