From 5763d91e8a4905591aae132c9f75af6ca97196b9 Mon Sep 17 00:00:00 2001 From: kieran Date: Tue, 14 May 2024 13:16:03 +0100 Subject: [PATCH] feat: NIP-96 server list --- packages/app/src/Pages/settings/Menu/Menu.tsx | 6 ++ .../app/src/Pages/settings/Preferences.tsx | 13 --- packages/app/src/Pages/settings/Routes.tsx | 5 + .../app/src/Pages/settings/media-settings.tsx | 98 +++++++++++++++++++ .../app/src/Utils/Login/MultiAccountStore.ts | 4 + packages/app/src/Utils/Login/Preferences.ts | 5 - packages/app/src/Utils/Upload/Nip96.ts | 4 +- packages/app/src/Utils/Upload/index.ts | 92 +++++++++-------- packages/app/src/lang.json | 15 ++- packages/app/src/translations/en.json | 5 +- packages/system/src/event-kind.ts | 1 + packages/system/src/query.ts | 2 +- packages/system/src/sync/diff-sync.ts | 6 +- packages/system/src/user-state.ts | 8 +- 14 files changed, 192 insertions(+), 72 deletions(-) create mode 100644 packages/app/src/Pages/settings/media-settings.tsx diff --git a/packages/app/src/Pages/settings/Menu/Menu.tsx b/packages/app/src/Pages/settings/Menu/Menu.tsx index 94cfc0e7..747c9066 100644 --- a/packages/app/src/Pages/settings/Menu/Menu.tsx +++ b/packages/app/src/Pages/settings/Menu/Menu.tsx @@ -125,6 +125,12 @@ const SettingsIndex = () => { message: , path: "cache", }, + { + icon: "camera-plus", + iconBg: "bg-lime-500", + message: , + path: "media", + }, ], }, { diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx index 0c9c90a9..7d95d3ca 100644 --- a/packages/app/src/Pages/settings/Preferences.tsx +++ b/packages/app/src/Pages/settings/Preferences.tsx @@ -480,19 +480,6 @@ const PreferencesPage = () => { - {pref.fileUploader === "nip96" && ( - <> - - - - setPref({ ...pref, nip96Server: e.target.value })} - placeholder="https://my-nip96-server.com/" - /> - - )}
diff --git a/packages/app/src/Pages/settings/Routes.tsx b/packages/app/src/Pages/settings/Routes.tsx index 4d814275..4aff8f14 100644 --- a/packages/app/src/Pages/settings/Routes.tsx +++ b/packages/app/src/Pages/settings/Routes.tsx @@ -4,6 +4,7 @@ import AccountsPage from "@/Pages/settings/Accounts"; import { CacheSettings } from "@/Pages/settings/Cache"; import { ManageHandleRoutes } from "@/Pages/settings/handle"; import ExportKeys from "@/Pages/settings/Keys"; +import MediaSettingsPage from "@/Pages/settings/media-settings"; import Menu from "@/Pages/settings/Menu/Menu"; import ModerationSettings from "@/Pages/settings/Moderation"; import Notifications from "@/Pages/settings/Notifications"; @@ -65,6 +66,10 @@ export default [ path: "cache", element: , }, + { + path: "media", + element: , + }, { path: "invite", element: , diff --git a/packages/app/src/Pages/settings/media-settings.tsx b/packages/app/src/Pages/settings/media-settings.tsx new file mode 100644 index 00000000..434f3fd7 --- /dev/null +++ b/packages/app/src/Pages/settings/media-settings.tsx @@ -0,0 +1,98 @@ +import { unwrap } from "@snort/shared"; +import { EventKind, UnknownTag } from "@snort/system"; +import { useState } from "react"; +import { FormattedMessage } from "react-intl"; + +import AsyncButton from "@/Components/Button/AsyncButton"; +import IconButton from "@/Components/Button/IconButton"; +import useEventPublisher from "@/Hooks/useEventPublisher"; +import useLogin from "@/Hooks/useLogin"; +import { Nip96Uploader } from "@/Utils/Upload/Nip96"; + +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 [newServer, setNewServer] = useState(""); + const [error, setError] = useState(""); + + async function validateServer() { + if (!publisher) return; + + setError(""); + try { + const svc = new Nip96Uploader(newServer, publisher); + await svc.loadInfo(); + + return true; + } catch (e) { + if (e instanceof Error) { + setError(e.message); + } + return false; + } + } + + return ( +
+
+ +
+

+ +

+
+ {list.map(a => { + const [, addr] = unwrap(a.toEventTag()); + return ( +
+ {addr} + { + await state.removeFromList(EventKind.StorageServerList, [new UnknownTag(["server", addr])], true); + }} + /> +
+ ); + })} + {list.length === 0 && ( + + + + )} +
+
+
+ +
+
+ setNewServer(e.target.value)} + /> + { + if (await validateServer()) { + await state.addToList( + EventKind.StorageServerList, + [new UnknownTag(["server", new URL(newServer).toString()])], + true, + ); + setNewServer(""); + } + }}> + + +
+ {error && {error}} +
+
+ ); +} diff --git a/packages/app/src/Utils/Login/MultiAccountStore.ts b/packages/app/src/Utils/Login/MultiAccountStore.ts index e75cc685..f72a5d8c 100644 --- a/packages/app/src/Utils/Login/MultiAccountStore.ts +++ b/packages/app/src/Utils/Login/MultiAccountStore.ts @@ -3,6 +3,7 @@ import * as utils from "@noble/curves/abstract/utils"; import * as secp from "@noble/curves/secp256k1"; import { ExternalStore, unwrap } from "@snort/shared"; import { + EventKind, EventPublisher, HexKey, KeyStorage, @@ -111,6 +112,7 @@ export class MultiAccountStore extends ExternalStore { }, stateObj, ); + stateClass.checkIsStandardList(EventKind.StorageServerList); // track nip96 list stateClass.on("change", () => this.#save()); v.state = stateClass; @@ -197,6 +199,7 @@ export class MultiAccountStore extends ExternalStore { stalker: stalker ?? false, } as LoginSession; + newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list newSession.state.on("change", () => this.#save()); const pub = createPublisher(newSession); if (pub) { @@ -246,6 +249,7 @@ export class MultiAccountStore extends ExternalStore { appdataId: "snort", }), } as LoginSession; + newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list newSession.state.on("change", () => this.#save()); if ("nostr_os" in window && window?.nostr_os) { diff --git a/packages/app/src/Utils/Login/Preferences.ts b/packages/app/src/Utils/Login/Preferences.ts index 93573897..c3a02d96 100644 --- a/packages/app/src/Utils/Login/Preferences.ts +++ b/packages/app/src/Utils/Login/Preferences.ts @@ -48,11 +48,6 @@ export interface UserPreferences { */ fileUploader: "void.cat" | "nostr.build" | "nostrimg.com" | "void.cat-NIP96" | "nostrcheck.me" | "nip96"; - /** - * Custom file server to upload files to - */ - nip96Server?: string; - /** * Use imgproxy to optimize images */ diff --git a/packages/app/src/Utils/Upload/Nip96.ts b/packages/app/src/Utils/Upload/Nip96.ts index d04e1754..78c08c61 100644 --- a/packages/app/src/Utils/Upload/Nip96.ts +++ b/packages/app/src/Utils/Upload/Nip96.ts @@ -62,8 +62,8 @@ export class Nip96Uploader implements Uploader { ?.split("x"); const mime = data.nip94_event.tags.find(a => a[0] === "m")?.at(1) ?? ""; let url = data.nip94_event.tags.find(a => a[0] === "url")?.at(1) ?? ""; - if(!url.match(FileExtensionRegex) && mime) { - switch(mime) { + if (!url.match(FileExtensionRegex) && mime) { + switch (mime) { case "image/webp": { url += ".webp"; break; diff --git a/packages/app/src/Utils/Upload/index.ts b/packages/app/src/Utils/Upload/index.ts index 9c09bf93..a85ef0e0 100644 --- a/packages/app/src/Utils/Upload/index.ts +++ b/packages/app/src/Utils/Upload/index.ts @@ -1,10 +1,12 @@ -import { NostrEvent } from "@snort/system"; +import { removeUndefined } from "@snort/shared"; +import { EventKind, NostrEvent } from "@snort/system"; import { useState } from "react"; import { v4 as uuid } from "uuid"; import useEventPublisher from "@/Hooks/useEventPublisher"; +import useLogin from "@/Hooks/useLogin"; import usePreferences from "@/Hooks/usePreferences"; -import { bech32ToHex, unwrap } from "@/Utils"; +import { bech32ToHex, randomSample, unwrap } from "@/Utils"; import { KieranPubKey } from "@/Utils/Const"; import NostrBuild from "@/Utils/Upload/NostrBuild"; import NostrImg from "@/Utils/Upload/NostrImg"; @@ -48,6 +50,10 @@ export const UploaderServices = [ name: "nostrimg.com", owner: bech32ToHex("npub1xv6axulxcx6mce5mfvfzpsy89r4gee3zuknulm45cqqpmyw7680q5pxea6"), }, + { + name: "nostrcheck.me", + owner: bech32ToHex("npub138s5hey76qrnm2pmv7p8nnffhfddsm8sqzm285dyc0wy4f8a6qkqtzx624"), + }, ]; export interface Uploader { @@ -65,14 +71,44 @@ export interface UploadProgress { export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined; export default function useFileUpload(): Uploader { - const { fileUploader, nip96Server } = usePreferences(s => ({ - fileUploader: s.fileUploader, - nip96Server: s.nip96Server, - })); + const fileUploader = usePreferences(s => s.fileUploader); + const { state } = useLogin(s => ({ v: s.state.version, state: s.state })); const { publisher } = useEventPublisher(); const [progress, setProgress] = useState>([]); const [stage, setStage] = useState(); + const defaultUploader = { + upload: async (f, n) => { + const id = uuid(); + setProgress(s => [ + ...s, + { + id, + file: f, + progress: 0, + stage: undefined, + }, + ]); + const px = (n: number) => { + setProgress(s => + s.map(v => + v.id === id + ? { + ...v, + progress: n, + } + : v, + ), + ); + }; + const ret = await VoidCat(f, n, publisher, px, s => setStage(s)); + setProgress(s => s.filter(a => a.id !== id)); + return ret; + }, + progress, + stage, + } as Uploader; + switch (fileUploader) { case "nostr.build": { return { @@ -80,9 +116,6 @@ export default function useFileUpload(): Uploader { progress: [], } as Uploader; } - case "nip96": { - return new Nip96Uploader(unwrap(nip96Server), unwrap(publisher)); - } case "void.cat-NIP96": { return new Nip96Uploader("https://void.cat/nostr", unwrap(publisher)); } @@ -95,38 +128,17 @@ export default function useFileUpload(): Uploader { progress: [], } as Uploader; } + case "nip96": { + const servers = removeUndefined(state.getList(EventKind.StorageServerList).map(a => a.toEventTag()?.at(1))); + if (servers.length > 0) { + const random = randomSample(servers, 1)[0]; + return new Nip96Uploader(random, unwrap(publisher)); + } else { + return defaultUploader; + } + } default: { - return { - upload: async (f, n) => { - const id = uuid(); - setProgress(s => [ - ...s, - { - id, - file: f, - progress: 0, - stage: undefined, - }, - ]); - const px = (n: number) => { - setProgress(s => - s.map(v => - v.id === id - ? { - ...v, - progress: n, - } - : v, - ), - ); - }; - const ret = await VoidCat(f, n, publisher, px, s => setStage(s)); - setProgress(s => s.filter(a => a.id !== id)); - return ret; - }, - progress, - stage, - } as Uploader; + return defaultUploader; } } } diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index a8b61507..cf36ef3b 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -517,6 +517,9 @@ "FvanT6": { "defaultMessage": "Accounts" }, + "FzbSGg": { + "defaultMessage": "You dont have any media servers, try adding some." + }, "G/yZLu": { "defaultMessage": "Remove" }, @@ -949,6 +952,9 @@ "UUPFlt": { "defaultMessage": "Users must accept the content warning to show the content of your note." }, + "UaCh1c": { + "defaultMessage": "Add Server" + }, "Ub+AGc": { "defaultMessage": "Sign In" }, @@ -1063,6 +1069,9 @@ "ZlmK/p": { "defaultMessage": "{name} invited you to {app}" }, + "a1x4gD": { + "defaultMessage": "Media servers store media which you can share in notes as images and videos" + }, "a5UPxh": { "defaultMessage": "Fund developers and platforms providing NIP-05 verification services" }, @@ -1142,6 +1151,9 @@ "defaultMessage": "URL..", "description": "Placeholder text for imgproxy url textbox" }, + "cVcgLJ": { + "defaultMessage": "Media Servers" + }, "cWx9t8": { "defaultMessage": "Mute all" }, @@ -1313,9 +1325,6 @@ "ha8JKG": { "defaultMessage": "Show graph" }, - "hf6g/W": { - "defaultMessage": "Custom server URL" - }, "hicxcO": { "defaultMessage": "Show replies" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index e87a9896..e78935c6 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -171,6 +171,7 @@ "FfYsOb": "An error has occured!", "FmXUJg": "follows you", "FvanT6": "Accounts", + "FzbSGg": "You dont have any media servers, try adding some.", "G/yZLu": "Remove", "G1BGCg": "Select Wallet", "G3A56c": "Subscribed to Push", @@ -314,6 +315,7 @@ "UNjfWJ": "Check all event signatures received from relays", "UT7Nkj": "New Chat", "UUPFlt": "Users must accept the content warning to show the content of your note.", + "UaCh1c": "Add Server", "Ub+AGc": "Sign In", "Up5U7K": "Block", "Ups2/p": "Your application is pending", @@ -352,6 +354,7 @@ "ZS+jRE": "Send zap splits to", "Zff6lu": "Username iris.to/{name} is reserved for you!", "ZlmK/p": "{name} invited you to {app}", + "a1x4gD": "Media servers store media which you can share in notes as images and videos", "a5UPxh": "Fund developers and platforms providing NIP-05 verification services", "a7TDNm": "Notes will stream in real time into global and notes tab", "aHje0o": "Name or nym", @@ -378,6 +381,7 @@ "cHCwbF": "Photography", "cPIKU2": "Following", "cQfLWb": "URL..", + "cVcgLJ": "Media Servers", "cWx9t8": "Mute all", "cg1VJ2": "Connect Wallet", "cuP16y": "Multi account support", @@ -435,7 +439,6 @@ "hY4lzx": "Supports", "hYOE+U": "Invite", "ha8JKG": "Show graph", - "hf6g/W": "Custom server URL", "hicxcO": "Show replies", "hmZ3Bz": "Media", "hniz8Z": "here", diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts index f169ba82..8bbbbaee 100644 --- a/packages/system/src/event-kind.ts +++ b/packages/system/src/event-kind.ts @@ -34,6 +34,7 @@ const enum EventKind { SearchRelaysList = 10_007, // NIP-51 InterestsList = 10_015, // NIP-51 EmojisList = 10_030, // NIP-51 + StorageServerList = 10_096, // NIP-96 server list FollowSet = 30_000, // NIP-51 RelaySet = 30_002, // NIP-51 diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index d8e3f8e0..5fa4d33f 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -76,7 +76,7 @@ export class QueryTrace extends EventEmitter { * Total time spent waiting for relay to respond */ get responseTime() { - return this.finished ? unwrap(this.eose) - unwrap(this.sent) : 0; + return this.finished ? unwrap(this.eose) - (this.sent ?? unixNowMs()) : 0; } /** diff --git a/packages/system/src/sync/diff-sync.ts b/packages/system/src/sync/diff-sync.ts index 9c25beb7..ca9b397a 100644 --- a/packages/system/src/sync/diff-sync.ts +++ b/packages/system/src/sync/diff-sync.ts @@ -160,7 +160,7 @@ export class DiffSyncTags extends EventEmitter { ? (change.tag as Array>) : [change.tag as Array]; for (const changeTag of changeTags) { - const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]); + const existing = tags.findIndex(a => changeTag[0] === a[0] && changeTag[1] === a[1]); if (existing === -1) { tags.push(changeTag); } else { @@ -174,7 +174,7 @@ export class DiffSyncTags extends EventEmitter { ? (change.tag as Array>) : [change.tag as Array]; for (const changeTag of changeTags) { - const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]); + const existing = tags.findIndex(a => changeTag[0] === a[0] && changeTag[1] === a[1]); if (existing !== -1) { tags.splice(existing, 1); } else { @@ -188,7 +188,7 @@ export class DiffSyncTags extends EventEmitter { ? (change.tag as Array>) : [change.tag as Array]; for (const changeTag of changeTags) { - const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]); + const existing = tags.findIndex(a => changeTag[0] === a[0] && changeTag[1] === a[1]); if (existing !== -1) { tags[existing] = changeTag; } else { diff --git a/packages/system/src/user-state.ts b/packages/system/src/user-state.ts index b51a7922..62b186bb 100644 --- a/packages/system/src/user-state.ts +++ b/packages/system/src/user-state.ts @@ -88,7 +88,7 @@ export class UserState extends EventEmitter { } // always track mute list - this.#checkIsStandardList(EventKind.MuteList); + this.checkIsStandardList(EventKind.MuteList); this.#profile.on("change", () => this.emit("change", UserStateChangeType.Profile)); this.#contacts.on("change", () => this.emit("change", UserStateChangeType.Contacts)); @@ -338,7 +338,7 @@ export class UserState extends EventEmitter { autoCommit = false, encrypted = false, ) { - this.#checkIsStandardList(kind); + this.checkIsStandardList(kind); this.#checkInit(); const list = this.#standardLists.get(kind); const tags = removeUndefined(Array.isArray(links) ? links.map(a => a.toEventTag()) : [links.toEventTag()]); @@ -363,7 +363,7 @@ export class UserState extends EventEmitter { autoCommit = false, encrypted = false, ) { - this.#checkIsStandardList(kind); + this.checkIsStandardList(kind); this.#checkInit(); const list = this.#standardLists.get(kind); const tags = removeUndefined(Array.isArray(links) ? links.map(a => a.toEventTag()) : [links.toEventTag()]); @@ -416,7 +416,7 @@ export class UserState extends EventEmitter { }; } - #checkIsStandardList(kind: EventKind) { + checkIsStandardList(kind: EventKind) { if (!(kind >= 10_000 && kind < 20_000)) { throw new Error("Not a standar list"); }