fix: default nip96
This commit is contained in:
parent
736189d0d2
commit
ce10d920f4
@ -261,7 +261,7 @@ export function NoteCreator() {
|
||||
|
||||
async function uploadFile(file: File) {
|
||||
try {
|
||||
if (file) {
|
||||
if (file && uploader) {
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
note.update(v => {
|
||||
if (rx.header) {
|
||||
|
@ -10,10 +10,11 @@ import useFileUpload from "@/Utils/Upload";
|
||||
interface AvatarEditorProps {
|
||||
picture?: string;
|
||||
onPictureChange?: (newPicture: string) => void;
|
||||
privKey?: string;
|
||||
}
|
||||
|
||||
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
|
||||
const uploader = useFileUpload();
|
||||
export default function AvatarEditor({ picture, onPictureChange, privKey }: AvatarEditorProps) {
|
||||
const uploader = useFileUpload(privKey);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@ -22,7 +23,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
|
||||
setLoading(true);
|
||||
try {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
if (f && uploader) {
|
||||
const rsp = await uploader.upload(f, f.name);
|
||||
console.log(rsp);
|
||||
if (typeof rsp?.error === "string") {
|
||||
|
26
packages/app/src/Hooks/useDiscoverMediaServers.ts
Normal file
26
packages/app/src/Hooks/useDiscoverMediaServers.ts
Normal 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>,
|
||||
);
|
||||
}
|
@ -1,28 +1,34 @@
|
||||
import { NotEncrypted } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import AvatarEditor from "@/Components/User/AvatarEditor";
|
||||
import { trackEvent } from "@/Utils";
|
||||
import { generateNewLogin } from "@/Utils/Login";
|
||||
import { generateNewLogin, generateNewLoginKeys } from "@/Utils/Login";
|
||||
|
||||
import { NewUserState } from ".";
|
||||
|
||||
export function Profile() {
|
||||
const system = useContext(SnortContext);
|
||||
const [keys, setNewKeys] = useState<{ entropy: Uint8Array; privateKey: string }>();
|
||||
const [picture, setPicture] = useState<string>();
|
||||
const [error, setError] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const state = location.state as NewUserState;
|
||||
|
||||
async function makeRandomKey() {
|
||||
useEffect(() => {
|
||||
generateNewLoginKeys().then(setNewKeys);
|
||||
}, []);
|
||||
|
||||
async function loginNewKeys() {
|
||||
try {
|
||||
if (!keys) return;
|
||||
setError("");
|
||||
await generateNewLogin(system, key => Promise.resolve(new NotEncrypted(key)), {
|
||||
await generateNewLogin(keys, system, key => Promise.resolve(new NotEncrypted(key)), {
|
||||
name: state.name,
|
||||
picture,
|
||||
});
|
||||
@ -40,8 +46,8 @@ export function Profile() {
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Profile Image" />
|
||||
</h1>
|
||||
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
|
||||
<AsyncButton className="primary" onClick={() => makeRandomKey()}>
|
||||
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} privKey={keys?.privateKey} />
|
||||
<AsyncButton className="primary" onClick={() => loginNewKeys()}>
|
||||
<FormattedMessage defaultMessage="Next" />
|
||||
</AsyncButton>
|
||||
{error && <b className="error">{error}</b>}
|
||||
|
@ -454,33 +454,6 @@ const PreferencesPage = () => {
|
||||
/>
|
||||
</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 flex-col g8">
|
||||
<h4>
|
||||
|
@ -131,7 +131,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
try {
|
||||
setError(undefined);
|
||||
const file = await openFile();
|
||||
if (file) {
|
||||
if (file && uploader) {
|
||||
const rsp = await uploader.upload(file, file.name);
|
||||
if (typeof rsp?.error === "string") {
|
||||
throw new Error(`Upload failed ${rsp.error}`);
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventKind, UnknownTag } from "@snort/system";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
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";
|
||||
@ -15,13 +18,14 @@ export default function MediaSettingsPage() {
|
||||
const list = state.getList(EventKind.StorageServerList);
|
||||
const [newServer, setNewServer] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const knownServers = useDiscoverMediaServers();
|
||||
|
||||
async function validateServer() {
|
||||
async function validateServer(url: string) {
|
||||
if (!publisher) return;
|
||||
|
||||
setError("");
|
||||
try {
|
||||
const svc = new Nip96Uploader(newServer, publisher);
|
||||
const svc = new Nip96Uploader(url, publisher);
|
||||
await svc.loadInfo();
|
||||
|
||||
return true;
|
||||
@ -79,7 +83,7 @@ export default function MediaSettingsPage() {
|
||||
/>
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
if (await validateServer()) {
|
||||
if (await validateServer(newServer)) {
|
||||
await state.addToList(
|
||||
EventKind.StorageServerList,
|
||||
[new UnknownTag(["server", new URL(newServer).toString()])],
|
||||
@ -93,6 +97,57 @@ export default function MediaSettingsPage() {
|
||||
</div>
|
||||
{error && <b className="text-warning">{error}</b>}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ import {
|
||||
} from "@snort/system";
|
||||
|
||||
import { GiftsCache } from "@/Cache";
|
||||
import SnortApi from "@/External/SnortApi";
|
||||
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
||||
import { bech32ToHex, dedupeById, deleteRefCode, unwrap } from "@/Utils";
|
||||
import { Blasters } from "@/Utils/Const";
|
||||
import { LoginSession, LoginSessionType, LoginStore, SnortAppData } from "@/Utils/Login/index";
|
||||
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(
|
||||
keys: { entropy: Uint8Array; privateKey: string },
|
||||
system: SystemInterface,
|
||||
pin: (key: string) => Promise<KeyStorage>,
|
||||
profile: UserMetadata,
|
||||
) {
|
||||
const entropy = generateBip39Entropy();
|
||||
const privateKey = await entropyToPrivateKey(entropy);
|
||||
const { entropy, privateKey } = keys;
|
||||
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)) {
|
||||
if (!newRelays[k]) {
|
||||
newRelays[k] = v;
|
||||
@ -75,8 +69,8 @@ export async function generateNewLogin(
|
||||
// connect to new relays
|
||||
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 publicKey = publisher.pubKey;
|
||||
|
||||
// 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<
|
||||
|
@ -43,11 +43,6 @@ export interface UserPreferences {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -117,7 +112,6 @@ export const DefaultPreferences = {
|
||||
confirmReposts: false,
|
||||
showDebugMenus: true,
|
||||
autoShowLatest: false,
|
||||
fileUploader: "nostr.build",
|
||||
imgProxyConfig: DefaultImgProxy,
|
||||
defaultRootTab: "following",
|
||||
defaultZapAmount: 50,
|
||||
|
@ -45,16 +45,6 @@ export class Nip96Uploader {
|
||||
const data = (await rsp.json()) as Nip96Result;
|
||||
if (data.status === "success") {
|
||||
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 {
|
||||
url: addExtensionToNip94Url(meta),
|
||||
header: data.nip94_event,
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export class BlossomClient {}
|
@ -1,16 +1,9 @@
|
||||
import { removeUndefined } from "@snort/shared";
|
||||
import { EventKind, NostrEvent } from "@snort/system";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { EventPublisher, NostrEvent } from "@snort/system";
|
||||
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePreferences from "@/Hooks/usePreferences";
|
||||
import { useMediaServerList } from "@/Hooks/useMediaServerList";
|
||||
import { bech32ToHex, randomSample, unwrap } from "@/Utils";
|
||||
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";
|
||||
|
||||
@ -81,118 +74,19 @@ export interface UploadProgress {
|
||||
|
||||
export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined;
|
||||
|
||||
export default function useFileUpload(): Uploader {
|
||||
const fileUploader = usePreferences(s => s.fileUploader);
|
||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
||||
export default function useFileUpload(privKey?: string) {
|
||||
const { publisher } = useEventPublisher();
|
||||
const [progress, setProgress] = useState<Array<UploadProgress>>([]);
|
||||
const [stage, setStage] = useState<UploadStage>();
|
||||
const { servers } = useMediaServerList();
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
const pub = privKey ? EventPublisher.privateKey(privKey) : publisher;
|
||||
if (servers.length > 0 && pub) {
|
||||
const random = randomSample(servers, 1)[0];
|
||||
return new Nip96Uploader(random, pub);
|
||||
} else if (pub) {
|
||||
return new Nip96Uploader("https://nostr.build", pub);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
|
||||
switch (meta.mimeType) {
|
||||
|
@ -74,6 +74,9 @@
|
||||
"08zn6O": {
|
||||
"defaultMessage": "Export Keys"
|
||||
},
|
||||
"0AmhUh": {
|
||||
"defaultMessage": "Popular media servers used by people you follow."
|
||||
},
|
||||
"0Azlrb": {
|
||||
"defaultMessage": "Manage"
|
||||
},
|
||||
@ -476,6 +479,9 @@
|
||||
"C81/uG": {
|
||||
"defaultMessage": "Logout"
|
||||
},
|
||||
"C8FsOr": {
|
||||
"defaultMessage": "Popular Servers"
|
||||
},
|
||||
"C8HhVE": {
|
||||
"defaultMessage": "Suggested Follows"
|
||||
},
|
||||
@ -578,6 +584,9 @@
|
||||
"EnCOBJ": {
|
||||
"defaultMessage": "Buy"
|
||||
},
|
||||
"F/6VqP": {
|
||||
"defaultMessage": "Server"
|
||||
},
|
||||
"F3l7xL": {
|
||||
"defaultMessage": "Add Account"
|
||||
},
|
||||
@ -759,9 +768,6 @@
|
||||
"JSx7y9": {
|
||||
"defaultMessage": "Subscribe to {site_name} {plan} for {price} and receive the following rewards"
|
||||
},
|
||||
"JTht/T": {
|
||||
"defaultMessage": "NIP-96"
|
||||
},
|
||||
"JeoS4y": {
|
||||
"defaultMessage": "Repost"
|
||||
},
|
||||
|
@ -24,6 +24,7 @@
|
||||
"00LcfG": "Load more",
|
||||
"01iNut": "Nostr address does not belong to you",
|
||||
"08zn6O": "Export Keys",
|
||||
"0AmhUh": "Popular media servers used by people you follow.",
|
||||
"0Azlrb": "Manage",
|
||||
"0BUTMv": "Search...",
|
||||
"0MndVW": "Generic LNDHub wallet (BTCPayServer / Alby / LNBits)",
|
||||
@ -157,6 +158,7 @@
|
||||
"C1LjMx": "Lightning Donation",
|
||||
"C7642/": "Quote Repost",
|
||||
"C81/uG": "Logout",
|
||||
"C8FsOr": "Popular Servers",
|
||||
"C8HhVE": "Suggested Follows",
|
||||
"CHTbO3": "Failed to load invoice",
|
||||
"CM+Cfj": "Follow List",
|
||||
@ -191,6 +193,7 @@
|
||||
"EcglP9": "Key",
|
||||
"EjFyoR": "On-chain Donation Address",
|
||||
"EnCOBJ": "Buy",
|
||||
"F/6VqP": "Server",
|
||||
"F3l7xL": "Add Account",
|
||||
"F4eJ/3": "Classified Listings",
|
||||
"FDguSC": "{n} Zaps",
|
||||
@ -251,7 +254,6 @@
|
||||
"JIVWWA": "Sport",
|
||||
"JPFYIM": "No lightning address",
|
||||
"JSx7y9": "Subscribe to {site_name} {plan} for {price} and receive the following rewards",
|
||||
"JTht/T": "NIP-96",
|
||||
"JeoS4y": "Repost",
|
||||
"JjGgXI": "Search users",
|
||||
"JkLHGw": "Website",
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
import { EventBuilder } from "./event-builder";
|
||||
import { findTag } from "./utils";
|
||||
import { Nip7Signer } from "./impl/nip7";
|
||||
import { base64 } from "@scure/base";
|
||||
import { Nip10 } from "./impl/nip10";
|
||||
|
||||
type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
|
||||
|
Loading…
x
Reference in New Issue
Block a user