feat: NIP-96 server list
This commit is contained in:
parent
7d0d3030f4
commit
5763d91e8a
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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">
|
||||
|
@ -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 />,
|
||||
|
98
packages/app/src/Pages/settings/media-settings.tsx
Normal file
98
packages/app/src/Pages/settings/media-settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user