Clean up drafts implementation (#164)

This commit was merged in pull request #164.
This commit is contained in:
2026-04-07 13:06:29 +00:00
parent 30c2a6ef79
commit 17fb4e780b
13 changed files with 173 additions and 162 deletions
+20 -26
View File
@@ -24,28 +24,28 @@
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70} from "@app/core/commands"
type CalendarValues = { type Values = {
d?: string d: string
title: string title: string
content: unknown content: string | object
location: string location: string
start: number | undefined start?: number
end: number | undefined end?: number
} }
type Props = { type Props = {
url: string url: string
h?: string h?: string
header: Snippet header: Snippet
initialValues?: CalendarValues initialValues?: Values
} }
let {url, h, header, initialValues}: Props = $props() let {url, h, header, initialValues}: Props = $props()
const draftKey = new DraftKey<CalendarValues>(`calendar:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`)
const draft = draftKey.get()
if (!initialValues) { if (!initialValues) {
initialValues = draft initialValues = draftKey.get()
} }
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -83,9 +83,9 @@
const ed = await editor const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = [ const tags = [
["d", initialValues?.d || randomId()], ["d", d],
["title", title], ["title", title],
["location", location || ""], ["location", location],
["start", start.toString()], ["start", start.toString()],
["end", end.toString()], ["end", end.toString()],
...daysBetween(start, end).map(D => ["D", D.toString()]), ...daysBetween(start, end).map(D => ["D", D.toString()]),
@@ -108,28 +108,22 @@
history.back() history.back()
} }
let editorContent = $state<unknown>(initialValues?.content ?? "") const d = $state(initialValues?.d ?? randomId())
const onChange = (json: unknown) => {
editorContent = json
}
const editor = makeEditor({
url,
submit,
uploading,
onChange,
content: initialValues?.content ?? "",
})
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let location = $state(initialValues?.location ?? "") let location = $state(initialValues?.location ?? "")
let start: number | undefined = $state(initialValues?.start) let start: number | undefined = $state(initialValues?.start)
let end: number | undefined = $state(initialValues?.end) let end: number | undefined = $state(initialValues?.end)
let endDirty = $state(Boolean(initialValues?.end)) let endDirty = $state(Boolean(initialValues?.end))
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, uploading, onChange, content})
$effect(() => { $effect(() => {
draftKey.set({d: initialValues?.d, title, location, start, end, content: editorContent}) draftKey.set({d, title, location, start, end, content})
}) })
$effect(() => { $effect(() => {
+2 -2
View File
@@ -67,7 +67,7 @@
const {pubkeys, info}: Props = $props() const {pubkeys, info}: Props = $props()
const chat = deriveChat(pubkeys) const chat = deriveChat(pubkeys)
const draftKey = new DraftKey<{content?: unknown}>(`dm:${$chat?.id}`) const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
const others = remove($pubkey!, pubkeys) const others = remove($pubkey!, pubkeys)
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk))) const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
@@ -338,7 +338,7 @@
{onSubmit} {onSubmit}
{onEscape} {onEscape}
{onEditPrevious} {onEditPrevious}
content={eventToEdit?.content} initialValues={eventToEdit}
draftKey={eventToEdit ? undefined : draftKey} draftKey={eventToEdit ? undefined : draftKey}
disabled={Boolean(missingRelayLists.length)} /> disabled={Boolean(missingRelayLists.length)} />
{/key} {/key}
+27 -9
View File
@@ -12,18 +12,31 @@
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {type DraftKey} from "@app/util/drafts" import {type DraftKey} from "@app/util/drafts"
type Values = {
content?: string | object
}
type Props = { type Props = {
content?: string
disabled?: boolean disabled?: boolean
draftKey?: DraftKey<{content?: unknown}> draftKey?: DraftKey<Values>
onEscape?: () => void onEscape?: () => void
onEditPrevious?: () => void onEditPrevious?: () => void
onSubmit: (event: EventContent) => void onSubmit: (event: EventContent) => void
initialValues?: Values
} }
const {content, disabled = false, draftKey, onEscape, onEditPrevious, onSubmit}: Props = $props() let {
initialValues,
disabled = false,
draftKey,
onEscape,
onEditPrevious,
onSubmit,
}: Props = $props()
const draft = draftKey?.get() if (!initialValues) {
initialValues = draftKey?.get()
}
const autofocus = !isMobile && !disabled const autofocus = !isMobile && !disabled
@@ -67,13 +80,14 @@
ed.chain().clearContent().run() ed.chain().clearContent().run()
} }
const onChange = (json: unknown) => { let content = $state(initialValues?.content ?? "")
draftKey?.set({content: json})
const onChange = (json: object) => {
content = json
} }
const editor = makeEditor({ const editor = makeEditor({
content: content ?? (draft?.content as string | object | undefined), content,
autofocus,
submit, submit,
uploading, uploading,
onChange, onChange,
@@ -81,6 +95,10 @@
encryptFiles: true, encryptFiles: true,
}) })
$effect(() => {
draftKey?.set({content})
})
onMount(async () => { onMount(async () => {
const ed = await editor const ed = await editor
ed.view.dom.addEventListener("keydown", handleKeyDown) ed.view.dom.addEventListener("keydown", handleKeyDown)
@@ -105,7 +123,7 @@
{/if} {/if}
</Button> </Button>
<div class={editorClass} aria-disabled={disabled}> <div class={editorClass} aria-disabled={disabled}>
<EditorContent {editor} /> <EditorContent {autofocus} {editor} />
</div> </div>
<Button <Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send" data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
+27 -40
View File
@@ -23,40 +23,30 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70, uploadFile} from "@app/core/commands" import {canEnforceNip70, uploadFile} from "@app/core/commands"
type Values = {
d: string
title: string
content: string | object
price: number
currency: string
images: (string | File)[]
status: string
topics: string[]
}
type Props = { type Props = {
url: string url: string
h?: string h?: string
header: Snippet header: Snippet
initialValues?: { initialValues?: Values
d?: string
title?: string
content?: string
price?: number
currency?: string
images?: (string | File)[]
status?: string
topics?: string[]
}
} }
let {url, h, header, initialValues}: Props = $props() let {url, h, header, initialValues}: Props = $props()
type ClassifiedDraft = { const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`)
editorContent?: unknown
d?: string
title?: string
content?: string
price?: number
currency?: string
images?: (string | File)[]
status?: string
topics?: string[]
}
const draftKey = new DraftKey<ClassifiedDraft>(`classified:${url}:${h ?? ""}`)
const draft = draftKey.get()
if (!initialValues) { if (!initialValues) {
initialValues = draft initialValues = draftKey.get()
} }
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -85,7 +75,7 @@
} }
const tags = [ const tags = [
["d", initialValues?.d || randomId()], ["d", d],
["title", title], ["title", title],
["summary", content], ["summary", content],
["price", String(price), currency], ["price", String(price), currency],
@@ -136,27 +126,24 @@
} }
} }
const initialContent = initialValues?.content || ""
const onChange = (json: unknown) => {
draftKey.set({editorContent: json, title, price, currency, topics, status, images})
}
const editor = makeEditor({
url,
submit,
onChange,
content: draft?.editorContent ?? initialContent,
})
let loading = $state(false) let loading = $state(false)
const d = $state(initialValues?.d ?? randomId())
let title = $state(initialValues?.title ?? "") let title = $state(initialValues?.title ?? "")
let status = $state(initialValues?.status ?? "active") let status = $state(initialValues?.status ?? "active")
let price = $state(Number(initialValues?.price ?? 0)) let price = $state(initialValues?.price ?? 0)
let currency = $state(initialValues?.currency ?? "SAT") let currency = $state(initialValues?.currency ?? "SAT")
let images = $state<(string | File)[]>(initialValues?.images ?? []) let images = $state(initialValues?.images ?? [])
let topics = $state(uniq(removeUndefined((initialValues?.topics ?? []).map(normalizeTopic)))) let topics = $state(uniq(removeUndefined(initialValues?.topics?.map(normalizeTopic) ?? [])))
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, onChange, content})
$effect(() => { $effect(() => {
draftKey.set({...draftKey.get(), title, price, currency, topics, status, images}) draftKey.set({d, title, status, price, currency, images, topics, content})
}) })
</script> </script>
+19 -17
View File
@@ -13,14 +13,16 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
type Values = {
content?: string | object
}
const {url, event, onClose, onSubmit} = $props() const {url, event, onClose, onSubmit} = $props()
const draftKey = new DraftKey<Values>(`reply:${event.id}`)
const draftKey = new DraftKey<{content?: unknown}>(`reply:${event.id}`) const initialValues = draftKey.get()
const draft = draftKey.get()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
const autofocus = !isMobile
const selectFiles = () => editor.then(ed => ed.commands.selectFiles()) const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
@@ -46,19 +48,19 @@
onSubmit(publishComment({event, content, tags, relays: [url]})) onSubmit(publishComment({event, content, tags, relays: [url]}))
} }
const onChange = (json: unknown) => draftKey.set({content: json})
const editor = makeEditor({
url,
submit,
uploading,
autofocus: !isMobile,
content: draft?.content as string | object | undefined,
onChange,
})
let form: HTMLElement let form: HTMLElement
let spacer: HTMLElement let spacer: HTMLElement
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, uploading, content, onChange})
$effect(() => {
draftKey.set({content})
})
onMount(() => { onMount(() => {
setTimeout(() => { setTimeout(() => {
@@ -86,7 +88,7 @@
<div class="card2 mx-2 my-2 bg-alt shadow-md"> <div class="card2 mx-2 my-2 bg-alt shadow-md">
<div class="relative"> <div class="relative">
<div class="note-editor flex-grow overflow-hidden"> <div class="note-editor flex-grow overflow-hidden">
<EditorContent {editor} /> <EditorContent {autofocus} {editor} />
</div> </div>
<Button <Button
data-tip="Add an image" data-tip="Add an image"
+24 -24
View File
@@ -23,27 +23,24 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70} from "@app/core/commands"
type Values = {
title: string
content: string | object
amount: number
}
type Props = { type Props = {
url: string url: string
h?: string h?: string
initialValues?: { initialValues?: Values
content?: string
amount?: number
}
} }
let {url, h, initialValues}: Props = $props() let {url, h, initialValues}: Props = $props()
type GoalDraft = { const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`)
editorContent?: unknown
content?: string
amount?: number
}
const draftKey = new DraftKey<GoalDraft>(`goal:${url}:${h ?? ""}`)
const draft = draftKey.get()
if (!initialValues) { if (!initialValues) {
initialValues = draft initialValues = draftKey.get()
} }
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -57,7 +54,7 @@
const submit = async () => { const submit = async () => {
if ($uploading) return if ($uploading) return
if (!content) { if (!title) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please provide a title for your funding goal.", message: "Please provide a title for your funding goal.",
@@ -65,9 +62,9 @@
} }
const ed = await editor const ed = await editor
const summary = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
if (!summary.trim()) { if (!content.trim()) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please provide details about your funding goal.", message: "Please provide details about your funding goal.",
@@ -76,7 +73,7 @@
const tags = [ const tags = [
...ed.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
["summary", summary], ["summary", content],
["amount", String(amount)], ["amount", String(amount)],
["relays", url], ["relays", url],
] ]
@@ -91,14 +88,20 @@
publishThunk({ publishThunk({
relays: [url], relays: [url],
event: makeEvent(ZAP_GOAL, {content, tags}), event: makeEvent(ZAP_GOAL, {content: title, tags}),
}) })
draftKey.clear() draftKey.clear()
history.back() history.back()
} }
const onChange = (json: unknown) => draftKey.update({editorContent: json}) let title = $state(initialValues?.title ?? "")
let amount = $state(initialValues?.amount ?? 1000)
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({ const editor = makeEditor({
url, url,
@@ -106,14 +109,11 @@
uploading, uploading,
onChange, onChange,
placeholder: "What's on your mind?", placeholder: "What's on your mind?",
content: draft?.editorContent as string | object | undefined, content,
}) })
let content = $state(initialValues?.content ?? "")
let amount = $state(initialValues?.amount ?? 1000)
$effect(() => { $effect(() => {
draftKey.update({content, amount}) draftKey.update({title, content, amount})
}) })
</script> </script>
@@ -133,7 +133,7 @@
<!-- svelte-ignore a11y_autofocus --> <!-- svelte-ignore a11y_autofocus -->
<input <input
autofocus={!isMobile} autofocus={!isMobile}
bind:value={content} bind:value={title}
class="grow" class="grow"
type="text" type="text"
placeholder="What do funds go towards?" /> placeholder="What do funds go towards?" />
+13 -14
View File
@@ -25,16 +25,16 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import type {PollType} from "@app/util/polls" import type {PollType} from "@app/util/polls"
type DraftOption = { type Option = {
id: string id: string
value: string value: string
} }
type PollDraft = { type Values = {
title?: string title: string
pollType?: PollType pollType: PollType
endsAt?: number endsAt?: number
options?: DraftOption[] options: Option[]
} }
type Props = { type Props = {
@@ -43,9 +43,8 @@
} }
const {url, h}: Props = $props() const {url, h}: Props = $props()
const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`)
const draftKey = new DraftKey<PollDraft>(`poll:${url}:${h ?? ""}`) const initialValues = draftKey.get()
const draft = draftKey.get()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -144,16 +143,16 @@
history.back() history.back()
} }
let title = $state(draft?.title ?? "") let draggedOptionId = $state<string | undefined>()
let pollType = $state<PollType>(draft?.pollType ?? "singlechoice") let title = $state(initialValues?.title ?? "")
let endsAt = $state<number | undefined>(draft?.endsAt) let pollType = $state<PollType>(initialValues?.pollType ?? "singlechoice")
let options = $state<DraftOption[]>( let endsAt = $state<number | undefined>(initialValues?.endsAt)
draft?.options ?? [ let options = $state<Option[]>(
initialValues?.options ?? [
{id: randomId(), value: "Yes"}, {id: randomId(), value: "Yes"},
{id: randomId(), value: "No"}, {id: randomId(), value: "No"},
], ],
) )
let draggedOptionId = $state<string | undefined>()
$effect(() => { $effect(() => {
draftKey.set({title, pollType, endsAt, options}) draftKey.set({title, pollType, endsAt, options})
+20 -11
View File
@@ -15,21 +15,26 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {onDestroy, onMount} from "svelte" import {onDestroy, onMount} from "svelte"
type Values = {
content?: string | object
}
type Props = { type Props = {
url?: string url?: string
h?: string h?: string
content?: string
onEscape?: () => void onEscape?: () => void
onEditPrevious?: () => void onEditPrevious?: () => void
onSubmit: (event: EventContent) => void onSubmit: (event: EventContent) => void
initialValues?: Values
} }
const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props() let {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
type ComposeDraft = {content?: unknown} const draftKey = url || h ? new DraftKey<Values>(`room:${url ?? ""}:${h ?? ""}`) : undefined
const draftKey = url || h ? new DraftKey<ComposeDraft>(`room:${url ?? ""}:${h ?? ""}`) : undefined if (!initialValues) {
const draft = draftKey?.get() initialValues = draftKey?.get()
}
const autofocus = !isMobile const autofocus = !isMobile
@@ -71,21 +76,25 @@
ed.chain().clearContent().run() ed.chain().clearContent().run()
} }
const onChange = (json: unknown) => { let popover: Instance | undefined = $state()
draftKey?.set({content: json}) let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
} }
const editor = makeEditor({ const editor = makeEditor({
url, url,
content: content ?? (draft?.content as string | object | undefined), content,
autofocus,
submit, submit,
uploading, uploading,
onChange, onChange,
aggressive: true, aggressive: true,
}) })
let popover: Instance | undefined = $state() $effect(() => {
draftKey?.set({content})
})
onMount(async () => { onMount(async () => {
const ed = await editor const ed = await editor
@@ -124,7 +133,7 @@
</Tippy> </Tippy>
</div> </div>
<div class="chat-editor flex-grow overflow-hidden"> <div class="chat-editor flex-grow overflow-hidden">
<EditorContent {editor} /> <EditorContent {autofocus} {editor} />
</div> </div>
<Button <Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send" data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
+15 -9
View File
@@ -21,16 +21,19 @@
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands" import {canEnforceNip70} from "@app/core/commands"
type Values = {
content?: string | object
title?: string
}
type Props = { type Props = {
url: string url: string
h?: string h?: string
} }
const {url, h}: Props = $props() const {url, h}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const draftKey = new DraftKey<{content?: unknown; title?: string}>(`thread:${url}:${h ?? ""}`) const initialValues = draftKey.get()
const draft = draftKey.get()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
@@ -78,7 +81,12 @@
history.back() history.back()
} }
const onChange = (json: unknown) => draftKey.update({content: json}) let title = $state(initialValues?.title ?? "")
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({ const editor = makeEditor({
url, url,
@@ -86,13 +94,11 @@
uploading, uploading,
onChange, onChange,
placeholder: "What's on your mind?", placeholder: "What's on your mind?",
content: draft?.content as string | object | undefined, content,
}) })
let title: string = $state(draft?.title ?? "")
$effect(() => { $effect(() => {
draftKey.update({title}) draftKey.update({title, content})
}) })
</script> </script>
+3 -2
View File
@@ -4,9 +4,10 @@
type Props = { type Props = {
editor: Promise<Editor> editor: Promise<Editor>
autofocus?: boolean
} }
const {editor}: Props = $props() const {editor, autofocus}: Props = $props()
let element: HTMLElement let element: HTMLElement
@@ -16,7 +17,7 @@
element?.append(ed.options.element) element?.append(ed.options.element)
} }
if ((ed as any)._shouldAutofocus) { if (autofocus) {
const hasContent = ed.getText().trim().length > 0 const hasContent = ed.getText().trim().length > 0
requestAnimationFrame(() => { requestAnimationFrame(() => {
+1 -6
View File
@@ -25,7 +25,6 @@ import {pushToast} from "@app/util/toast"
export const makeEditor = async ({ export const makeEditor = async ({
encryptFiles = false, encryptFiles = false,
aggressive = false, aggressive = false,
autofocus = false,
charCount, charCount,
content = "", content = "",
onChange, onChange,
@@ -37,10 +36,9 @@ export const makeEditor = async ({
}: { }: {
encryptFiles?: boolean encryptFiles?: boolean
aggressive?: boolean aggressive?: boolean
autofocus?: boolean
charCount?: Writable<number> charCount?: Writable<number>
content?: string | object content?: string | object
onChange?: (json: unknown) => void onChange?: (json: object) => void
placeholder?: string placeholder?: string
url?: string url?: string
submit: () => void submit: () => void
@@ -86,7 +84,6 @@ export const makeEditor = async ({
const ed = new Editor({ const ed = new Editor({
content: typeof content === "string" ? escapeHtml(content) : content, content: typeof content === "string" ? escapeHtml(content) : content,
autofocus: false,
editorProps, editorProps,
element: document.createElement("div"), element: document.createElement("div"),
extensions: [ extensions: [
@@ -148,7 +145,5 @@ export const makeEditor = async ({
}, },
}) })
;(ed as any)._shouldAutofocus = autofocus
return ed return ed
} }
+1 -1
View File
@@ -464,7 +464,7 @@
{onSubmit} {onSubmit}
{onEscape} {onEscape}
{onEditPrevious} {onEditPrevious}
content={eventToEdit?.content} initialValues={eventToEdit}
bind:this={compose} /> bind:this={compose} />
{/key} {/key}
{/if} {/if}
+1 -1
View File
@@ -352,7 +352,7 @@
{onSubmit} {onSubmit}
{onEscape} {onEscape}
{onEditPrevious} {onEditPrevious}
content={eventToEdit?.content} initialValues={eventToEdit}
bind:this={compose} /> bind:this={compose} />
{/key} {/key}
</div> </div>