1
0
forked from Kieran/snort

feat: file upload progress / imeta

This commit is contained in:
Kieran 2023-10-16 11:07:13 +01:00
parent a29d82bd56
commit 6448996529
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
13 changed files with 257 additions and 60 deletions

View File

@ -0,0 +1,47 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApyUVkYJVwV7XgluUnllgCtrsdq1ctRICm5gQy8nd+aEdDQjA
CKPOWh5miLl/fAQVZGZy/JxavzXulwXo8238E6n6bmNB1Us2nuw7a0aW4iUSQ1Pt
P4ZhPpcrqeqMf+hp7iBW0nAHFy/aa2UR84d7tBmSk5J3NNrfBsZdUex/7FqF1EVv
mEzlc8kepU9lRXWFQDtZCllEZ1kY3SBJPm10h0g9saI8YIVRxUuNII5GHDYAE3hb
EmoY6fuSEoiXA8u0Yt9soBQxgxIhQVKSRPPoIPjGFOxsGHY6h8R9nx1kxhHKFRuV
nwsn0uWl/7yjhwyHanogJu73/WgelPcgP/hMDQIDAQABAoIBAAru+xU0oGVwzcoi
MXuWPxkWrwcoWfsiPXduIBMklleg+WSD4QPvqyzr9isVb0huf/O8W+M4WxtM7NmG
MnHSDP5ATThxV7obHGyS6WQgDvimEibDU66nHK9adim8RQqM6nkANo23dE9I+xGx
X9Y9U5M5ZQQwPYoAkzw/N5WHUerk+cSEYWYV8jDtO7wJhYOMu5qliPeuNOaWZ1W6
1uwr8A4ih69WwzugPuBSgBrPAW1c84zWIFN+njAugqPF5x8xp2uM3tUO9s5UlHJC
FWEuU40KcDT2utSUY+2HXSHbycF4KLKT5jAKSa4sPziLfo+YifrlN0Y3rhofUlZT
jCaeZ8ECgYEA5/xpk8aVhCEvv5iCghv0p/IHcjdXjx5+PCWh3Adx0fF91UvU5oqn
okdyYZDShZMuLDfJ0lG+OMKZd01JapnbTtiVNceVRMnraIdoWEM2/4bTXTSZGtdA
8gh/Kc/PMbPf5ppVWwqTCbUkPOSyGHyGc7+DQquq1w6yZu04A3x9vHECgYEAuHJk
uz8YKY5ZUR7CZ3y7YFuwq5Lcpl43AfiiCasjRch0P8yLrITc/6fORsXyy64XW9fC
h3YmXvEPaM03W2dxw2aQDvXEvXiEITzmILs7SE3UmZR9m7OMy7Jeqr3+JOc0ckZe
Rz5FfuMt1IvNB6lrpfHVtoVrpCOXpzHgC/k/x10CgYA6lU18GfwL/+107uiWPsUL
3FzxBPTBmau7OK2lSOP/ZoKmaJ39Eiq/GlfSN6ZSQRa55+S5jhcBcnMa45OUrgHp
6VvU1u/lDTC7luZM07yBzuR1dyDq3Ez0Uhz6zBXAsXHrZDIF6ae0HeBm2EH5WQkD
Fevp3DwqTvXSdDle+AMwoQKBgQCBSlaH1rNmNc0wCsK07f8ejUcrDZgz2mjurc1P
v7HK8bdjHUtvE/ciEguLGqiV06O2EmjesZg2Bv4JNYivPrTFBrjGc8qEEd10uw6J
NRVaGoyDV04w/UwdYRvwzZs/XP4reF4PzHvEdRSkH5cJ3t2BhiKLfby1YumkHlbx
rbbiVQKBgB02jyZUiB6pPTCP8vXZCJbZELgqNyS04ALhBBpdfGMcU1+0hRLJFBaE
tClJPGARFXl+MPkY032vmJZOuH3LrcTCm8DmMLzM/hT1EWawQ8BJkkwiIokE4lqc
Bi8CrkvuQs2cuCStK6C3Nkyr1lTkDge46trsb7KTcfHdtLsS7EPj
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIJDji8iiceMvQlMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0yMzEwMTYwOTI0MThaFw0yMzExMTUxMDI0MThaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKclFZGCVcFe14JblJ5ZYAra7HatXLUSApuYEMvJ3fmhHQ0IwAijzloeZoi5
f3wEFWRmcvycWr817pcF6PNt/BOp+m5jQdVLNp7sO2tGluIlEkNT7T+GYT6XK6nq
jH/oae4gVtJwBxcv2mtlEfOHe7QZkpOSdzTa3wbGXVHsf+xahdRFb5hM5XPJHqVP
ZUV1hUA7WQpZRGdZGN0gST5tdIdIPbGiPGCFUcVLjSCORhw2ABN4WxJqGOn7khKI
lwPLtGLfbKAUMYMSIUFSkkTz6CD4xhTsbBh2OofEfZ8dZMYRyhUblZ8LJ9Llpf+8
o4cMh2p6ICbu9/1oHpT3ID/4TA0CAwEAAaOBrzCBrDAMBgNVHRMEBTADAQH/MAsG
A1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUF
BwMDBggrBgEFBQcDCDBcBgNVHREEVTBTgglsb2NhbGhvc3SCFWxvY2FsaG9zdC5s
b2NhbGRvbWFpboIGbHZoLm1lgggqLmx2aC5tZYIFWzo6MV2HBH8AAAGHEP6AAAAA
AAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBABY0rgWuzLYvVtvoVvWKS9cg
8rVhBRIFvpYO814ocN1iaxYQ9t9uLRsJXj0K+z1BHWf0zBiw4mB3dD9VpiKpuliL
4tRT+vATA96OYCd9G5k7DFQascAau40H3jxckh9rimIWa45FUSd7FIcddo1jeciv
gdAdiNUuHBen82O8KHJb+1PCBdA8RYeO5EGKfJM2yrOovu7dAFilf1ZPkXWgXnfG
nN6YfDDo9rAVDbvNXImrkwmGqEcN3Pq909IHiM/VETlU5lP4AbTNgrDa/aaZ+I+b
1MC1p87MvnibyXs+rTlK5+j8E6noNcD7tsHNd4ufkVCqr+pvSpuA3OvnXjbbm54=
-----END CERTIFICATE-----

View File

@ -1,14 +1,6 @@
import { ProxyImg } from "Element/ProxyImg";
import React from "react";
/*
[
"imeta",
"url https://nostr.build/i/148e3e8cbe29ae268b0d6aad0065a086319d3c3b1fdf8b89f1e2327d973d2d05.jpg",
"blurhash e6A0%UE2t6D*R%?u?a9G?aM|~pM|%LR*RjR-%2NG%2t7_2R*%1IVWB",
"dim 3024x4032"
],
*/
interface MediaElementProps {
mime: string;
url: string;

View File

@ -0,0 +1,11 @@
import Progress from "Element/Progress";
import { UploadProgress } from "Upload";
export default function FileUploadProgress({ progress }: { progress: Array<UploadProgress> }) {
return <div className="flex-column g8">
{progress.map(p => <div className="flex-column g2" id={p.id}>
{p.file.name}
<Progress value={p.progress} />
</div>)}
</div>
}

View File

@ -20,6 +20,7 @@ import { fetchNip05Pubkey } from "@snort/shared";
import { ZapTarget } from "Zapper";
import { useNoteCreator } from "State/NoteCreator";
import { NoteBroadcaster } from "./NoteBroadcaster";
import FileUploadProgress from "./FileUpload";
export function NoteCreator() {
const { formatMessage } = useIntl();
@ -114,6 +115,7 @@ export function NoteCreator() {
}
const hk = (eb: EventBuilder) => {
extraTags?.forEach(t => eb.tag(t));
note.extraTags?.forEach(t => eb.tag(t));
eb.kind(kind);
return eb;
};
@ -170,6 +172,17 @@ export function NoteCreator() {
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
} else if (rx.url) {
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
if (rx.metadata) {
v.extraTags ??= [];
const imeta = ["imeta", `url ${rx.url}`];
if (rx.metadata.blurhash) {
imeta.push(`blurhash ${rx.metadata.blurhash}`);
}
if (rx.metadata.width && rx.metadata.height) {
imeta.push(`dim ${rx.metadata.width}x${rx.metadata.height}`);
}
v.extraTags.push(imeta);
}
} else if (rx?.error) {
v.error = rx.error;
}
@ -290,18 +303,18 @@ export function NoteCreator() {
onChange={e => {
note.update(
v =>
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
note.selectedCustomRelays &&
note.selectedCustomRelays.length == a.length - 1
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
);
}}
/>
@ -373,9 +386,9 @@ export function NoteCreator() {
onChange={e =>
note.update(
v =>
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
)
}
/>
@ -536,6 +549,7 @@ export function NoteCreator() {
{renderPollOptions()}
</div>
)}
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
{noteCreatorFooter()}
{note.error && <span className="error">{note.error}</span>}
{note.advanced && noteCreatorAdvanced()}

View File

@ -1,21 +1,3 @@
.zap-goal {
}
.zap-goal h1 {
line-height: 1em;
}
.zap-goal .progress {
position: relative;
height: 1em;
border-radius: 4px;
overflow: hidden;
background-color: var(--gray);
}
.zap-goal .progress > div {
position: absolute;
background-color: var(--success);
width: var(--progress);
height: 100%;
}
}

View File

@ -1,5 +1,5 @@
import "./ZapGoal.css";
import { CSSProperties, useState } from "react";
import { useState } from "react";
import { NostrEvent, NostrLink } from "@snort/system";
import useZapsFeed from "Feed/ZapsFeed";
import { formatShort } from "Number";
@ -7,13 +7,15 @@ import { findTag } from "SnortUtils";
import Icon from "Icons/Icon";
import SendSats from "../SendSats";
import { Zapper } from "Zapper";
import Progress from "Element/Progress";
import { FormattedNumber } from "react-intl";
export function ZapGoal({ ev }: { ev: NostrEvent }) {
const [zap, setZap] = useState(false);
const zaps = useZapsFeed(NostrLink.fromEvent(ev));
const target = Number(findTag(ev, "amount"));
const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0);
const progress = 100 * (amount / target);
const progress = amount / target;
return (
<div className="zap-goal card">
@ -26,19 +28,14 @@ export function ZapGoal({ ev }: { ev: NostrEvent }) {
</div>
<div className="flex f-space">
<div>{progress.toFixed(1)}%</div>
<div>
<FormattedNumber value={progress} style="percent" />
</div>
<div>
{formatShort(amount / 1000)}/{formatShort(target / 1000)}
</div>
</div>
<div className="progress">
<div
style={
{
"--progress": `${Math.min(100, progress)}%`,
} as CSSProperties
}></div>
</div>
<Progress value={progress} />
</div>
);
}

View File

@ -0,0 +1,23 @@
.progress {
position: relative;
height: 1em;
border-radius: 4px;
overflow: hidden;
background-color: var(--gray);
}
.progress > div {
position: absolute;
background-color: var(--success);
width: var(--progress);
height: 100%;
}
.progress > span {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
font-size: small;
line-height: 1em;
}

View File

@ -0,0 +1,18 @@
import { FormattedNumber } from "react-intl";
import "./Progress.css";
import { CSSProperties } from "react";
export default function Progress({ value }: { value: number }) {
const v = Math.max(0.01, Math.min(1, value));
return <div className="progress">
<div
style={
{
"--progress": `${v * 100}%`,
} as CSSProperties
}></div>
<span>
<FormattedNumber value={v} style="percent" />
</span>
</div>
}

View File

@ -18,6 +18,7 @@ interface NoteCreatorDataSnapshot {
sensitive?: string;
pollOptions?: Array<string>;
otherEvents?: Array<NostrEvent>;
extraTags?: Array<Array<string>>;
sending?: Array<NostrEvent>;
sendStarted: boolean;
reset: () => void;
@ -63,6 +64,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
d.pollOptions = undefined;
d.otherEvents = undefined;
d.sending = undefined;
d.extraTags = undefined;
}
takeSnapshot(): NoteCreatorDataSnapshot {

View File

@ -30,17 +30,38 @@ export default async function NostrBuild(file: File | Blob, publisher?: EventPub
headers,
});
if (rsp.ok) {
const data = (await rsp.json()) as {
success: boolean;
data: Array<{
url: string;
}>;
};
const data = (await rsp.json()) as NostrBuildUploadResponse;
const res = data.data[0];
return {
url: data.data[0].url,
url: res.url,
metadata: {
blurhash: res.blurhash,
width: res.dimensions.width,
height: res.dimensions.height,
},
};
}
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

@ -13,6 +13,7 @@ export default async function VoidCatUpload(
file: File | Blob,
filename: string,
publisher?: EventPublisher,
progress?: (n: number) => void,
): Promise<UploadResult> {
const auth = publisher
? async (url: string, method: string) => {
@ -23,7 +24,9 @@ export default async function VoidCatUpload(
}
: undefined;
const api = new VoidApi(VoidCatHost, auth);
const uploader = api.getUploader(file);
const uploader = api.getUploader(file, undefined, px => {
progress?.(px / file.size);
});
const rsp = await uploader.upload({
"V-Strip-Metadata": "true",

View File

@ -1,5 +1,7 @@
import { useState } from "react";
import useLogin from "Hooks/useLogin";
import { NostrEvent } from "@snort/system";
import { v4 as uuid } from "uuid";
import NostrBuild from "Upload/NostrBuild";
import VoidCat from "Upload/VoidCat";
@ -16,6 +18,15 @@ export interface UploadResult {
* NIP-94 File Header
*/
header?: NostrEvent;
/**
* Media metadata
*/
metadata?: {
blurhash?: string;
width?: number;
height?: number;
};
}
/**
@ -38,27 +49,102 @@ export const UploaderServices = [
export interface Uploader {
upload: (f: File | Blob, filename: string) => Promise<UploadResult>;
progress: Array<UploadProgress>;
}
export interface UploadProgress {
id: string;
file: File | Blob;
progress: number;
}
export default function useFileUpload(): Uploader {
const fileUploader = useLogin().preferences.fileUploader;
const { publisher } = useEventPublisher();
const [progress, setProgress] = useState<Array<UploadProgress>>([]);
switch (fileUploader) {
case "nostr.build": {
return {
upload: f => NostrBuild(f, publisher),
progress: [],
} as Uploader;
}
case "nostrimg.com": {
return {
upload: NostrImg,
progress: [],
} as Uploader;
}
default: {
return {
upload: (f, n) => VoidCat(f, n, publisher),
upload: async (f, n) => {
const id = uuid();
setProgress(s => [
...s,
{
id,
file: f,
progress: 0,
},
]);
const px = (n: number) => {
setProgress(s =>
s.map(v =>
v.id === id
? {
...v,
progress: n,
}
: v,
),
);
};
const ret = await VoidCat(f, n, publisher, px);
setProgress(s => s.filter(a => a.id !== id));
return ret;
},
progress,
} as Uploader;
}
}
}
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;
};

View File

@ -52,6 +52,7 @@ const config = {
},
devServer: {
open: true,
https: true,
host: "localhost",
historyApiFallback: true,
},