Use space as blossom server if supported

This commit is contained in:
Jon Staab
2025-04-29 12:24:20 -07:00
parent 5a7750a91b
commit 6ddba63ff9
7 changed files with 98 additions and 38 deletions
+7 -7
View File
@@ -36,7 +36,9 @@
const back = () => history.back() const back = () => history.back()
const submit = () => { const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const submit = async () => {
if ($uploading) return if ($uploading) return
if (!title) { if (!title) {
@@ -60,8 +62,9 @@
}) })
} }
const ed = await editor
const event = createEvent(EVENT_TIME, { const event = createEvent(EVENT_TIME, {
content: editor.getText({blockSeparator: "\n"}).trim(), content: ed.getText({blockSeparator: "\n"}).trim(),
tags: [ tags: [
["d", initialValues?.d || randomId()], ["d", initialValues?.d || randomId()],
["title", title], ["title", title],
@@ -69,7 +72,7 @@
["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()]),
...editor.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
tagRoom(GENERAL, url), tagRoom(GENERAL, url),
PROTECTED, PROTECTED,
], ],
@@ -119,10 +122,7 @@
<div class="input-editor flex-grow overflow-hidden"> <div class="input-editor flex-grow overflow-hidden">
<EditorContent {editor} /> <EditorContent {editor} />
</div> </div>
<Button <Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}>
data-tip="Add an image"
class="center btn tooltip"
onclick={() => editor.chain().selectFiles().run()}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
+7 -6
View File
@@ -18,21 +18,22 @@
const uploading = writable(false) const uploading = writable(false)
export const focus = () => editor.chain().focus().run() export const focus = () => editor.then(ed => ed.chain().focus().run())
const uploadFiles = () => editor.chain().selectFiles().run() const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const submit = () => { const submit = async () => {
if ($uploading) return if ($uploading) return
const content = editor.getText({blockSeparator: "\n"}).trim() const ed = await editor
const tags = editor.storage.nostr.getEditorTags() const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
if (!content) return if (!content) return
onSubmit({content, tags}) onSubmit({content, tags})
editor.chain().clearContent().run() ed.chain().clearContent().run()
} }
const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true}) const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true})
+7 -4
View File
@@ -16,11 +16,14 @@
const uploading = writable(false) const uploading = writable(false)
const submit = () => { const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
const submit = async () => {
if ($uploading) return if ($uploading) return
const content = editor.getText({blockSeparator: "\n"}).trim() const ed = await editor
const tags = [...editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED] const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = [...ed.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
if (!content) { if (!content) {
return pushToast({ return pushToast({
@@ -68,7 +71,7 @@
<Button <Button
data-tip="Add an image" data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2" class="tooltip tooltip-left absolute bottom-1 right-2"
onclick={editor.commands.selectFiles}> onclick={selectFiles}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
+7 -4
View File
@@ -19,7 +19,9 @@
const back = () => history.back() const back = () => history.back()
const submit = () => { const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
const submit = async () => {
if ($uploading) return if ($uploading) return
if (!title) { if (!title) {
@@ -29,7 +31,8 @@
}) })
} }
const content = editor.getText({blockSeparator: "\n"}).trim() const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
if (!content.trim()) { if (!content.trim()) {
return pushToast({ return pushToast({
@@ -39,7 +42,7 @@
} }
const tags = [ const tags = [
...editor.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
tagRoom(GENERAL, url), tagRoom(GENERAL, url),
["title", title], ["title", title],
PROTECTED, PROTECTED,
@@ -97,7 +100,7 @@
<Button <Button
data-tip="Add an image" data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2" class="tooltip tooltip-left absolute bottom-1 right-2"
onclick={editor.commands.selectFiles}> onclick={selectFiles}>
{#if $uploading} {#if $uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}
+15 -8
View File
@@ -1,22 +1,29 @@
<script lang="ts"> <script lang="ts">
import {Editor} from "@welshman/editor"
import {onDestroy, onMount} from "svelte" import {onDestroy, onMount} from "svelte"
const {editor} = $props() type Props = {
editor: Promise<Editor>
}
const {editor}: Props = $props()
let element: HTMLElement let element: HTMLElement
onMount(() => { onMount(() => {
if (editor.options.element) { editor.then(({options}) => {
element?.append(editor.options.element) if (options.element) {
} element?.append(options.element)
}
if (editor.options.autofocus) { if (options.autofocus) {
;(element?.querySelector("[contenteditable]") as HTMLElement)?.focus() ;(element?.querySelector("[contenteditable]") as HTMLElement)?.focus()
} }
})
}) })
onDestroy(() => { onDestroy(() => {
editor.destroy() editor.then($editor => $editor.destroy())
}) })
</script> </script>
+51 -9
View File
@@ -2,15 +2,58 @@ import {mount} from "svelte"
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import {get} from "svelte/store" import {get} from "svelte/store"
import type {StampedEvent} from "@welshman/util" import type {StampedEvent} from "@welshman/util"
import {getTagValue, getListTags} from "@welshman/util" import {makeEvent, getTagValues, getListTags, BLOSSOM_AUTH} from "@welshman/util"
import {simpleCache, removeNil, now} from "@welshman/lib"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {signer, profileSearch, userBlossomServers} from "@welshman/app" import {signer, profileSearch, userBlossomServers} from "@welshman/app"
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor" import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
import {makeMentionNodeView} from "./MentionNodeView" import {makeMentionNodeView} from "./MentionNodeView"
import ProfileSuggestion from "./ProfileSuggestion.svelte" import ProfileSuggestion from "./ProfileSuggestion.svelte"
export const getUploadUrl = () => export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
getTagValue("server", getListTags(userBlossomServers.get())) || "https://cdn.satellite.earth" try {
const event = await signer.get()!.sign(
makeEvent(BLOSSOM_AUTH, {
tags: [
["t", "upload"],
["server", url],
["expiration", String(now() + 30)],
],
}),
)
const res = await fetch(url + "/upload", {
method: "head",
headers: {
Authorization: `Nostr ${btoa(JSON.stringify(event))}`,
"X-Content-Type": "text/plain",
"X-Content-Length": "1",
"X-SHA-256": "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac",
},
})
return res.status === 200
} catch (e) {
if (!String(e).includes("Failed to fetch")) {
console.error(e)
}
}
})
export const getUploadUrl = async (spaceUrl?: string) => {
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
const allUrls = removeNil([spaceUrl, ...userUrls])
for (let url of allUrls) {
url = url.replace(/wss?:\/\//, "https://")
if (await hasBlossomSupport(url)) {
return url
}
}
return "https://cdn.satellite.earth"
}
export const signWithAssert = async (template: StampedEvent) => { export const signWithAssert = async (template: StampedEvent) => {
const event = await signer.get().sign(template) const event = await signer.get().sign(template)
@@ -18,7 +61,7 @@ export const signWithAssert = async (template: StampedEvent) => {
return event! return event!
} }
export const makeEditor = ({ export const makeEditor = async ({
aggressive = false, aggressive = false,
autofocus = false, autofocus = false,
charCount, charCount,
@@ -26,7 +69,6 @@ export const makeEditor = ({
placeholder = "", placeholder = "",
url, url,
submit, submit,
uploadUrl = getUploadUrl(),
uploading, uploading,
wordCount, wordCount,
}: { }: {
@@ -37,11 +79,10 @@ export const makeEditor = ({
placeholder?: string placeholder?: string
url?: string url?: string
submit: () => void submit: () => void
uploadUrl?: string
uploading?: Writable<boolean> uploading?: Writable<boolean>
wordCount?: Writable<number> wordCount?: Writable<number>
}) => }) => {
new Editor({ return new Editor({
content, content,
autofocus, autofocus,
element: document.createElement("div"), element: document.createElement("div"),
@@ -50,7 +91,7 @@ export const makeEditor = ({
submit, submit,
sign: signWithAssert, sign: signWithAssert,
defaultUploadType: "blossom", defaultUploadType: "blossom",
defaultUploadUrl: uploadUrl, defaultUploadUrl: await getUploadUrl(url),
extensions: { extensions: {
placeholder: { placeholder: {
config: { config: {
@@ -101,3 +142,4 @@ export const makeEditor = ({
charCount?.set(editor.storage.wordCount.chars) charCount?.set(editor.storage.wordCount.chars)
}, },
}) })
}
+4
View File
@@ -10,6 +10,7 @@
import SpaceAuthError from "@app/components/SpaceAuthError.svelte" import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {getUploadUrl} from "@app/editor"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands" import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands"
import {decodeRelay, userRoomsByUrl} from "@app/state" import {decodeRelay, userRoomsByUrl} from "@app/state"
@@ -51,6 +52,9 @@
onMount(() => { onMount(() => {
checkConnection() checkConnection()
// Prime our cache so inputs show up quickly
getUploadUrl(url)
const relays = [url] const relays = [url]
const since = ago(WEEK) const since = ago(WEEK)
const controller = new AbortController() const controller = new AbortController()