diff --git a/packages/app/.yarn/.cache/webpack-dev-server/server.pem b/packages/app/.yarn/.cache/webpack-dev-server/server.pem new file mode 100644 index 00000000..6bd4b6ba --- /dev/null +++ b/packages/app/.yarn/.cache/webpack-dev-server/server.pem @@ -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----- diff --git a/packages/app/src/Element/Embed/MediaElement.tsx b/packages/app/src/Element/Embed/MediaElement.tsx index 3db43944..4895a12e 100644 --- a/packages/app/src/Element/Embed/MediaElement.tsx +++ b/packages/app/src/Element/Embed/MediaElement.tsx @@ -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; diff --git a/packages/app/src/Element/Event/FileUpload.tsx b/packages/app/src/Element/Event/FileUpload.tsx new file mode 100644 index 00000000..1b5821c9 --- /dev/null +++ b/packages/app/src/Element/Event/FileUpload.tsx @@ -0,0 +1,11 @@ +import Progress from "Element/Progress"; +import { UploadProgress } from "Upload"; + +export default function FileUploadProgress({ progress }: { progress: Array }) { + return
+ {progress.map(p =>
+ {p.file.name} + +
)} +
+} \ No newline at end of file diff --git a/packages/app/src/Element/Event/NoteCreator.tsx b/packages/app/src/Element/Event/NoteCreator.tsx index 0393b249..150d973b 100644 --- a/packages/app/src/Element/Event/NoteCreator.tsx +++ b/packages/app/src/Element/Event/NoteCreator.tsx @@ -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()} )} + {uploader.progress.length > 0 && } {noteCreatorFooter()} {note.error && {note.error}} {note.advanced && noteCreatorAdvanced()} diff --git a/packages/app/src/Element/Event/ZapGoal.css b/packages/app/src/Element/Event/ZapGoal.css index f4201535..260bccde 100644 --- a/packages/app/src/Element/Event/ZapGoal.css +++ b/packages/app/src/Element/Event/ZapGoal.css @@ -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%; -} +} \ No newline at end of file diff --git a/packages/app/src/Element/Event/ZapGoal.tsx b/packages/app/src/Element/Event/ZapGoal.tsx index a871e5f9..b8185783 100644 --- a/packages/app/src/Element/Event/ZapGoal.tsx +++ b/packages/app/src/Element/Event/ZapGoal.tsx @@ -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 (
@@ -26,19 +28,14 @@ export function ZapGoal({ ev }: { ev: NostrEvent }) {
-
{progress.toFixed(1)}%
+
+ +
{formatShort(amount / 1000)}/{formatShort(target / 1000)}
-
-
-
+ ); } diff --git a/packages/app/src/Element/Progress.css b/packages/app/src/Element/Progress.css new file mode 100644 index 00000000..f9b79962 --- /dev/null +++ b/packages/app/src/Element/Progress.css @@ -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; +} diff --git a/packages/app/src/Element/Progress.tsx b/packages/app/src/Element/Progress.tsx new file mode 100644 index 00000000..5e35c0f0 --- /dev/null +++ b/packages/app/src/Element/Progress.tsx @@ -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
+
+ + + +
+} \ No newline at end of file diff --git a/packages/app/src/State/NoteCreator.tsx b/packages/app/src/State/NoteCreator.tsx index f5d08278..c8657484 100644 --- a/packages/app/src/State/NoteCreator.tsx +++ b/packages/app/src/State/NoteCreator.tsx @@ -18,6 +18,7 @@ interface NoteCreatorDataSnapshot { sensitive?: string; pollOptions?: Array; otherEvents?: Array; + extraTags?: Array>; sending?: Array; sendStarted: boolean; reset: () => void; @@ -63,6 +64,7 @@ class NoteCreatorStore extends ExternalStore { d.pollOptions = undefined; d.otherEvents = undefined; d.sending = undefined; + d.extraTags = undefined; } takeSnapshot(): NoteCreatorDataSnapshot { diff --git a/packages/app/src/Upload/NostrBuild.ts b/packages/app/src/Upload/NostrBuild.ts index bc50b594..324bf560 100644 --- a/packages/app/src/Upload/NostrBuild.ts +++ b/packages/app/src/Upload/NostrBuild.ts @@ -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; +} +interface NostrBuildUploadData { + input_name: string; + name: string; + url: string; + thumbnail: string; + blurhash: string; + sha256: string; + type: string; + mime: string; + size: number; + metadata: Record; + dimensions: { + width: number; + height: number; + }; +} diff --git a/packages/app/src/Upload/VoidCat.ts b/packages/app/src/Upload/VoidCat.ts index 8602817e..6fe206a3 100644 --- a/packages/app/src/Upload/VoidCat.ts +++ b/packages/app/src/Upload/VoidCat.ts @@ -13,6 +13,7 @@ export default async function VoidCatUpload( file: File | Blob, filename: string, publisher?: EventPublisher, + progress?: (n: number) => void, ): Promise { 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", diff --git a/packages/app/src/Upload/index.ts b/packages/app/src/Upload/index.ts index 8105c5ab..b006cea8 100644 --- a/packages/app/src/Upload/index.ts +++ b/packages/app/src/Upload/index.ts @@ -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; + progress: Array; +} + +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>([]); 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; +}; diff --git a/packages/app/webpack.config.js b/packages/app/webpack.config.js index 4f40b87f..d7eaf093 100644 --- a/packages/app/webpack.config.js +++ b/packages/app/webpack.config.js @@ -52,6 +52,7 @@ const config = { }, devServer: { open: true, + https: true, host: "localhost", historyApiFallback: true, },