feat: file upload progress / imeta
This commit is contained in:
parent
a29d82bd56
commit
6448996529
47
packages/app/.yarn/.cache/webpack-dev-server/server.pem
Normal file
47
packages/app/.yarn/.cache/webpack-dev-server/server.pem
Normal 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-----
|
@ -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;
|
||||
|
11
packages/app/src/Element/Event/FileUpload.tsx
Normal file
11
packages/app/src/Element/Event/FileUpload.tsx
Normal 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>
|
||||
}
|
@ -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()}
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
23
packages/app/src/Element/Progress.css
Normal file
23
packages/app/src/Element/Progress.css
Normal 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;
|
||||
}
|
18
packages/app/src/Element/Progress.tsx
Normal file
18
packages/app/src/Element/Progress.tsx
Normal 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>
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -52,6 +52,7 @@ const config = {
|
||||
},
|
||||
devServer: {
|
||||
open: true,
|
||||
https: true,
|
||||
host: "localhost",
|
||||
historyApiFallback: true,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user