diff --git a/packages/app/src/Components/Embed/MediaElement.tsx b/packages/app/src/Components/Embed/MediaElement.tsx index 8be5c23e..377494b5 100644 --- a/packages/app/src/Components/Embed/MediaElement.tsx +++ b/packages/app/src/Components/Embed/MediaElement.tsx @@ -1,6 +1,6 @@ import { IMeta } from "@snort/system"; import classNames from "classnames"; -import React, { CSSProperties, useEffect, useMemo, useRef } from "react"; +import React, { CSSProperties, useEffect, useMemo, useRef, useState } from "react"; import { useInView } from "react-intersection-observer"; import { ProxyImg } from "@/Components/ProxyImg"; @@ -45,6 +45,8 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => { return style; }, [imageRef?.current, meta]); + const [alternatives, setAlternatives] = useState>(meta?.fallback ?? []); + const [currentUrl, setCurrentUrl] = useState(url); return (
{ "cursor-pointer": onMediaClick, })}> { })} style={style} ref={imageRef} + onError={() => { + const next = alternatives.at(0); + if (next) { + console.warn("IMG FALLBACK", "Failed to load url, trying next: ", next); + setAlternatives(z => z.filter(y => y !== next)); + setCurrentUrl(next); + } + }} />
); diff --git a/packages/app/src/Components/Event/Create/NoteCreator.tsx b/packages/app/src/Components/Event/Create/NoteCreator.tsx index ee0902b1..5630871b 100644 --- a/packages/app/src/Components/Event/Create/NoteCreator.tsx +++ b/packages/app/src/Components/Event/Create/NoteCreator.tsx @@ -1,9 +1,9 @@ /* eslint-disable max-lines */ import { fetchNip05Pubkey, unixNow } from "@snort/shared"; import { - addExtensionToNip94Url, EventBuilder, EventKind, + Nip94Tags, nip94TagsToIMeta, NostrLink, NostrPrefix, @@ -158,15 +158,32 @@ export function NoteCreator() { extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()])); } - for (const ex of note.otherEvents ?? []) { - const meta = readNip94Tags(ex.tags); - if (!meta.url) continue; - if (!note.note.endsWith("\n")) { - note.note += "\n"; - } - note.note += addExtensionToNip94Url(meta); + // attach 1 link and use other duplicates as fallback urls + for (const [, v] of Object.entries(note.attachments ?? {})) { + const at = v[0]; + note.note += note.note.length > 0 ? `\n${at.url}` : at.url; + console.debug(at); + const n94 = + (at.nip94?.length ?? 0) > 0 + ? readNip94Tags(at.nip94!) + : ({ + url: at.url, + hash: at.sha256, + size: at.size, + mimeType: at.type, + } as Nip94Tags); + + // attach fallbacks + n94.fallback ??= []; + n94.fallback.push( + ...v + .slice(1) + .filter(a => a.url) + .map(a => a.url!), + ); + extraTags ??= []; - extraTags.push(nip94TagsToIMeta(meta)); + extraTags.push(nip94TagsToIMeta(n94)); } // add quote repost @@ -272,20 +289,12 @@ export function NoteCreator() { async function uploadFile(file: File) { try { if (file && uploader) { - const rx = await uploader.upload(file, file.name); + const rx = await uploader.upload(file); note.update(v => { - if (rx.header) { - v.otherEvents ??= []; - v.otherEvents.push(rx.header); - } else if (rx.url) { - v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`; - if (rx.metadata) { - v.extraTags ??= []; - const imeta = nip94TagsToIMeta(rx.metadata); - v.extraTags.push(imeta); - } - } else if (rx?.error) { - v.error = rx.error; + if (rx.url) { + v.attachments ??= {}; + v.attachments[rx.sha256] ??= []; + v.attachments[rx.sha256].push(rx); } }); } @@ -677,31 +686,25 @@ export function NoteCreator() { )} - {(note.otherEvents?.length ?? 0) > 0 && !note.preview && ( + {Object.entries(note.attachments ?? {}).length > 0 && !note.preview && (
- {note.otherEvents - ?.map(a => ({ - event: a, - tags: readNip94Tags(a.tags), - })) - .filter(a => a.tags.url) - .map(a => ( -
- - - note.update( - n => (n.otherEvents = n.otherEvents?.filter(b => readNip94Tags(b.tags).url !== a.tags.url)), - ) - } - /> -
- ))} + {Object.entries(note.attachments ?? {}).map(([k, v]) => ( +
+ + + note.update(n => { + if (n.attachments?.[k]) { + delete n.attachments[k]; + } + return n; + }) + } + /> +
+ ))}
)} {noteCreatorFooter()} @@ -733,8 +736,11 @@ export function NoteCreator() { { note.update(n => { - n.otherEvents ??= []; - n.otherEvents?.push(...files); + for (const x of files) { + n.attachments ??= {}; + n.attachments[x.sha256] ??= []; + n.attachments[x.sha256].push(x); + } n.filePicker = "hidden"; }); }} diff --git a/packages/app/src/Components/Upload/file-picker.tsx b/packages/app/src/Components/Upload/file-picker.tsx index a0305eab..1b0adf28 100644 --- a/packages/app/src/Components/Upload/file-picker.tsx +++ b/packages/app/src/Components/Upload/file-picker.tsx @@ -1,4 +1,3 @@ -import { NostrEvent } from "@snort/system"; import classNames from "classnames"; import { useEffect, useState } from "react"; import { FormattedMessage, FormattedNumber } from "react-intl"; @@ -7,8 +6,7 @@ import useEventPublisher from "@/Hooks/useEventPublisher"; import useImgProxy from "@/Hooks/useImgProxy"; import useLogin from "@/Hooks/useLogin"; import { useMediaServerList } from "@/Hooks/useMediaServerList"; -import { findTag } from "@/Utils"; -import { Nip96Uploader } from "@/Utils/Upload/Nip96"; +import { BlobDescriptor, Blossom } from "@/Utils/Upload/blossom"; import AsyncButton from "../Button/AsyncButton"; @@ -16,12 +14,12 @@ export function MediaServerFileList({ onPicked, cols, }: { - onPicked: (files: Array) => void; + onPicked: (files: Array) => void; cols?: number; }) { const { state } = useLogin(s => ({ v: s.state.version, state: s.state })); const { publisher } = useEventPublisher(); - const [fileList, setFilesList] = useState>([]); + const [fileList, setFilesList] = useState>([]); const [pickedFiles, setPickedFiles] = useState>([]); const servers = useMediaServerList(); @@ -30,11 +28,9 @@ export function MediaServerFileList({ if (!publisher) return; for (const s of servers.servers) { try { - const sx = new Nip96Uploader(s, publisher); - const files = await sx.listFiles(); - if (files?.files) { - res.push(...files.files); - } + const sx = new Blossom(s, publisher); + const files = await sx.list(state.pubkey); + res.push(...files); } catch (e) { console.error(e); } @@ -42,14 +38,12 @@ export function MediaServerFileList({ setFilesList(res); } - function toggleFile(ev: NostrEvent) { - const hash = findTag(ev, "x"); - if (!hash) return; + function toggleFile(b: BlobDescriptor) { setPickedFiles(a => { - if (a.includes(hash)) { - return a.filter(a => a != hash); + if (a.includes(b.sha256)) { + return a.filter(a => a != b.sha256); } else { - return [...a, hash]; + return [...a, b.sha256]; } }); } @@ -58,6 +52,17 @@ export function MediaServerFileList({ listFiles().catch(console.error); }, [servers.servers.length, state?.version]); + const finalFileList = fileList + .sort((a, b) => (b.uploaded ?? 0) - (a.uploaded ?? 0)) + .reduce( + (acc, v) => { + acc[v.sha256] ??= []; + acc[v.sha256].push(v); + return acc; + }, + {} as Record>, + ); + return (
- {fileList.map(a => ( - toggleFile(a)} - checked={pickedFiles.includes(findTag(a, "x") ?? "")} - /> + {Object.entries(finalFileList).map(([k, v]) => ( + toggleFile(v[0])} checked={pickedFiles.includes(k)} /> ))}
onPicked(fileList.filter(a => pickedFiles.includes(findTag(a, "x") ?? "")))}> + onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(a.sha256)))}>
); } -function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: boolean; onClick: () => void }) { - const mime = findTag(file, "m"); - const url = findTag(file, "url"); - const size = findTag(file, "size"); +function ServerFile({ file, checked, onClick }: { file: BlobDescriptor; checked: boolean; onClick: () => void }) { const { proxy } = useImgProxy(); function backgroundImage() { - if (url && (mime?.startsWith("image/") || mime?.startsWith("video/"))) { - return `url(${proxy(url, 512)})`; + if (file.url && (file.type?.startsWith("image/") || file.type?.startsWith("video/"))) { + return `url(${proxy(file.url, 512)})`; } } @@ -103,26 +100,25 @@ function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: bool backgroundImage: backgroundImage(), }}>
-
{file.content.length === 0 ? : file.content}
- {Number(size) > 1024 * 1024 && ( + {file.size > 1024 * 1024 && ( , + n: , }} /> )} - {Number(size) < 1024 * 1024 && ( + {file.size < 1024 * 1024 && ( , + n: , }} /> )}
-
{new Date(file.created_at * 1000).toLocaleString()}
+
{file.uploaded && new Date(file.uploaded * 1000).toLocaleString()}
{ const rb = new RequestBuilder("media-servers-all"); - rb.withFilter().kinds([EventKind.StorageServerList]); + rb.withFilter().kinds([EventKind.BlossomServerList]); return rb; }, []); diff --git a/packages/app/src/Hooks/useMediaServerList.ts b/packages/app/src/Hooks/useMediaServerList.ts index 54dea400..2ee6686b 100644 --- a/packages/app/src/Hooks/useMediaServerList.ts +++ b/packages/app/src/Hooks/useMediaServerList.ts @@ -2,23 +2,21 @@ import { removeUndefined, sanitizeRelayUrl } from "@snort/shared"; import { EventKind, UnknownTag } from "@snort/system"; import { useMemo } from "react"; -import { Nip96Uploader } from "@/Utils/Upload/Nip96"; - import useEventPublisher from "./useEventPublisher"; import useLogin from "./useLogin"; export const DefaultMediaServers = [ - //"https://media.zap.stream", - new UnknownTag(["server", "https://nostr.build/"]), + new UnknownTag(["server", "https://nostr.download/"]), + new UnknownTag(["server", "https://blossom.build/"]), new UnknownTag(["server", "https://nostrcheck.me/"]), - new UnknownTag(["server", "https://files.v0l.io/"]), + new UnknownTag(["server", "https://blossom.primal.net/"]), ]; export function useMediaServerList() { const { publisher } = useEventPublisher(); const { state } = useLogin(s => ({ v: s.state.version, state: s.state })); - let servers = state?.getList(EventKind.StorageServerList) ?? []; + let servers = state?.getList(EventKind.BlossomServerList) ?? []; if (servers.length === 0) { servers = DefaultMediaServers; } @@ -33,14 +31,12 @@ export function useMediaServerList() { const u = sanitizeRelayUrl(s); if (!u) return; - const server = new Nip96Uploader(u, publisher); - await server.loadInfo(); - await state?.addToList(EventKind.StorageServerList, new UnknownTag(["server", u]), true); + await state?.addToList(EventKind.BlossomServerList, new UnknownTag(["server", u]), true); }, removeServer: async (s: string) => { const u = sanitizeRelayUrl(s); if (!u) return; - await state?.removeFromList(EventKind.StorageServerList, new UnknownTag(["server", u]), true); + await state?.removeFromList(EventKind.BlossomServerList, new UnknownTag(["server", u]), true); }, }), [servers], diff --git a/packages/app/src/Pages/settings/media-settings.tsx b/packages/app/src/Pages/settings/media-settings.tsx index 593e19f6..0a01755f 100644 --- a/packages/app/src/Pages/settings/media-settings.tsx +++ b/packages/app/src/Pages/settings/media-settings.tsx @@ -1,4 +1,4 @@ -import { unwrap } from "@snort/shared"; +import { sanitizeRelayUrl, unwrap } from "@snort/shared"; import { EventKind, UnknownTag } from "@snort/system"; import { useState } from "react"; import { FormattedMessage, FormattedNumber } from "react-intl"; @@ -8,35 +8,15 @@ import IconButton from "@/Components/Button/IconButton"; import { CollapsedSection } from "@/Components/Collapsed"; import { RelayFavicon } from "@/Components/Relay/RelaysMetadata"; import useDiscoverMediaServers from "@/Hooks/useDiscoverMediaServers"; -import useEventPublisher from "@/Hooks/useEventPublisher"; import useLogin from "@/Hooks/useLogin"; -import { Nip96Uploader } from "@/Utils/Upload/Nip96"; +import { getRelayName } from "@/Utils"; export default function MediaSettingsPage() { const { state } = useLogin(s => ({ v: s.state.version, state: s.state })); - const { publisher } = useEventPublisher(); - const list = state.getList(EventKind.StorageServerList); + const list = state.getList(EventKind.BlossomServerList); const [newServer, setNewServer] = useState(""); - const [error, setError] = useState(""); const knownServers = useDiscoverMediaServers(); - async function validateServer(url: string) { - if (!publisher) return; - - setError(""); - try { - const svc = new Nip96Uploader(url, publisher); - await svc.loadInfo(); - - return true; - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } - return false; - } - } - return (
@@ -57,7 +37,7 @@ export default function MediaSettingsPage() { size: 15, }} onClick={async () => { - await state.removeFromList(EventKind.StorageServerList, [new UnknownTag(["server", addr])], true); + await state.removeFromList(EventKind.BlossomServerList, [new UnknownTag(["server", addr])], true); }} />
@@ -83,9 +63,9 @@ export default function MediaSettingsPage() { /> { - if (await validateServer(newServer)) { + if (sanitizeRelayUrl(newServer)) { await state.addToList( - EventKind.StorageServerList, + EventKind.BlossomServerList, [new UnknownTag(["server", new URL(newServer).toString()])], true, ); @@ -95,7 +75,6 @@ export default function MediaSettingsPage() {
- {error && {error}}
- {k} + {getRelayName(k)} @@ -136,9 +115,7 @@ export default function MediaSettingsPage() { { - if (await validateServer(k)) { - await state.addToList(EventKind.StorageServerList, [new UnknownTag(["server", k])], true); - } + await state.addToList(EventKind.BlossomServerList, [new UnknownTag(["server", k])], true); }}> diff --git a/packages/app/src/State/NoteCreator.ts b/packages/app/src/State/NoteCreator.ts index 47622445..cef3ef6e 100644 --- a/packages/app/src/State/NoteCreator.ts +++ b/packages/app/src/State/NoteCreator.ts @@ -3,6 +3,8 @@ import { NostrEvent, TaggedNostrEvent } from "@snort/system"; import { ZapTarget } from "@snort/wallet"; import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector"; +import { BlobDescriptor } from "@/Utils/Upload/blossom"; + interface NoteCreatorDataSnapshot { show: boolean; note: string; @@ -17,6 +19,7 @@ interface NoteCreatorDataSnapshot { sensitive?: string; pollOptions?: Array; otherEvents?: Array; + attachments?: Record>; extraTags?: Array>; sending?: Array; sendStarted: boolean; diff --git a/packages/app/src/Utils/Login/MultiAccountStore.ts b/packages/app/src/Utils/Login/MultiAccountStore.ts index 7da29808..4ac0541b 100644 --- a/packages/app/src/Utils/Login/MultiAccountStore.ts +++ b/packages/app/src/Utils/Login/MultiAccountStore.ts @@ -107,7 +107,7 @@ export class MultiAccountStore extends ExternalStore { }, stateObj, ); - stateClass.checkIsStandardList(EventKind.StorageServerList); // track nip96 list + stateClass.checkIsStandardList(EventKind.BlossomServerList); // track blossom list stateClass.on("change", () => this.#save()); if (v.state instanceof UserState) { v.state.destroy(); @@ -194,7 +194,7 @@ export class MultiAccountStore extends ExternalStore { stalker: stalker ?? false, } as LoginSession; - newSession.state!.checkIsStandardList(EventKind.StorageServerList); // track nip96 list + newSession.state!.checkIsStandardList(EventKind.BlossomServerList); // track blossom list newSession.state!.on("change", () => this.#save()); const pub = createPublisher(newSession); if (pub) { @@ -243,7 +243,7 @@ export class MultiAccountStore extends ExternalStore { appdataId: "snort", }), } as LoginSession; - newSession.state!.checkIsStandardList(EventKind.StorageServerList); // track nip96 list + newSession.state!.checkIsStandardList(EventKind.BlossomServerList); // track blossom list newSession.state!.on("change", () => this.#save()); if ("nostr_os" in window && window?.nostr_os) { diff --git a/packages/app/src/Utils/Upload/Nip96.ts b/packages/app/src/Utils/Upload/Nip96.ts deleted file mode 100644 index 6a32e2ed..00000000 --- a/packages/app/src/Utils/Upload/Nip96.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { base64 } from "@scure/base"; -import { throwIfOffline } from "@snort/shared"; -import { addExtensionToNip94Url, EventKind, EventPublisher, NostrEvent, readNip94Tags } from "@snort/system"; - -import { UploadResult } from "."; - -export class Nip96Uploader { - #info?: Nip96Info; - - constructor( - readonly url: string, - readonly publisher: EventPublisher, - ) { - this.url = new URL(this.url).toString(); - } - - get progress() { - return []; - } - - async loadInfo() { - const u = new URL(this.url); - - const rsp = await fetch(`${u.protocol}//${u.host}/.well-known/nostr/nip96.json`); - this.#info = (await rsp.json()) as Nip96Info; - return this.#info; - } - - async listFiles(page = 0, count = 50) { - const rsp = await this.#req(`?page=${page}&count=${count}`, "GET"); - if (rsp.ok) { - return (await rsp.json()) as Nip96FileList; - } - } - - async upload(file: File | Blob, filename: string): Promise { - const fd = new FormData(); - fd.append("size", file.size.toString()); - fd.append("caption", filename); - fd.append("content_type", file.type); - fd.append("file", file); - - const rsp = await this.#req("", "POST", fd); - if (rsp.ok) { - const data = (await rsp.json()) as Nip96Result; - if (data.status === "success") { - const meta = readNip94Tags(data.nip94_event.tags); - return { - url: addExtensionToNip94Url(meta), - header: data.nip94_event, - metadata: meta, - }; - } - return { - error: data.message, - }; - } else { - const text = await rsp.text(); - try { - const obj = JSON.parse(text) as Nip96Result; - return { - error: obj.message, - }; - } catch { - return { - error: `Upload failed: ${text}`, - }; - } - } - } - - async #req(path: string, method: "GET" | "POST" | "DELETE", body?: BodyInit) { - throwIfOffline(); - const auth = async (url: string, method: string) => { - const auth = await this.publisher.generic(eb => { - return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]); - }); - return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`; - }; - - const info = this.#info ?? (await this.loadInfo()); - let u = info.api_url; - if (u.startsWith("/")) { - u = `${this.url}${u.slice(1)}`; - } - u += path; - return await fetch(u, { - method, - body, - headers: { - accept: "application/json", - authorization: await auth(u, method), - }, - }); - } -} - -export interface Nip96Info { - api_url: string; - download_url?: string; -} - -export interface Nip96Result { - status: string; - message: string; - processing_url?: string; - nip94_event: NostrEvent; -} - -export interface Nip96FileList { - count: number; - total: number; - page: number; - files: Array; -} diff --git a/packages/app/src/Utils/Upload/blossom.ts b/packages/app/src/Utils/Upload/blossom.ts new file mode 100644 index 00000000..942ce150 --- /dev/null +++ b/packages/app/src/Utils/Upload/blossom.ts @@ -0,0 +1,134 @@ +import { base64, bytesToString } from "@scure/base"; +import { throwIfOffline, unixNow } from "@snort/shared"; +import { EventKind, EventPublisher } from "@snort/system"; + +export interface BlobDescriptor { + url?: string; + sha256: string; + size: number; + type?: string; + uploaded?: number; + nip94?: Array>; +} + +export class Blossom { + constructor( + readonly url: string, + readonly publisher: EventPublisher, + ) { + this.url = new URL(this.url).toString(); + } + + async upload(file: File) { + const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer()); + const tags = [["x", bytesToString("hex", new Uint8Array(hash))]]; + + const rsp = await this.#req("upload", "PUT", "upload", file, tags); + if (rsp.ok) { + const ret = (await rsp.json()) as BlobDescriptor; + this.#fixTags(ret); + return ret; + } else { + const text = await rsp.text(); + throw new Error(text); + } + } + + async media(file: File) { + const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer()); + const tags = [["x", bytesToString("hex", new Uint8Array(hash))]]; + + const rsp = await this.#req("media", "PUT", "media", file, tags); + if (rsp.ok) { + const ret = (await rsp.json()) as BlobDescriptor; + this.#fixTags(ret); + return ret; + } else { + const text = await rsp.text(); + throw new Error(text); + } + } + + async mirror(url: string) { + const rsp = await this.#req("mirror", "PUT", "mirror", JSON.stringify({ url }), undefined, { + "content-type": "application/json", + }); + if (rsp.ok) { + const ret = (await rsp.json()) as BlobDescriptor; + this.#fixTags(ret); + return ret; + } else { + const text = await rsp.text(); + throw new Error(text); + } + } + + async list(pk: string) { + const rsp = await this.#req(`list/${pk}`, "GET", "list"); + if (rsp.ok) { + const ret = (await rsp.json()) as Array; + ret.forEach(a => this.#fixTags(a)); + return ret; + } else { + const text = await rsp.text(); + throw new Error(text); + } + } + + async delete(id: string) { + const tags = [["x", id]]; + + const rsp = await this.#req(id, "DELETE", "delete", undefined, tags); + if (!rsp.ok) { + const text = await rsp.text(); + throw new Error(text); + } + } + + #fixTags(r: BlobDescriptor) { + if (!r.nip94) return; + if (Array.isArray(r.nip94)) return; + // blossom.band invalid response + if (r.nip94 && "tags" in r.nip94) { + r.nip94 = r.nip94["tags"]; + return; + } + r.nip94 = Object.entries(r.nip94 as Record); + } + + async #req( + path: string, + method: "GET" | "POST" | "DELETE" | "PUT", + term: string, + body?: BodyInit, + tags?: Array>, + headers?: Record, + ) { + throwIfOffline(); + + const url = `${this.url}${path}`; + const now = unixNow(); + const auth = async (url: string, method: string) => { + const auth = await this.publisher.generic(eb => { + eb.kind(24_242 as EventKind) + .tag(["u", url]) + .tag(["method", method.toLowerCase()]) + .tag(["t", term]) + .tag(["expiration", (now + 10).toString()]); + tags?.forEach(t => eb.tag(t)); + return eb; + }); + return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`; + }; + + return await fetch(url, { + method, + body, + headers: { + ...headers, + accept: "application/json", + authorization: await auth(url, method), + }, + }); + } +} diff --git a/packages/app/src/Utils/Upload/index.ts b/packages/app/src/Utils/Upload/index.ts index 46500598..ed6e8a9c 100644 --- a/packages/app/src/Utils/Upload/index.ts +++ b/packages/app/src/Utils/Upload/index.ts @@ -5,7 +5,7 @@ import { useMediaServerList } from "@/Hooks/useMediaServerList"; import { bech32ToHex, randomSample } from "@/Utils"; import { KieranPubKey } from "@/Utils/Const"; -import { Nip96Uploader } from "./Nip96"; +import { Blossom } from "./blossom"; export interface UploadResult { url?: string; @@ -65,8 +65,8 @@ export default function useFileUpload(privKey?: string) { const pub = privKey ? EventPublisher.privateKey(privKey) : publisher; if (servers.length > 0 && pub) { const random = randomSample(servers, 1)[0]; - return new Nip96Uploader(random, pub); + return new Blossom(random, pub); } else if (pub) { - return new Nip96Uploader("https://nostr.build", pub); + return new Blossom("https://blossom.build", pub); } } diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index aaabae76..cffc5508 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -219,9 +219,6 @@ "3gOsZq": { "defaultMessage": "Translators" }, - "3kbIhS": { - "defaultMessage": "Untitled" - }, "3qnJlS": { "defaultMessage": "You are voting with {amount} sats" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 2f667a91..3bd1be00 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -72,7 +72,6 @@ "3adEeb": "{n} viewers", "3cc4Ct": "Light", "3gOsZq": "Translators", - "3kbIhS": "Untitled", "3qnJlS": "You are voting with {amount} sats", "3t3kok": "{n,plural,=1{{n} new note} other{{n} new notes}}", "3tVy+Z": "{n} Followers", diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts index 0e817d94..842acaa2 100644 --- a/packages/system/src/event-kind.ts +++ b/packages/system/src/event-kind.ts @@ -38,6 +38,7 @@ const enum EventKind { SearchRelaysList = 10_007, // NIP-51 InterestsList = 10_015, // NIP-51 EmojisList = 10_030, // NIP-51 + BlossomServerList = 10_063, StorageServerList = 10_096, // NIP-96 server list FollowSet = 30_000, // NIP-51 diff --git a/packages/system/src/impl/nip92.ts b/packages/system/src/impl/nip92.ts index bc61f95d..0cece68f 100644 --- a/packages/system/src/impl/nip92.ts +++ b/packages/system/src/impl/nip92.ts @@ -9,6 +9,9 @@ export function readNip94TagsFromIMeta(tag: Array) { } export function nip94TagsToIMeta(meta: Nip94Tags) { + if (!meta.url) { + throw new Error("URL is required!"); + } const ret: Array = ["imeta"]; const ifPush = (key: string, value?: string | number) => { if (value) { diff --git a/packages/system/src/text.ts b/packages/system/src/text.ts index b4da8cdb..eb8ee6b2 100644 --- a/packages/system/src/text.ts +++ b/packages/system/src/text.ts @@ -12,6 +12,7 @@ import { import { NostrLink, validateNostrLink } from "./nostr-link"; import { splitByUrl } from "./utils"; import { IMeta } from "./nostr"; +import { Nip94Tags, readNip94TagsFromIMeta } from "."; export interface ParsedFragment { type: @@ -254,33 +255,14 @@ function extractMarkdownCode(fragments: Fragment[]): (string | ParsedFragment)[] } export function parseIMeta(tags: Array>) { - let ret: Record | undefined; + let ret: Record | undefined; const imetaTags = tags.filter(a => a[0] === "imeta"); for (const imetaTag of imetaTags) { - ret ??= {}; - let imeta: IMeta = {}; - let url = ""; - for (const t of imetaTag.slice(1)) { - const [k, v] = t.split(" "); - if (k === "url") { - url = v; - } - if (k === "dim") { - const [w, h] = v.split("x"); - imeta.height = Number(h); - imeta.width = Number(w); - } - if (k === "blurhash") { - imeta.blurHash = v; - } - if (k === "x") { - imeta.sha256 = v; - } - if (k === "alt") { - imeta.alt = v; - } + const meta = readNip94TagsFromIMeta(imetaTag); + if (meta.url) { + ret ??= {}; + ret[meta.url] = meta; } - ret[url] = imeta; } return ret; }