feat: NIP-96 server list
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
kieran 2024-05-14 13:16:03 +01:00
parent 7d0d3030f4
commit 5763d91e8a
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
14 changed files with 192 additions and 72 deletions

View File

@ -125,6 +125,12 @@ const SettingsIndex = () => {
message: <FormattedMessage defaultMessage="Cache" />,
path: "cache",
},
{
icon: "camera-plus",
iconBg: "bg-lime-500",
message: <FormattedMessage defaultMessage="Media" />,
path: "media",
},
],
},
{

View File

@ -480,19 +480,6 @@ const PreferencesPage = () => {
<option value="nostrimg.com">nostrimg.com</option>
<option value="nostrcheck.me">nostrcheck.me (NIP-96)</option>
</select>
{pref.fileUploader === "nip96" && (
<>
<small>
<FormattedMessage defaultMessage="Custom server URL" />
</small>
<input
type="text"
value={pref.nip96Server}
onChange={e => setPref({ ...pref, nip96Server: e.target.value })}
placeholder="https://my-nip96-server.com/"
/>
</>
)}
</div>
<div className="flex justify-between">
<div className="flex flex-col g8">

View File

@ -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: <CacheSettings />,
},
{
path: "media",
element: <MediaSettingsPage />,
},
{
path: "invite",
element: <ReferralsPage />,

View File

@ -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 (
<div className="flex flex-col gap-3">
<div className="text-xl">
<FormattedMessage defaultMessage="Media Servers" />
</div>
<p>
<FormattedMessage defaultMessage="Media servers store media which you can share in notes as images and videos" />
</p>
<div className="flex flex-col gap-3">
{list.map(a => {
const [, addr] = unwrap(a.toEventTag());
return (
<div key={addr} className="p br bg-ultradark flex justify-between items-center">
{addr}
<IconButton
icon={{
name: "trash",
size: 15,
}}
onClick={async () => {
await state.removeFromList(EventKind.StorageServerList, [new UnknownTag(["server", addr])], true);
}}
/>
</div>
);
})}
{list.length === 0 && (
<small>
<FormattedMessage defaultMessage="You dont have any media servers, try adding some." />
</small>
)}
</div>
<div className="p br bg-ultradark flex flex-col gap-2">
<div className="text-lg">
<FormattedMessage defaultMessage="Add Server" />
</div>
<div className="flex gap-2">
<input
type="text"
className="flex-grow"
placeholder="https://my-files.com/"
value={newServer}
onChange={e => setNewServer(e.target.value)}
/>
<AsyncButton
onClick={async () => {
if (await validateServer()) {
await state.addToList(
EventKind.StorageServerList,
[new UnknownTag(["server", new URL(newServer).toString()])],
true,
);
setNewServer("");
}
}}>
<FormattedMessage defaultMessage="Add" />
</AsyncButton>
</div>
{error && <b className="text-warning">{error}</b>}
</div>
</div>
);
}

View File

@ -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<LoginSession> {
},
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<LoginSession> {
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<LoginSession> {
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) {

View File

@ -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
*/

View File

@ -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;

View File

@ -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<Array<UploadProgress>>([]);
const [stage, setStage] = useState<UploadStage>();
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;
}
}
}

View File

@ -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"
},

View File

@ -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/<b>{name}</b> 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",

View File

@ -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

View File

@ -76,7 +76,7 @@ export class QueryTrace extends EventEmitter<QueryTraceEvents> {
* 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;
}
/**

View File

@ -160,7 +160,7 @@ export class DiffSyncTags extends EventEmitter<SafeSyncEvents> {
? (change.tag as Array<Array<string>>)
: [change.tag as Array<string>];
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<SafeSyncEvents> {
? (change.tag as Array<Array<string>>)
: [change.tag as Array<string>];
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<SafeSyncEvents> {
? (change.tag as Array<Array<string>>)
: [change.tag as Array<string>];
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 {

View File

@ -88,7 +88,7 @@ export class UserState<TAppData> extends EventEmitter<UserStateEvents> {
}
// 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<TAppData> extends EventEmitter<UserStateEvents> {
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<TAppData> extends EventEmitter<UserStateEvents> {
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<TAppData> extends EventEmitter<UserStateEvents> {
};
}
#checkIsStandardList(kind: EventKind) {
checkIsStandardList(kind: EventKind) {
if (!(kind >= 10_000 && kind < 20_000)) {
throw new Error("Not a standar list");
}