forked from coracle/flotilla
feat: persist composer drafts in memory across navigation
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -37,7 +38,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, h, header, initialValues}: Props = $props()
|
||||
let {url, h, header, initialValues}: Props = $props()
|
||||
|
||||
const draftKey = `calendar:${url}:${h ?? ""}`
|
||||
const draft = getDraft(draftKey)
|
||||
if (!initialValues) {
|
||||
initialValues = draft as unknown as typeof initialValues
|
||||
}
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -95,17 +102,34 @@
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
publishThunk({event, relays: [url]})
|
||||
clearDraft(draftKey)
|
||||
history.back()
|
||||
}
|
||||
|
||||
const content = initialValues?.content || ""
|
||||
const editor = makeEditor({url, submit, uploading, content})
|
||||
const onChange = (json: unknown) => {
|
||||
setDraft(draftKey, {editorContent: json, title, location})
|
||||
}
|
||||
const editor = makeEditor({
|
||||
url,
|
||||
submit,
|
||||
uploading,
|
||||
onChange,
|
||||
content: draft.editorContent ?? initialValues?.content ?? "",
|
||||
})
|
||||
|
||||
let title = $state(initialValues?.title || "")
|
||||
let location = $state(initialValues?.location || "")
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let location = $state(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(() => {
|
||||
setDraft(draftKey, {
|
||||
...getDraft(draftKey),
|
||||
title,
|
||||
location,
|
||||
})
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!endDirty && start) {
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
const {pubkeys, info}: Props = $props()
|
||||
|
||||
const chat = deriveChat(pubkeys)
|
||||
const draftKey = `dm:${$chat?.id}`
|
||||
const others = remove($pubkey!, pubkeys)
|
||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||
|
||||
@@ -337,6 +338,7 @@
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
content={eventToEdit?.content}
|
||||
draftKey={eventToEdit ? undefined : draftKey}
|
||||
disabled={Boolean(missingRelayLists.length)} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
@@ -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 ? 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: content ?? (draft.content as string | object | undefined),
|
||||
autofocus,
|
||||
submit,
|
||||
uploading,
|
||||
onChange,
|
||||
aggressive: true,
|
||||
encryptFiles: true,
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
@@ -38,7 +39,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, h, header, initialValues}: Props = $props()
|
||||
let {url, h, header, initialValues}: Props = $props()
|
||||
|
||||
const draftKey = `classified:${url}:${h ?? ""}`
|
||||
const draft = getDraft(draftKey)
|
||||
if (!initialValues) {
|
||||
initialValues = draft as unknown as typeof initialValues
|
||||
}
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -110,22 +117,30 @@
|
||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
||||
})
|
||||
|
||||
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) => {
|
||||
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(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))))
|
||||
|
||||
$effect(() => {
|
||||
setDraft(draftKey, {...getDraft(draftKey), title, price, currency, topics, status})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
import {getDraft, setDraft, clearDraft} from "@app/util/drafts"
|
||||
import type {PollType} from "@app/util/polls"
|
||||
|
||||
type Props = {
|
||||
@@ -31,6 +32,9 @@
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const draftKey = `poll:${url}:${h ?? ""}`
|
||||
const draft = getDraft(draftKey)
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
type DraftOption = {
|
||||
@@ -129,17 +133,24 @@
|
||||
event: makeEvent(Poll, {content: title.trim(), tags}),
|
||||
})
|
||||
|
||||
clearDraft(draftKey)
|
||||
history.back()
|
||||
}
|
||||
|
||||
let title = $state("")
|
||||
let pollType = $state<PollType>("singlechoice")
|
||||
let endsAt = $state<number | undefined>()
|
||||
let options = $state<DraftOption[]>([
|
||||
{id: randomId(), value: "Yes"},
|
||||
{id: randomId(), value: "No"},
|
||||
])
|
||||
let title = $state((draft.title as string | undefined) ?? "")
|
||||
let pollType = $state<PollType>((draft.pollType as PollType | undefined) ?? "singlechoice")
|
||||
let endsAt = $state<number | undefined>((draft.endsAt as number | undefined) ?? undefined)
|
||||
let options = $state<DraftOption[]>(
|
||||
(draft.options as DraftOption[] | undefined) ?? [
|
||||
{id: randomId(), value: "Yes"},
|
||||
{id: randomId(), value: "No"},
|
||||
],
|
||||
)
|
||||
let draggedOptionId = $state<string | undefined>()
|
||||
|
||||
$effect(() => {
|
||||
setDraft(draftKey, {...getDraft(draftKey), title, pollType, endsAt, options})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
|
||||
@@ -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 ? 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: content ?? (draft.content as string | object | undefined),
|
||||
autofocus,
|
||||
submit,
|
||||
uploading,
|
||||
onChange,
|
||||
aggressive: true,
|
||||
})
|
||||
|
||||
let popover: Instance | undefined = $state()
|
||||
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+11
-4
@@ -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<number>
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const store = new Map<string, Record<string, unknown>>()
|
||||
|
||||
export const getDraft = (key: string): Record<string, unknown> => store.get(key) ?? {}
|
||||
|
||||
export const setDraft = (key: string, draft: Record<string, unknown>) => store.set(key, draft)
|
||||
|
||||
export const clearDraft = (key: string | undefined) => {
|
||||
if (key !== undefined) store.delete(key)
|
||||
}
|
||||
Reference in New Issue
Block a user