feat: new UI

This commit is contained in:
2024-09-24 13:49:17 +01:00
parent c8da87e0dd
commit 9bcdeabda8
55 changed files with 6732 additions and 189 deletions

View File

@ -0,0 +1,71 @@
import { base64, bytesToString } from "@scure/base";
import { throwIfOffline, unixNow } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
export interface BlobDescriptor {
url?: string;
sha256: string;
size: number;
type?: string;
uploaded?: number;
}
export class Blossom {
constructor(
readonly url: string,
readonly publisher: EventPublisher,
) {
this.url = new URL(this.url).toString();
}
async upload(file: File) {
const hash = await window.crypto.subtle.digest(
"SHA-256",
await file.arrayBuffer(),
);
const tags = [["x", bytesToString("hex", new Uint8Array(hash))]];
const rsp = await this.#req("/upload", "PUT", file, tags);
if (rsp.ok) {
return (await rsp.json()) as BlobDescriptor;
} else {
const text = await rsp.text();
throw new Error(text);
}
}
async #req(
path: string,
method: "GET" | "POST" | "DELETE" | "PUT",
body?: BodyInit,
tags?: Array<Array<string>>,
) {
throwIfOffline();
const url = `${this.url}upload`;
const now = unixNow();
const auth = async (url: string, method: string) => {
const auth = await this.publisher.generic((eb) => {
eb.kind(24_242 as EventKind)
.tag(["u", url])
.tag(["method", method])
.tag(["t", path.slice(1)])
.tag(["expiration", (now + 10).toString()]);
tags?.forEach((t) => eb.tag(t));
return eb;
});
return `Nostr ${base64.encode(
new TextEncoder().encode(JSON.stringify(auth)),
)}`;
};
return await fetch(url, {
method,
body,
headers: {
accept: "application/json",
authorization: await auth(url, method),
},
});
}
}

View File

@ -0,0 +1,30 @@
export async function openFile(): Promise<File | undefined> {
return new Promise((resolve) => {
const elm = document.createElement("input");
let lock = false;
elm.type = "file";
const handleInput = (e: Event) => {
lock = true;
const elm = e.target as HTMLInputElement;
if ((elm.files?.length ?? 0) > 0) {
resolve(elm.files![0]);
} else {
resolve(undefined);
}
};
elm.onchange = (e) => handleInput(e);
elm.click();
window.addEventListener(
"focus",
() => {
setTimeout(() => {
if (!lock) {
resolve(undefined);
}
}, 300);
},
{ once: true },
);
});
}

108
ui_src/src/upload/nip96.ts Normal file
View File

@ -0,0 +1,108 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
export class Nip96 {
#info?: Nip96Info;
constructor(
readonly url: string,
readonly publisher: EventPublisher,
) {
this.url = new URL(this.url).toString();
}
async loadInfo() {
const u = new URL(this.url);
const rsp = await fetch(
`${u.protocol}//${u.host}/.well-known/nostr/nip96.json`,
);
this.#info = (await rsp.json()) as Nip96Info;
return this.#info;
}
async listFiles(page = 0, count = 50) {
const rsp = await this.#req(`?page=${page}&count=${count}`, "GET");
const data = await this.#handleResponse<Nip96FileList>(rsp);
return data;
}
async upload(file: File) {
const fd = new FormData();
fd.append("size", file.size.toString());
fd.append("caption", file.name);
fd.append("media_type", file.type);
fd.append("file", file);
const rsp = await this.#req("", "POST", fd);
const data = await this.#handleResponse<Nip96Result>(rsp);
if(data.status !== "success") {
throw new Error(data.message);
}
return data;
}
async #handleResponse<T extends Nip96Status>(rsp: Response) {
if (rsp.ok) {
return (await rsp.json()) as T;
} else {
const text = await rsp.text();
try {
const obj = JSON.parse(text) as Nip96Result;
throw new Error(obj.message);
} catch {
throw new Error(`Upload failed: ${text}`);
}
}
}
async #req(path: string, method: "GET" | "POST" | "DELETE", body?: BodyInit) {
throwIfOffline();
const auth = async (url: string, method: string) => {
const auth = await this.publisher.generic((eb) => {
return eb
.kind(EventKind.HttpAuthentication)
.tag(["u", url])
.tag(["method", method]);
});
return `Nostr ${base64.encode(
new TextEncoder().encode(JSON.stringify(auth)),
)}`;
};
const info = this.#info ?? (await this.loadInfo());
let u = info.api_url;
if (u.startsWith("/")) {
u = `${this.url}${u.slice(1)}`;
}
u += path;
return await fetch(u, {
method,
body,
headers: {
accept: "application/json",
authorization: await auth(u, method),
},
});
}
}
export interface Nip96Info {
api_url: string;
download_url?: string;
}
export interface Nip96Status {status: string, message?: string}
export type Nip96Result = Nip96Status & {
processing_url?: string;
nip94_event: NostrEvent;
}
export type Nip96FileList = Nip96Status & {
count: number;
total: number;
page: number;
files: Array<NostrEvent>;
}