fix: default nip96

This commit is contained in:
2024-09-12 14:56:06 +01:00
parent 736189d0d2
commit ce10d920f4
18 changed files with 139 additions and 417 deletions

View File

@ -261,7 +261,7 @@ export function NoteCreator() {
async function uploadFile(file: File) { async function uploadFile(file: File) {
try { try {
if (file) { if (file && uploader) {
const rx = await uploader.upload(file, file.name); const rx = await uploader.upload(file, file.name);
note.update(v => { note.update(v => {
if (rx.header) { if (rx.header) {

View File

@ -10,10 +10,11 @@ import useFileUpload from "@/Utils/Upload";
interface AvatarEditorProps { interface AvatarEditorProps {
picture?: string; picture?: string;
onPictureChange?: (newPicture: string) => void; onPictureChange?: (newPicture: string) => void;
privKey?: string;
} }
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) { export default function AvatarEditor({ picture, onPictureChange, privKey }: AvatarEditorProps) {
const uploader = useFileUpload(); const uploader = useFileUpload(privKey);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -22,7 +23,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
setLoading(true); setLoading(true);
try { try {
const f = await openFile(); const f = await openFile();
if (f) { if (f && uploader) {
const rsp = await uploader.upload(f, f.name); const rsp = await uploader.upload(f, f.name);
console.log(rsp); console.log(rsp);
if (typeof rsp?.error === "string") { if (typeof rsp?.error === "string") {

View File

@ -0,0 +1,26 @@
import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
import { EventKind, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
export default function useDiscoverMediaServers() {
const sub = useMemo(() => {
const rb = new RequestBuilder("media-servers-all");
rb.withFilter().kinds([EventKind.StorageServerList]);
return rb;
}, []);
const data = useRequestBuilder(sub);
return data.reduce(
(acc, v) => {
const servers = removeUndefined(v.tags.filter(a => a[0] === "server").map(a => sanitizeRelayUrl(a[1])));
for (const server of servers) {
acc[server] ??= 0;
acc[server]++;
}
return acc;
},
{} as Record<string, number>,
);
}

View File

@ -1,28 +1,34 @@
import { NotEncrypted } from "@snort/system"; import { NotEncrypted } from "@snort/system";
import { SnortContext } from "@snort/system-react"; import { SnortContext } from "@snort/system-react";
import { useContext, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import AsyncButton from "@/Components/Button/AsyncButton"; import AsyncButton from "@/Components/Button/AsyncButton";
import AvatarEditor from "@/Components/User/AvatarEditor"; import AvatarEditor from "@/Components/User/AvatarEditor";
import { trackEvent } from "@/Utils"; import { trackEvent } from "@/Utils";
import { generateNewLogin } from "@/Utils/Login"; import { generateNewLogin, generateNewLoginKeys } from "@/Utils/Login";
import { NewUserState } from "."; import { NewUserState } from ".";
export function Profile() { export function Profile() {
const system = useContext(SnortContext); const system = useContext(SnortContext);
const [keys, setNewKeys] = useState<{ entropy: Uint8Array; privateKey: string }>();
const [picture, setPicture] = useState<string>(); const [picture, setPicture] = useState<string>();
const [error, setError] = useState(""); const [error, setError] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const state = location.state as NewUserState; const state = location.state as NewUserState;
async function makeRandomKey() { useEffect(() => {
generateNewLoginKeys().then(setNewKeys);
}, []);
async function loginNewKeys() {
try { try {
if (!keys) return;
setError(""); setError("");
await generateNewLogin(system, key => Promise.resolve(new NotEncrypted(key)), { await generateNewLogin(keys, system, key => Promise.resolve(new NotEncrypted(key)), {
name: state.name, name: state.name,
picture, picture,
}); });
@ -40,8 +46,8 @@ export function Profile() {
<h1> <h1>
<FormattedMessage defaultMessage="Profile Image" /> <FormattedMessage defaultMessage="Profile Image" />
</h1> </h1>
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} /> <AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} privKey={keys?.privateKey} />
<AsyncButton className="primary" onClick={() => makeRandomKey()}> <AsyncButton className="primary" onClick={() => loginNewKeys()}>
<FormattedMessage defaultMessage="Next" /> <FormattedMessage defaultMessage="Next" />
</AsyncButton> </AsyncButton>
{error && <b className="error">{error}</b>} {error && <b className="error">{error}</b>}

View File

@ -454,33 +454,6 @@ const PreferencesPage = () => {
/> />
</div> </div>
</div> </div>
<div className="flex flex-col g8">
<h4>
<FormattedMessage {...messages.FileUpload} />
</h4>
<small>
<FormattedMessage {...messages.FileUploadHelp} />
</small>
<select
value={pref.fileUploader}
onChange={e =>
setPref({
...pref,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="nip96">
<FormattedMessage defaultMessage="NIP-96" />
</option>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
</option>
<option value="void.cat-NIP96">void.cat (NIP-96)</option>
<option value="nostr.build">nostr.build</option>
<option value="nostrimg.com">nostrimg.com</option>
<option value="nostrcheck.me">nostrcheck.me (NIP-96)</option>
</select>
</div>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-col g8"> <div className="flex flex-col g8">
<h4> <h4>

View File

@ -131,7 +131,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
try { try {
setError(undefined); setError(undefined);
const file = await openFile(); const file = await openFile();
if (file) { if (file && uploader) {
const rsp = await uploader.upload(file, file.name); const rsp = await uploader.upload(file, file.name);
if (typeof rsp?.error === "string") { if (typeof rsp?.error === "string") {
throw new Error(`Upload failed ${rsp.error}`); throw new Error(`Upload failed ${rsp.error}`);

View File

@ -1,10 +1,13 @@
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { EventKind, UnknownTag } from "@snort/system"; import { EventKind, UnknownTag } from "@snort/system";
import { useState } from "react"; import { useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import AsyncButton from "@/Components/Button/AsyncButton"; import AsyncButton from "@/Components/Button/AsyncButton";
import IconButton from "@/Components/Button/IconButton"; 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 useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import { Nip96Uploader } from "@/Utils/Upload/Nip96"; import { Nip96Uploader } from "@/Utils/Upload/Nip96";
@ -15,13 +18,14 @@ export default function MediaSettingsPage() {
const list = state.getList(EventKind.StorageServerList); const list = state.getList(EventKind.StorageServerList);
const [newServer, setNewServer] = useState(""); const [newServer, setNewServer] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const knownServers = useDiscoverMediaServers();
async function validateServer() { async function validateServer(url: string) {
if (!publisher) return; if (!publisher) return;
setError(""); setError("");
try { try {
const svc = new Nip96Uploader(newServer, publisher); const svc = new Nip96Uploader(url, publisher);
await svc.loadInfo(); await svc.loadInfo();
return true; return true;
@ -79,7 +83,7 @@ export default function MediaSettingsPage() {
/> />
<AsyncButton <AsyncButton
onClick={async () => { onClick={async () => {
if (await validateServer()) { if (await validateServer(newServer)) {
await state.addToList( await state.addToList(
EventKind.StorageServerList, EventKind.StorageServerList,
[new UnknownTag(["server", new URL(newServer).toString()])], [new UnknownTag(["server", new URL(newServer).toString()])],
@ -93,6 +97,57 @@ export default function MediaSettingsPage() {
</div> </div>
{error && <b className="text-warning">{error}</b>} {error && <b className="text-warning">{error}</b>}
</div> </div>
<CollapsedSection
title={
<div className="text-xl font-medium">
<FormattedMessage defaultMessage="Popular Servers" />
</div>
}>
<small>
<FormattedMessage defaultMessage="Popular media servers." />
</small>
<table className="table">
<thead>
<tr className="uppercase text-secondary">
<th>
<FormattedMessage defaultMessage="Server" />
</th>
<th>
<FormattedMessage defaultMessage="Users" />
</th>
<th></th>
</tr>
</thead>
<tbody>
{Object.entries(knownServers)
.sort((a, b) => (b[1] < a[1] ? -1 : 1))
.filter(([k]) => !list.some(b => b.equals(new UnknownTag(["server", k]))))
.slice(0, 20)
.map(([k, v]) => (
<tr key={k}>
<td className="flex gap-2 items-center">
<RelayFavicon url={k} />
{k}
</td>
<td className="text-center">
<FormattedNumber value={v} />
</td>
<td className="text-end">
<AsyncButton
className="!py-1 mb-1"
onClick={async () => {
if (await validateServer(k)) {
await state.addToList(EventKind.StorageServerList, [new UnknownTag(["server", k])], true);
}
}}>
<FormattedMessage defaultMessage="Add" />
</AsyncButton>
</td>
</tr>
))}
</tbody>
</table>
</CollapsedSection>
</div> </div>
); );
} }

View File

@ -14,8 +14,7 @@ import {
} from "@snort/system"; } from "@snort/system";
import { GiftsCache } from "@/Cache"; import { GiftsCache } from "@/Cache";
import SnortApi from "@/External/SnortApi"; import { bech32ToHex, dedupeById, deleteRefCode, unwrap } from "@/Utils";
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
import { Blasters } from "@/Utils/Const"; import { Blasters } from "@/Utils/Const";
import { LoginSession, LoginSessionType, LoginStore, SnortAppData } from "@/Utils/Login/index"; import { LoginSession, LoginSessionType, LoginStore, SnortAppData } from "@/Utils/Login/index";
import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6"; import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6";
@ -41,31 +40,26 @@ export function clearEntropy(state: LoginSession) {
} }
/** /**
* Generate a new key and login with this generated key * Generate a new key
*/
export async function generateNewLoginKeys() {
const entropy = generateBip39Entropy();
const privateKey = await entropyToPrivateKey(entropy);
return { entropy, privateKey };
}
/**
* Login with newly generated key
*/ */
export async function generateNewLogin( export async function generateNewLogin(
keys: { entropy: Uint8Array; privateKey: string },
system: SystemInterface, system: SystemInterface,
pin: (key: string) => Promise<KeyStorage>, pin: (key: string) => Promise<KeyStorage>,
profile: UserMetadata, profile: UserMetadata,
) { ) {
const entropy = generateBip39Entropy(); const { entropy, privateKey } = keys;
const privateKey = await entropyToPrivateKey(entropy);
const newRelays = {} as Record<string, RelaySettings>; const newRelays = {} as Record<string, RelaySettings>;
// Use current timezone info to determine approx location
// use closest 5 relays
const country = getCountry();
const api = new SnortApi();
const closeRelays = await api.closeRelays(country.lat, country.lon, 20);
for (const cr of closeRelays.sort((a, b) => (a.distance > b.distance ? 1 : -1)).filter(a => !a.is_paid)) {
const rr = sanitizeRelayUrl(cr.url);
if (rr) {
newRelays[rr] = { read: true, write: true };
if (Object.keys(newRelays).length >= 5) {
break;
}
}
}
for (const [k, v] of Object.entries(CONFIG.defaultRelays)) { for (const [k, v] of Object.entries(CONFIG.defaultRelays)) {
if (!newRelays[k]) { if (!newRelays[k]) {
newRelays[k] = v; newRelays[k] = v;
@ -75,8 +69,8 @@ export async function generateNewLogin(
// connect to new relays // connect to new relays
await Promise.all(Object.entries(newRelays).map(([k, v]) => system.ConnectToRelay(k, v))); await Promise.all(Object.entries(newRelays).map(([k, v]) => system.ConnectToRelay(k, v)));
const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
const publisher = EventPublisher.privateKey(privateKey); const publisher = EventPublisher.privateKey(privateKey);
const publicKey = publisher.pubKey;
// Create new contact list following self and site account // Create new contact list following self and site account
const contactList = [publicKey, ...CONFIG.signUp.defaultFollows.map(a => bech32ToHex(a))].map(a => ["p", a]) as Array< const contactList = [publicKey, ...CONFIG.signUp.defaultFollows.map(a => bech32ToHex(a))].map(a => ["p", a]) as Array<

View File

@ -43,11 +43,6 @@ export interface UserPreferences {
*/ */
showDebugMenus: boolean; showDebugMenus: boolean;
/**
* File uploading service to upload attachments to
*/
fileUploader: "void.cat" | "nostr.build" | "nostrimg.com" | "void.cat-NIP96" | "nostrcheck.me" | "nip96";
/** /**
* Use imgproxy to optimize images * Use imgproxy to optimize images
*/ */
@ -117,7 +112,6 @@ export const DefaultPreferences = {
confirmReposts: false, confirmReposts: false,
showDebugMenus: true, showDebugMenus: true,
autoShowLatest: false, autoShowLatest: false,
fileUploader: "nostr.build",
imgProxyConfig: DefaultImgProxy, imgProxyConfig: DefaultImgProxy,
defaultRootTab: "following", defaultRootTab: "following",
defaultZapAmount: 50, defaultZapAmount: 50,

View File

@ -45,16 +45,6 @@ export class Nip96Uploader {
const data = (await rsp.json()) as Nip96Result; const data = (await rsp.json()) as Nip96Result;
if (data.status === "success") { if (data.status === "success") {
const meta = readNip94Tags(data.nip94_event.tags); const meta = readNip94Tags(data.nip94_event.tags);
if (
meta.dimensions === undefined ||
meta.dimensions.length !== 2 ||
meta.dimensions[0] === 0 ||
meta.dimensions[1] === 0
) {
return {
error: `Invalid dimensions: "${meta.dimensions?.join("x")}"`,
};
}
return { return {
url: addExtensionToNip94Url(meta), url: addExtensionToNip94Url(meta),
header: data.nip94_event, header: data.nip94_event,

View File

@ -1,71 +0,0 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
import { UploadResult } from "@/Utils/Upload/index";
export default async function NostrBuild(file: File | Blob, publisher?: EventPublisher): Promise<UploadResult> {
const auth = publisher
? async (url: string, method: string) => {
const auth = await publisher.generic(eb => {
return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
}
: undefined;
const fd = new FormData();
fd.append("fileToUpload", file);
fd.append("submit", "Upload Image");
const url = "https://nostr.build/api/v2/upload/files";
const headers = {
accept: "application/json",
} as Record<string, string>;
if (auth) {
headers["Authorization"] = await auth(url, "POST");
}
const rsp = await fetch(url, {
body: fd,
method: "POST",
headers,
});
if (rsp.ok) {
throwIfOffline();
const data = (await rsp.json()) as NostrBuildUploadResponse;
const res = data.data[0];
return {
url: res.url,
metadata: {
blurhash: res.blurhash,
width: res.dimensions.width,
height: res.dimensions.height,
hash: res.sha256,
},
};
}
return {
error: "Upload failed",
};
}
interface NostrBuildUploadResponse {
data: Array<NostrBuildUploadData>;
}
interface NostrBuildUploadData {
input_name: string;
name: string;
url: string;
thumbnail: string;
blurhash: string;
sha256: string;
type: string;
mime: string;
size: number;
metadata: Record<string, string>;
dimensions: {
width: number;
height: number;
};
}

View File

@ -1,44 +0,0 @@
import { throwIfOffline } from "@snort/shared";
import { UploadResult } from "@/Utils/Upload/index";
export default async function NostrImg(file: File | Blob): Promise<UploadResult> {
throwIfOffline();
const fd = new FormData();
fd.append("image", file);
const rsp = await fetch("https://nostrimg.com/api/upload", {
body: fd,
method: "POST",
headers: {
accept: "application/json",
},
});
if (rsp.ok) {
const data: UploadResponse = await rsp.json();
if (typeof data?.imageUrl === "string" && data.success) {
return {
url: new URL(data.imageUrl).toString(),
};
}
}
return {
error: "Upload failed",
};
}
interface UploadResponse {
fileID?: string;
fileName?: string;
imageUrl?: string;
lightningDestination?: string;
lightningPaymentLink?: string;
message?: string;
route?: string;
status: number;
success: boolean;
url?: string;
data?: {
url?: string;
};
}

View File

@ -1,102 +0,0 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
import { UploadState, VoidApi } from "@void-cat/api";
import { FileExtensionRegex } from "@/Utils/Const";
import { UploadResult } from "@/Utils/Upload/index";
/**
* Upload file to void.cat
* https://void.cat/swagger/index.html
*/
export default async function VoidCatUpload(
file: File | Blob,
filename: string,
publisher?: EventPublisher,
progress?: (n: number) => void,
stage?: (n: "starting" | "hashing" | "uploading" | "done" | undefined) => void,
): Promise<UploadResult> {
throwIfOffline();
const auth = publisher
? async (url: string, method: string) => {
const auth = await publisher.generic(eb => {
return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
}
: undefined;
const api = new VoidApi("https://void.cat", auth);
const uploader = api.getUploader(
file,
sx => {
stage?.(
(() => {
switch (sx) {
case UploadState.Starting:
return "starting";
case UploadState.Hashing:
return "hashing";
case UploadState.Uploading:
return "uploading";
case UploadState.Done:
return "done";
}
})(),
);
},
px => {
progress?.(px / file.size);
},
);
const rsp = await uploader.upload({
"V-Strip-Metadata": "true",
});
if (rsp.ok) {
let ext = filename.match(FileExtensionRegex);
if (rsp.file?.metadata?.mimeType === "image/webp") {
ext = ["", "webp"];
}
const resultUrl = rsp.file?.metadata?.url ?? `https://void.cat/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
const dim = rsp.file?.metadata?.mediaDimensions ? rsp.file.metadata.mediaDimensions.split("x") : undefined;
const ret = {
url: resultUrl,
metadata: {
hash: rsp.file?.metadata?.digest,
width: dim ? Number(dim[0]) : undefined,
height: dim ? Number(dim[1]) : undefined,
},
} as UploadResult;
if (publisher) {
// NIP-94
/*const tags = [
["url", resultUrl],
["x", rsp.file?.metadata?.digest ?? ""],
["m", rsp.file?.metadata?.mimeType ?? "application/octet-stream"],
];
if (rsp.file?.metadata?.size) {
tags.push(["size", rsp.file.metadata.size.toString()]);
}
if (rsp.file?.metadata?.magnetLink) {
tags.push(["magnet", rsp.file.metadata.magnetLink]);
const parsedMagnet = magnetURIDecode(rsp.file.metadata.magnetLink);
if (parsedMagnet?.infoHash) {
tags.push(["i", parsedMagnet?.infoHash]);
}
}
ret.header = await publisher.generic(eb => {
eb.kind(EventKind.FileHeader).content(filename);
tags.forEach(t => eb.tag(t));
return eb;
});*/
}
return ret;
} else {
return {
error: rsp.errorMessage,
};
}
}

View File

@ -1 +0,0 @@
export class BlossomClient {}

View File

@ -1,16 +1,9 @@
import { removeUndefined } from "@snort/shared"; import { EventPublisher, NostrEvent } from "@snort/system";
import { EventKind, NostrEvent } from "@snort/system";
import { useState } from "react";
import { v4 as uuid } from "uuid";
import useEventPublisher from "@/Hooks/useEventPublisher"; import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin"; import { useMediaServerList } from "@/Hooks/useMediaServerList";
import usePreferences from "@/Hooks/usePreferences";
import { bech32ToHex, randomSample, unwrap } from "@/Utils"; import { bech32ToHex, randomSample, unwrap } from "@/Utils";
import { FileExtensionRegex, KieranPubKey } from "@/Utils/Const"; import { FileExtensionRegex, KieranPubKey } from "@/Utils/Const";
import NostrBuild from "@/Utils/Upload/NostrBuild";
import NostrImg from "@/Utils/Upload/NostrImg";
import VoidCat from "@/Utils/Upload/VoidCat";
import { Nip96Uploader } from "./Nip96"; import { Nip96Uploader } from "./Nip96";
@ -81,118 +74,19 @@ export interface UploadProgress {
export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined; export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined;
export default function useFileUpload(): Uploader { export default function useFileUpload(privKey?: string) {
const fileUploader = usePreferences(s => s.fileUploader);
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
const { publisher } = useEventPublisher(); const { publisher } = useEventPublisher();
const [progress, setProgress] = useState<Array<UploadProgress>>([]); const { servers } = useMediaServerList();
const [stage, setStage] = useState<UploadStage>();
const defaultUploader = { const pub = privKey ? EventPublisher.privateKey(privKey) : publisher;
upload: async (f, n) => { if (servers.length > 0 && pub) {
const id = uuid(); const random = randomSample(servers, 1)[0];
setProgress(s => [ return new Nip96Uploader(random, pub);
...s, } else if (pub) {
{ return new Nip96Uploader("https://nostr.build", pub);
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 {
upload: f => NostrBuild(f, publisher),
progress: [],
} as Uploader;
}
case "void.cat-NIP96": {
return new Nip96Uploader("https://void.cat/nostr", unwrap(publisher));
}
case "nostrcheck.me": {
return new Nip96Uploader("https://nostrcheck.me/api/v2/nip96", unwrap(publisher));
}
case "nostrimg.com": {
return {
upload: NostrImg,
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 defaultUploader;
}
} }
} }
export const ProgressStream = (file: File | Blob, progress: (n: number) => void) => {
let offset = 0;
const DefaultChunkSize = 1024 * 32;
const readChunk = async (offset: number, size: number) => {
if (offset > file.size) {
return new Uint8Array(0);
}
const end = Math.min(offset + size, file.size);
const blob = file.slice(offset, end, file.type);
const data = await blob.arrayBuffer();
return new Uint8Array(data);
};
const rsBase = new ReadableStream(
{
start: async () => {},
pull: async controller => {
const chunk = await readChunk(offset, controller.desiredSize ?? DefaultChunkSize);
if (chunk.byteLength === 0) {
controller.close();
return;
}
progress((offset + chunk.byteLength) / file.size);
offset += chunk.byteLength;
controller.enqueue(chunk);
},
cancel: reason => {
console.log(reason);
},
type: "bytes",
},
{
highWaterMark: DefaultChunkSize,
},
);
return rsBase;
};
export function addExtensionToNip94Url(meta: Nip94Tags) { export function addExtensionToNip94Url(meta: Nip94Tags) {
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) { if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
switch (meta.mimeType) { switch (meta.mimeType) {

View File

@ -74,6 +74,9 @@
"08zn6O": { "08zn6O": {
"defaultMessage": "Export Keys" "defaultMessage": "Export Keys"
}, },
"0AmhUh": {
"defaultMessage": "Popular media servers used by people you follow."
},
"0Azlrb": { "0Azlrb": {
"defaultMessage": "Manage" "defaultMessage": "Manage"
}, },
@ -476,6 +479,9 @@
"C81/uG": { "C81/uG": {
"defaultMessage": "Logout" "defaultMessage": "Logout"
}, },
"C8FsOr": {
"defaultMessage": "Popular Servers"
},
"C8HhVE": { "C8HhVE": {
"defaultMessage": "Suggested Follows" "defaultMessage": "Suggested Follows"
}, },
@ -578,6 +584,9 @@
"EnCOBJ": { "EnCOBJ": {
"defaultMessage": "Buy" "defaultMessage": "Buy"
}, },
"F/6VqP": {
"defaultMessage": "Server"
},
"F3l7xL": { "F3l7xL": {
"defaultMessage": "Add Account" "defaultMessage": "Add Account"
}, },
@ -759,9 +768,6 @@
"JSx7y9": { "JSx7y9": {
"defaultMessage": "Subscribe to {site_name} {plan} for {price} and receive the following rewards" "defaultMessage": "Subscribe to {site_name} {plan} for {price} and receive the following rewards"
}, },
"JTht/T": {
"defaultMessage": "NIP-96"
},
"JeoS4y": { "JeoS4y": {
"defaultMessage": "Repost" "defaultMessage": "Repost"
}, },

View File

@ -24,6 +24,7 @@
"00LcfG": "Load more", "00LcfG": "Load more",
"01iNut": "Nostr address does not belong to you", "01iNut": "Nostr address does not belong to you",
"08zn6O": "Export Keys", "08zn6O": "Export Keys",
"0AmhUh": "Popular media servers used by people you follow.",
"0Azlrb": "Manage", "0Azlrb": "Manage",
"0BUTMv": "Search...", "0BUTMv": "Search...",
"0MndVW": "Generic LNDHub wallet (BTCPayServer / Alby / LNBits)", "0MndVW": "Generic LNDHub wallet (BTCPayServer / Alby / LNBits)",
@ -157,6 +158,7 @@
"C1LjMx": "Lightning Donation", "C1LjMx": "Lightning Donation",
"C7642/": "Quote Repost", "C7642/": "Quote Repost",
"C81/uG": "Logout", "C81/uG": "Logout",
"C8FsOr": "Popular Servers",
"C8HhVE": "Suggested Follows", "C8HhVE": "Suggested Follows",
"CHTbO3": "Failed to load invoice", "CHTbO3": "Failed to load invoice",
"CM+Cfj": "Follow List", "CM+Cfj": "Follow List",
@ -191,6 +193,7 @@
"EcglP9": "Key", "EcglP9": "Key",
"EjFyoR": "On-chain Donation Address", "EjFyoR": "On-chain Donation Address",
"EnCOBJ": "Buy", "EnCOBJ": "Buy",
"F/6VqP": "Server",
"F3l7xL": "Add Account", "F3l7xL": "Add Account",
"F4eJ/3": "Classified Listings", "F4eJ/3": "Classified Listings",
"FDguSC": "{n} Zaps", "FDguSC": "{n} Zaps",
@ -251,7 +254,6 @@
"JIVWWA": "Sport", "JIVWWA": "Sport",
"JPFYIM": "No lightning address", "JPFYIM": "No lightning address",
"JSx7y9": "Subscribe to {site_name} {plan} for {price} and receive the following rewards", "JSx7y9": "Subscribe to {site_name} {plan} for {price} and receive the following rewards",
"JTht/T": "NIP-96",
"JeoS4y": "Repost", "JeoS4y": "Repost",
"JjGgXI": "Search users", "JjGgXI": "Search users",
"JkLHGw": "Website", "JkLHGw": "Website",

View File

@ -24,7 +24,6 @@ import {
import { EventBuilder } from "./event-builder"; import { EventBuilder } from "./event-builder";
import { findTag } from "./utils"; import { findTag } from "./utils";
import { Nip7Signer } from "./impl/nip7"; import { Nip7Signer } from "./impl/nip7";
import { base64 } from "@scure/base";
import { Nip10 } from "./impl/nip10"; import { Nip10 } from "./impl/nip10";
type EventBuilderHook = (ev: EventBuilder) => EventBuilder; type EventBuilderHook = (ev: EventBuilder) => EventBuilder;