feat: file picker
This commit is contained in:
@ -12,6 +12,7 @@ export interface ModalProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
ready?: boolean;
|
ready?: boolean;
|
||||||
|
largeModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
@ -49,9 +50,13 @@ export default function Modal(props: ModalProps) {
|
|||||||
className={
|
className={
|
||||||
props.bodyClassName ??
|
props.bodyClassName ??
|
||||||
classNames(
|
classNames(
|
||||||
"relative bg-layer-1 p-8 transition max-xl:rounded-t-3xl xl:rounded-3xl max-xl:mt-auto xl:my-auto lg:w-[500px] max-lg:w-full",
|
"relative bg-layer-1 p-8 transition max-xl:rounded-t-3xl xl:rounded-3xl max-xl:mt-auto xl:my-auto max-lg:w-full",
|
||||||
{ "max-xl:translate-y-0": props.ready ?? true },
|
{
|
||||||
{ "max-xl:translate-y-[50vh]": !(props.ready ?? true) },
|
"max-xl:translate-y-0": props.ready ?? true,
|
||||||
|
"max-xl:translate-y-[50vh]": !(props.ready ?? true),
|
||||||
|
"lg:w-[500px]": !(props.largeModal ?? false),
|
||||||
|
"lg:w-[80vw]": props.largeModal ?? false,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onMouseDown={e => e.stopPropagation()}
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
107
src/element/upload/file-list.tsx
Normal file
107
src/element/upload/file-list.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useLogin } from "@/hooks/login";
|
||||||
|
import { useMediaServerList } from "@/hooks/media-servers";
|
||||||
|
import { Nip96Server } from "@/service/upload/nip96";
|
||||||
|
import { findTag } from "@/utils";
|
||||||
|
import { NostrEvent } from "@snort/system";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
|
import { PrimaryButton } from "../buttons";
|
||||||
|
|
||||||
|
export function MediaServerFileList({ onPicked }: { onPicked: (files: Array<NostrEvent>) => void }) {
|
||||||
|
const login = useLogin();
|
||||||
|
const [fileList, setFilesList] = useState<Array<NostrEvent>>([]);
|
||||||
|
const [pickedFiles, setPickedFiles] = useState<Array<string>>([]);
|
||||||
|
const servers = useMediaServerList();
|
||||||
|
|
||||||
|
async function listFiles() {
|
||||||
|
const res = [];
|
||||||
|
const pub = login?.publisher();
|
||||||
|
if (!pub) return;
|
||||||
|
for (const s of servers.servers) {
|
||||||
|
try {
|
||||||
|
const sx = new Nip96Server(s, pub);
|
||||||
|
const files = await sx.listFiles();
|
||||||
|
if (files?.files) {
|
||||||
|
res.push(...files.files);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFilesList(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFile(ev: NostrEvent) {
|
||||||
|
const hash = findTag(ev, "x");
|
||||||
|
if (!hash) return;
|
||||||
|
setPickedFiles(a => {
|
||||||
|
if (a.includes(hash)) {
|
||||||
|
return a.filter(a => a != hash);
|
||||||
|
} else {
|
||||||
|
return [...a, hash];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listFiles().catch(console.error);
|
||||||
|
}, [servers.servers.length, login?.state?.version]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="File List" />
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{fileList.map(a => (
|
||||||
|
<Nip96File file={a} onClick={() => toggleFile(a)} checked={pickedFiles.includes(findTag(a, "x") ?? "")} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<PrimaryButton
|
||||||
|
disabled={pickedFiles.length === 0}
|
||||||
|
onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(findTag(a, "x") ?? "")))}>
|
||||||
|
<FormattedMessage defaultMessage="Select" />
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: boolean; onClick: () => void }) {
|
||||||
|
const mime = findTag(file, "m");
|
||||||
|
const url = findTag(file, "url");
|
||||||
|
const size = findTag(file, "size");
|
||||||
|
return (
|
||||||
|
<div onClick={() => onClick()}>
|
||||||
|
<div
|
||||||
|
className="relative bg-layer-2 rounded-xl overflow-hidden aspect-video cursor-pointer hover:outline outline-layer-3"
|
||||||
|
style={{
|
||||||
|
backgroundImage: mime?.startsWith("image/") ? `url(${url})` : "",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
}}>
|
||||||
|
<div className="absolute w-full h-full opacity-0 bg-black hover:opacity-80 flex flex-col items-center justify-center gap-4">
|
||||||
|
<div>
|
||||||
|
{Number(size) > 1024 * 1024 && (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n}MiB"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={Number(size) / 1024 / 1024} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{Number(size) < 1024 * 1024 && (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n}KiB"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={Number(size) / 1024} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{new Date(file.created_at * 1000).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" className="left-2 top-2 absolute" checked={checked} />
|
||||||
|
</div>
|
||||||
|
<small>{file.content}</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,31 +1,35 @@
|
|||||||
import { EventKind, UnknownTag } from "@snort/system";
|
import { EventKind, UnknownTag } from "@snort/system";
|
||||||
import { useLogin } from "./login";
|
import { useLogin } from "./login";
|
||||||
import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
||||||
import { Nip96Uploader } from "@/service/upload/nip96";
|
import { Nip96Server } from "@/service/upload/nip96";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useMediaServerList() {
|
export function useMediaServerList() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
|
|
||||||
const servers = login?.state?.getList(EventKind.StorageServerList) ?? [];
|
const servers = login?.state?.getList(EventKind.StorageServerList) ?? [];
|
||||||
|
|
||||||
return {
|
return useMemo(
|
||||||
servers: removeUndefined(servers.map(a => a.toEventTag()))
|
() => ({
|
||||||
.filter(a => a[0] === "server")
|
servers: removeUndefined(servers.map(a => a.toEventTag()))
|
||||||
.map(a => a[1]),
|
.filter(a => a[0] === "server")
|
||||||
addServer: async (s: string) => {
|
.map(a => a[1]),
|
||||||
const pub = login?.publisher();
|
addServer: async (s: string) => {
|
||||||
if (!pub) return;
|
const pub = login?.publisher();
|
||||||
|
if (!pub) return;
|
||||||
|
|
||||||
const u = sanitizeRelayUrl(s);
|
const u = sanitizeRelayUrl(s);
|
||||||
if (!u) return;
|
if (!u) return;
|
||||||
const server = new Nip96Uploader(u, pub);
|
const server = new Nip96Server(u, pub);
|
||||||
await server.loadInfo();
|
await server.loadInfo();
|
||||||
await login?.state?.addToList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
|
await login?.state?.addToList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
|
||||||
},
|
},
|
||||||
removeServer: async (s: string) => {
|
removeServer: async (s: string) => {
|
||||||
const u = sanitizeRelayUrl(s);
|
const u = sanitizeRelayUrl(s);
|
||||||
if (!u) return;
|
if (!u) return;
|
||||||
await login?.state?.removeFromList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
|
await login?.state?.removeFromList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
|
||||||
},
|
},
|
||||||
};
|
}),
|
||||||
|
[servers],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -405,6 +405,9 @@
|
|||||||
"NnHu0L": {
|
"NnHu0L": {
|
||||||
"defaultMessage": "Please upload at least 1 video"
|
"defaultMessage": "Please upload at least 1 video"
|
||||||
},
|
},
|
||||||
|
"Ntjkqd": {
|
||||||
|
"defaultMessage": "or"
|
||||||
|
},
|
||||||
"O2Cy6m": {
|
"O2Cy6m": {
|
||||||
"defaultMessage": "Yes, I am over 18"
|
"defaultMessage": "Yes, I am over 18"
|
||||||
},
|
},
|
||||||
@ -452,6 +455,9 @@
|
|||||||
"Pe0ogR": {
|
"Pe0ogR": {
|
||||||
"defaultMessage": "Theme"
|
"defaultMessage": "Theme"
|
||||||
},
|
},
|
||||||
|
"PySFxG": {
|
||||||
|
"defaultMessage": "Choose file."
|
||||||
|
},
|
||||||
"Q3au2v": {
|
"Q3au2v": {
|
||||||
"defaultMessage": "About {estimate}"
|
"defaultMessage": "About {estimate}"
|
||||||
},
|
},
|
||||||
@ -553,6 +559,9 @@
|
|||||||
"defaultMessage": "Value",
|
"defaultMessage": "Value",
|
||||||
"description": "Config value column header"
|
"description": "Config value column header"
|
||||||
},
|
},
|
||||||
|
"WcZM+B": {
|
||||||
|
"defaultMessage": "File List"
|
||||||
|
},
|
||||||
"Wp4l7+": {
|
"Wp4l7+": {
|
||||||
"defaultMessage": "More Videos"
|
"defaultMessage": "More Videos"
|
||||||
},
|
},
|
||||||
@ -633,6 +642,9 @@
|
|||||||
"bfvyfs": {
|
"bfvyfs": {
|
||||||
"defaultMessage": "Anon"
|
"defaultMessage": "Anon"
|
||||||
},
|
},
|
||||||
|
"c3LlRO": {
|
||||||
|
"defaultMessage": "{n}KiB"
|
||||||
|
},
|
||||||
"cPIKU2": {
|
"cPIKU2": {
|
||||||
"defaultMessage": "Following"
|
"defaultMessage": "Following"
|
||||||
},
|
},
|
||||||
@ -754,6 +766,9 @@
|
|||||||
"kGjqZ4": {
|
"kGjqZ4": {
|
||||||
"defaultMessage": "Latest Shorts"
|
"defaultMessage": "Latest Shorts"
|
||||||
},
|
},
|
||||||
|
"kQAf2d": {
|
||||||
|
"defaultMessage": "Select"
|
||||||
|
},
|
||||||
"kc5EOy": {
|
"kc5EOy": {
|
||||||
"defaultMessage": "Username is too long"
|
"defaultMessage": "Username is too long"
|
||||||
},
|
},
|
||||||
@ -790,6 +805,9 @@
|
|||||||
"n19IQE": {
|
"n19IQE": {
|
||||||
"defaultMessage": "Recommended size: 1920x1080 (16:9)"
|
"defaultMessage": "Recommended size: 1920x1080 (16:9)"
|
||||||
},
|
},
|
||||||
|
"n8k1SG": {
|
||||||
|
"defaultMessage": "{n}MiB"
|
||||||
|
},
|
||||||
"nBCvvJ": {
|
"nBCvvJ": {
|
||||||
"defaultMessage": "Topup"
|
"defaultMessage": "Topup"
|
||||||
},
|
},
|
||||||
|
@ -4,12 +4,13 @@ import { Icon } from "@/element/icon";
|
|||||||
import Modal from "@/element/modal";
|
import Modal from "@/element/modal";
|
||||||
import { Profile } from "@/element/profile";
|
import { Profile } from "@/element/profile";
|
||||||
import Spinner from "@/element/spinner";
|
import Spinner from "@/element/spinner";
|
||||||
|
import { MediaServerFileList } from "@/element/upload/file-list";
|
||||||
import { ServerList } from "@/element/upload/server-list";
|
import { ServerList } from "@/element/upload/server-list";
|
||||||
import useImgProxy from "@/hooks/img-proxy";
|
import useImgProxy from "@/hooks/img-proxy";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { useMediaServerList } from "@/hooks/media-servers";
|
import { useMediaServerList } from "@/hooks/media-servers";
|
||||||
import { Nip94Tags, UploadResult, nip94TagsToIMeta } from "@/service/upload";
|
import { Nip94Tags, UploadResult, nip94TagsToIMeta } from "@/service/upload";
|
||||||
import { Nip96Uploader } from "@/service/upload/nip96";
|
import { Nip96Server } from "@/service/upload/nip96";
|
||||||
import { openFile } from "@/utils";
|
import { openFile } from "@/utils";
|
||||||
import { ExternalStore, removeUndefined, unixNow, unwrap } from "@snort/shared";
|
import { ExternalStore, removeUndefined, unixNow, unwrap } from "@snort/shared";
|
||||||
import { EventPublisher, NostrLink } from "@snort/system";
|
import { EventPublisher, NostrLink } from "@snort/system";
|
||||||
@ -33,7 +34,7 @@ interface UploadDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UploadManager extends ExternalStore<Array<UploadStatus>> {
|
class UploadManager extends ExternalStore<Array<UploadStatus>> {
|
||||||
#uploaders: Map<string, Nip96Uploader> = new Map();
|
#uploaders: Map<string, Nip96Server> = new Map();
|
||||||
#uploads: Map<string, UploadStatus> = new Map();
|
#uploads: Map<string, UploadStatus> = new Map();
|
||||||
#id: string;
|
#id: string;
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ class UploadManager extends ExternalStore<Array<UploadStatus>> {
|
|||||||
async uploadTo(server: string, file: File, pub: EventPublisher, type: UploadStatus["type"]) {
|
async uploadTo(server: string, file: File, pub: EventPublisher, type: UploadStatus["type"]) {
|
||||||
let uploader = this.#uploaders.get(server);
|
let uploader = this.#uploaders.get(server);
|
||||||
if (!uploader) {
|
if (!uploader) {
|
||||||
uploader = new Nip96Uploader(server, pub);
|
uploader = new Nip96Server(server, pub);
|
||||||
this.#uploaders.set(server, uploader);
|
this.#uploaders.set(server, uploader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +174,7 @@ export function UploadPage() {
|
|||||||
const [summary, setSummary] = useState("");
|
const [summary, setSummary] = useState("");
|
||||||
const [thumb, setThumb] = useState("");
|
const [thumb, setThumb] = useState("");
|
||||||
const [editServers, setEditServers] = useState(false);
|
const [editServers, setEditServers] = useState(false);
|
||||||
|
const [mediaPicker, setMediaPicker] = useState(false);
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
const uploads = useSyncExternalStore(
|
const uploads = useSyncExternalStore(
|
||||||
c => manager.hook(c),
|
c => manager.hook(c),
|
||||||
@ -298,10 +300,16 @@ export function UploadPage() {
|
|||||||
|
|
||||||
const uploadButton = () => {
|
const uploadButton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4 bg-layer-3 rounded-lg p-4">
|
<>
|
||||||
<Icon name="upload" />
|
<div className="flex items-center gap-4 bg-layer-3 rounded-lg p-4" onClick={() => uploadFile()}>
|
||||||
<FormattedMessage defaultMessage="Upload Video" />
|
<Icon name="upload" />
|
||||||
</div>
|
<FormattedMessage defaultMessage="Upload Video" />
|
||||||
|
</div>
|
||||||
|
<FormattedMessage defaultMessage="or" />
|
||||||
|
<div className="flex items-center gap-4 bg-layer-3 rounded-lg p-4" onClick={() => setMediaPicker(true)}>
|
||||||
|
<FormattedMessage defaultMessage="Choose file." />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -313,9 +321,7 @@ export function UploadPage() {
|
|||||||
<FormattedMessage defaultMessage="Manage Servers" />
|
<FormattedMessage defaultMessage="Manage Servers" />
|
||||||
</Layer3Button>
|
</Layer3Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="relative bg-layer-2 rounded-xl w-full aspect-video cursor-pointer overflow-hidden">
|
||||||
onClick={() => uploadFile()}
|
|
||||||
className="relative bg-layer-2 rounded-xl w-full aspect-video cursor-pointer overflow-hidden">
|
|
||||||
{videos > 0 && (
|
{videos > 0 && (
|
||||||
<video
|
<video
|
||||||
className="w-full h-full absolute"
|
className="w-full h-full absolute"
|
||||||
@ -324,7 +330,7 @@ export function UploadPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{videos === 0 && (
|
{videos === 0 && (
|
||||||
<div className="absolute w-full h-full flex items-center justify-center">{uploadButton()}</div>
|
<div className="absolute w-full h-full flex items-center gap-4 justify-center">{uploadButton()}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@ -407,6 +413,15 @@ export function UploadPage() {
|
|||||||
<ServerList />
|
<ServerList />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{mediaPicker && (
|
||||||
|
<Modal id="media-picker" onClose={() => setMediaPicker(false)} largeModal={true}>
|
||||||
|
<MediaServerFileList
|
||||||
|
onPicked={files => {
|
||||||
|
setMediaPicker(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { base64 } from "@scure/base";
|
import { base64 } from "@scure/base";
|
||||||
import { throwIfOffline } from "@snort/shared";
|
import { throwIfOffline } from "@snort/shared";
|
||||||
import { EventKind, EventPublisher } from "@snort/system";
|
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
|
||||||
|
|
||||||
import { FileExtensionRegex, UploadResult, readNip94Tags } from ".";
|
import { FileExtensionRegex, UploadResult, readNip94Tags } from ".";
|
||||||
|
|
||||||
export class Nip96Uploader {
|
export class Nip96Server {
|
||||||
|
#info?: Nip96Info;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
readonly publisher: EventPublisher,
|
readonly publisher: EventPublisher,
|
||||||
@ -20,39 +22,26 @@ export class Nip96Uploader {
|
|||||||
const u = new URL(this.url);
|
const u = new URL(this.url);
|
||||||
|
|
||||||
const rsp = await fetch(`${u.protocol}//${u.host}/.well-known/nostr/nip96.json`);
|
const rsp = await fetch(`${u.protocol}//${u.host}/.well-known/nostr/nip96.json`);
|
||||||
return (await rsp.json()) as Nip96Info;
|
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");
|
||||||
|
if (rsp.ok) {
|
||||||
|
return (await rsp.json()) as Nip96FileList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(file: File | Blob, filename: string): Promise<UploadResult> {
|
async upload(file: File | Blob, filename: string): Promise<UploadResult> {
|
||||||
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 = await this.loadInfo();
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("size", file.size.toString());
|
fd.append("size", file.size.toString());
|
||||||
fd.append("caption", filename);
|
fd.append("caption", filename);
|
||||||
fd.append("media_type", file.type);
|
fd.append("media_type", file.type);
|
||||||
fd.append("file", file);
|
fd.append("file", file);
|
||||||
|
|
||||||
let u = info.api_url;
|
const rsp = await this.#req("", "POST", fd);
|
||||||
if (u.startsWith("/")) {
|
|
||||||
u = `${this.url}${u.slice(1)}`;
|
|
||||||
}
|
|
||||||
const rsp = await fetch(u, {
|
|
||||||
body: fd,
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
accept: "application/json",
|
|
||||||
authorization: await auth(u, "POST"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
throwIfOffline();
|
|
||||||
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);
|
||||||
@ -72,10 +61,15 @@ export class Nip96Uploader {
|
|||||||
meta.url += ".webp";
|
meta.url += ".webp";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
case "image/jpeg":
|
||||||
|
case "image/jpg": {
|
||||||
meta.url += ".jpg";
|
meta.url += ".jpg";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "video/mp4": {
|
||||||
|
meta.url += ".mp4";
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -100,6 +94,31 @@ export class Nip96Uploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface Nip96Info {
|
||||||
@ -111,8 +130,12 @@ export interface Nip96Result {
|
|||||||
status: string;
|
status: string;
|
||||||
message: string;
|
message: string;
|
||||||
processing_url?: string;
|
processing_url?: string;
|
||||||
nip94_event: {
|
nip94_event: NostrEvent;
|
||||||
tags: Array<Array<string>>;
|
}
|
||||||
content: string;
|
|
||||||
};
|
export interface Nip96FileList {
|
||||||
|
count: number;
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
files: Array<NostrEvent>;
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,7 @@
|
|||||||
"My6HwN": "Ok, it's safe",
|
"My6HwN": "Ok, it's safe",
|
||||||
"NTpJIm": "Enable Recording",
|
"NTpJIm": "Enable Recording",
|
||||||
"NnHu0L": "Please upload at least 1 video",
|
"NnHu0L": "Please upload at least 1 video",
|
||||||
|
"Ntjkqd": "or",
|
||||||
"O2Cy6m": "Yes, I am over 18",
|
"O2Cy6m": "Yes, I am over 18",
|
||||||
"O7AeYh": "Description..",
|
"O7AeYh": "Description..",
|
||||||
"OEW7yJ": "Zaps",
|
"OEW7yJ": "Zaps",
|
||||||
@ -149,6 +150,7 @@
|
|||||||
"PXAur5": "Withdraw",
|
"PXAur5": "Withdraw",
|
||||||
"Pc+tM3": "Generate",
|
"Pc+tM3": "Generate",
|
||||||
"Pe0ogR": "Theme",
|
"Pe0ogR": "Theme",
|
||||||
|
"PySFxG": "Choose file.",
|
||||||
"Q3au2v": "About {estimate}",
|
"Q3au2v": "About {estimate}",
|
||||||
"Q8Qw5B": "Description",
|
"Q8Qw5B": "Description",
|
||||||
"QNvtaq": "Share on X",
|
"QNvtaq": "Share on X",
|
||||||
@ -182,6 +184,7 @@
|
|||||||
"W7IRLs": "Your title is too short",
|
"W7IRLs": "Your title is too short",
|
||||||
"W9355R": "Unmute",
|
"W9355R": "Unmute",
|
||||||
"WVJZ0U": "Value",
|
"WVJZ0U": "Value",
|
||||||
|
"WcZM+B": "File List",
|
||||||
"Wp4l7+": "More Videos",
|
"Wp4l7+": "More Videos",
|
||||||
"WsjXrZ": "Click on Log In",
|
"WsjXrZ": "Click on Log In",
|
||||||
"X2PZ7D": "Create Goal",
|
"X2PZ7D": "Create Goal",
|
||||||
@ -208,6 +211,7 @@
|
|||||||
"bD/ZwY": "Edit Cards",
|
"bD/ZwY": "Edit Cards",
|
||||||
"bbUGS7": "Recommended Stream Settings",
|
"bbUGS7": "Recommended Stream Settings",
|
||||||
"bfvyfs": "Anon",
|
"bfvyfs": "Anon",
|
||||||
|
"c3LlRO": "{n}KiB",
|
||||||
"cPIKU2": "Following",
|
"cPIKU2": "Following",
|
||||||
"ccXLVi": "Category",
|
"ccXLVi": "Category",
|
||||||
"cg1VJ2": "Connect Wallet",
|
"cg1VJ2": "Connect Wallet",
|
||||||
@ -248,6 +252,7 @@
|
|||||||
"k21gTS": "e.g. about me",
|
"k21gTS": "e.g. about me",
|
||||||
"kAEQyV": "OK",
|
"kAEQyV": "OK",
|
||||||
"kGjqZ4": "Latest Shorts",
|
"kGjqZ4": "Latest Shorts",
|
||||||
|
"kQAf2d": "Select",
|
||||||
"kc5EOy": "Username is too long",
|
"kc5EOy": "Username is too long",
|
||||||
"khJ51Q": "Stream Earnings",
|
"khJ51Q": "Stream Earnings",
|
||||||
"kp0NPF": "Planned",
|
"kp0NPF": "Planned",
|
||||||
@ -260,6 +265,7 @@
|
|||||||
"msjwph": "Keyframe Interval",
|
"msjwph": "Keyframe Interval",
|
||||||
"mtNGwh": "A short description of the content",
|
"mtNGwh": "A short description of the content",
|
||||||
"n19IQE": "Recommended size: 1920x1080 (16:9)",
|
"n19IQE": "Recommended size: 1920x1080 (16:9)",
|
||||||
|
"n8k1SG": "{n}MiB",
|
||||||
"nBCvvJ": "Topup",
|
"nBCvvJ": "Topup",
|
||||||
"nOaArs": "Setup Profile",
|
"nOaArs": "Setup Profile",
|
||||||
"nwA8Os": "Add card",
|
"nwA8Os": "Add card",
|
||||||
|
Reference in New Issue
Block a user