feat: Improved upload and image post editor

This commit is contained in:
florian 2024-04-17 09:34:49 +02:00
parent 5b399482ed
commit caf95a2eeb
12 changed files with 408 additions and 159 deletions

View File

@ -1,30 +1,48 @@
# React + TypeScript + Vite
# Bouquet
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
A tool to manage your content on blossom severs (Upload, Distribution, Posting)
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
## TODO / Ideas
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
Add Blurhash
https://github.com/verbiricha/filestr/blob/master/src/blur.tsx
- Configure the top-level `parserOptions` property like this:
Upload
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
Audio
Audio Event 31337, maybe Podcast 31338
Album Event 30029
Read ID3 INfo
Display in Metadata Editor
Post Audio Events
Option to select "Full Album" and POst Album (Playlist) Event
Upload of Album Art from Disc
Usage of Album Art from ID3
--> Album art as new blob
Video
FileMeta Data Event 1063
Video Preview
PDF
FileMeta Data Event 1063
Images
FileMeta Data Event 1063
dimensions
blurhash
Nav
Add AUdio Player like Soundcloud
Blob List
- Selection -> Delete Selected
Audio List
Audio List, mit Mini Thumnnail (Artitst / Title )
Join von Album/year aus Album (Playlist) Event ????
Display blob as "published" when in Audio Event, else as "unlisted"
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

BIN
bun.lockb

Binary file not shown.

107
package-lock.json generated
View File

@ -20,7 +20,9 @@
"blossom-client-sdk": "^0.4.0",
"dayjs": "^1.11.10",
"id3js": "^2.1.1",
"lodash": "^4.17.21",
"nostr-tools": "^2.4.0",
"p-limit": "^5.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "^7.7.1",
@ -28,6 +30,7 @@
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.28.6",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@ -1667,6 +1670,12 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
@ -3787,6 +3796,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -4246,6 +4260,35 @@
}
},
"node_modules/p-limit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate/node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
@ -4260,14 +4303,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"node_modules/p-locate/node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
@ -5847,12 +5887,11 @@
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"engines": {
"node": ">=10"
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -6809,6 +6848,12 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
@ -8340,6 +8385,11 @@
"p-locate": "^5.0.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -8649,12 +8699,11 @@
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
"requires": {
"yocto-queue": "^0.1.0"
"yocto-queue": "^1.0.0"
}
},
"p-locate": {
@ -8664,6 +8713,23 @@
"dev": true,
"requires": {
"p-limit": "^3.0.2"
},
"dependencies": {
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
}
}
},
"parent-module": {
@ -9711,10 +9777,9 @@
"dev": true
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g=="
}
}
}

View File

@ -24,7 +24,9 @@
"blossom-client-sdk": "^0.4.0",
"dayjs": "^1.11.10",
"id3js": "^2.1.1",
"lodash": "^4.17.21",
"nostr-tools": "^2.4.0",
"p-limit": "^5.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "^7.7.1",
@ -32,6 +34,7 @@
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.28.6",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@typescript-eslint/eslint-plugin": "^7.2.0",

View File

@ -3,16 +3,7 @@
}
.blob-list {
@apply bg-base-200 p-4 text-neutral-content rounded-lg;
}
.blob-list .blob {
@apply p-1 hover:bg-base-200 rounded-md grid pr-4;
grid-template-columns: 2em auto /*auto*/ 2em 6em 10em 7em 3em;
}
.blob-list .blob span {
@apply overflow-ellipsis overflow-hidden text-nowrap;
@apply p-4 text-neutral-content rounded-lg;
}
.blog-list-header {
@ -23,18 +14,10 @@
@apply flex-grow;
}
.blog-list-header button {
@apply btn p-2 ml-2 my-2 text-neutral-content rounded-lg;
}
.blog-list-header button.selected {
@apply btn-primary text-primary-content;
}
.blog-list-header svg {
@apply w-6 opacity-80 hover:opacity-100;
}
.blob-list .blob span a.pill {
@apply bg-base-200 p-1 px-2 rounded-2xl text-white;
}
.blob-list .table :where(th, td) {
padding: .25em;
}

View File

@ -17,6 +17,10 @@ import * as id3 from 'id3js';
import { ID3Tag, ID3TagV2 } from 'id3js/lib/id3Tag';
import { useQueries } from '@tanstack/react-query';
import { useServerInfo } from '../../utils/useServerInfo';
import useFileMetaEventsByHash, { KIND_BLOSSOM_DRIVE, KIND_FILE_META } from '../../utils/useFileMetaEvents';
import { nip19 } from 'nostr-tools';
import { AddressPointer, EventPointer } from 'nostr-tools/nip19';
import { NDKEvent } from '@nostr-dev-kit/ndk';
type ListMode = 'gallery' | 'list' | 'audio' | 'video' | 'docs';
@ -31,6 +35,7 @@ type AudioBlob = BlobDescriptor & { id3?: ID3Tag; imageData?: string };
const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
const [mode, setMode] = useState<ListMode>('list');
const { distribution } = useServerInfo();
const fileMetaEventsByHash = useFileMetaEventsByHash();
const images = useMemo(
() => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
@ -121,6 +126,46 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
</div>
);
const Badge = ({ ev }: { ev: NDKEvent }) => {
if (ev.kind == KIND_FILE_META) {
const nevent = nip19.neventEncode({
kind: ev.kind,
id: ev.id,
author: ev.author.pubkey,
relays: ev.onRelays.map(r => r.url),
} as EventPointer);
return (
<a target="_blank" href={`https://filestr.vercel.app/e/${nevent}`}>
<div className="badge badge-primary mr-2">published</div>
</a>
);
}
if (ev.kind == KIND_BLOSSOM_DRIVE) {
const naddr = nip19.naddrEncode({
kind: ev.kind,
identifier: ev.tagValue('d'),
pubkey: ev.author.pubkey,
relays: ev.onRelays.map(r => r.url),
} as AddressPointer);
return (
<a target="_blank" className="badge badge-primary mr-2" href={`https://blossom.hzrd149.com/#/drive/${naddr}`}>
🌸 drive
</a>
);
}
return <></>;
}
const Badges = ({ blob }: { blob: BlobDescriptor }) => {
const events = fileMetaEventsByHash[blob.sha256];
if (!events) return;
return events.map(ev => <Badge ev={ev}></Badge>)
};
return (
<>
<div className={`blog-list-header ${!title ? 'justify-end' : ''}`}>
@ -293,34 +338,42 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
{mode == 'list' && (
<div className="blob-list">
{blobs.map((blob: BlobDescriptor) => (
<div className="blob" key={blob.sha256}>
<span>
<DocumentIcon />
</span>
<span>
<a className="link link-primary" href={blob.url} target="_blank">
{blob.sha256}
</a>
</span>
{/*
<span>
<a className="pill">🌸 drive</a> <a className="pill">📝 post</a>
</span>
*/}
<span>
{distribution[blob.sha256].servers.length == 1 ? (
<ExclamationTriangleIcon title="Not distributed to any other server" />
) : (
''
)}
</span>
<span>{formatFileSize(blob.size)}</span>
<span>{blob.type && `${blob.type}`}</span>
<span>{formatDate(blob.created)}</span>
<Actions blob={blob}></Actions>
</div>
))}
<table className="table hover">
<thead>
<tr>
<th>Hash</th>
<th>Uses</th>
<th>Size</th>
<th>Type</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{blobs.map((blob: BlobDescriptor) => (
<tr className="hover" key={blob.sha256}>
<td className=" whitespace-nowrap">
<DocumentIcon />
<a className="link link-primary" href={blob.url} target="_blank">
{blob.sha256.slice(0, 15)}
</a>
</td>
<td>
<Badges blob={blob} />
<span className="text-warning tooltip" data-tip="Not distributed to any other server">
{distribution[blob.sha256].servers.length == 1 && <ExclamationTriangleIcon />}
</span>
</td>
<td>{formatFileSize(blob.size)}</td>
<td>{blob.type && `${blob.type}`}</td>
<td>{formatDate(blob.created)}</td>
<td className=" whitespace-nowrap">
<Actions blob={blob}></Actions>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>

View File

@ -2,14 +2,16 @@ import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import { useNDK } from '../../ndk';
import dayjs from 'dayjs';
import { useState } from 'react';
import uniq from 'lodash/uniq';
import { formatFileSize } from '../../utils';
export type FileEventData = {
content: string;
url: string;
url: string[];
dim?: string;
x: string;
m?: string;
size?: string;
size: number;
//summary: string;
//alt: string;
};
@ -23,8 +25,8 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
created_at: dayjs().unix(),
content: data.content,
tags: [
...uniq(data.url).map(du => ['url', du]),
['x', data.x],
['url', data.url],
//['summary', data.summary],
//['alt', data.alt],
],
@ -33,7 +35,7 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
};
if (data.size) {
e.tags.push(['size', data.size]);
e.tags.push(['size', `${data.size}`]);
}
if (data.dim) {
e.tags.push(['dim', data.dim]);
@ -45,21 +47,45 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
const ev = new NDKEvent(ndk, e);
await ev.sign();
console.log(ev.rawEvent());
await ev.publish();
// await ev.publish();
};
return (
<div>
<pre>{JSON.stringify(fileEventData, null, 2)}</pre>
<img src={`https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${fileEventData.url}`}></img>
{fileEventData.dim ? `(${fileEventData.dim})` : ''}
<div className="flex flex-col gap-4">
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
{fileEventData.m?.startsWith('image/') && (
<div className="p-4 bg-base-300">
<img
width={200}
height={200}
src={`https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${fileEventData.url[0]}`}
></img>
</div>
)}
<div className="grid gap-4" style={{ gridTemplateColumns: '1fr 30em' }}>
<span className="font-bold">Type</span>
<span>{fileEventData.m}</span>
{fileEventData.dim && (
<>
<span className="font-bold">Dimensions</span>
<span>{fileEventData.dim}</span>
</>
)}
<span className="font-bold">File size</span>
<span>{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}</span>
<span className="font-bold">Content / Description</span>
<textarea
value={fileEventData.content}
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
className="textarea textarea-secondary"
className="textarea"
placeholder="Caption"
></textarea>
<span className="font-bold">URL</span>
<textarea
value={fileEventData.url.join('\n')}
className="textarea"
placeholder="URL"/>
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
Publish
</button>

View File

@ -3,11 +3,11 @@
}
.content {
@apply flex flex-col self-center sm:w-10/12 w-full min-h-[80vh];
@apply flex flex-col self-center md:w-10/12 w-full min-h-[80vh] px-4 md:px-0;
}
.title {
@apply text-neutral-content text-4xl flex flex-row items-center gap-2 p-4 sm:w-10/12 w-full self-center;
@apply text-neutral-content text-4xl flex flex-row items-center gap-2 p-4 md:w-10/12 w-full self-center;
}
.title img {

View File

@ -1,13 +1,13 @@
const ProgressBar = ({ value, max, description = '' }: { value: number; max: number; description?: string }) => {
const percent = Math.floor((value * 100) / max);
const showDescription = percent > 10 && percent < 100;
const showDescription = description.length > 0;
return (
<div className="w-full bg-base-200 rounded-lg">
{max !== undefined && value !== undefined && max > 0 && (
<div className="grid items-center gap-4" style={{gridTemplateColumns:'8fr 5em minmax(0, 1fr)'}}>
<div className="grid items-center gap-4" style={{ gridTemplateColumns: '8fr 4em 6em' }}>
<progress className="progress w-full accent-primary" value={percent} max="100" />
<span>{percent}%</span>
<span>{showDescription ? description : ''}</span>
<span className="whitespace-nowrap overflow-ellipsis">{showDescription ? description : ''}</span>
</div>
)}
</div>

View File

@ -1,5 +1,5 @@
import { ChangeEvent, DragEvent, useEffect, useMemo, useState } from 'react';
import { useServers } from '../utils/useServers';
import { Server, useServers } from '../utils/useServers';
import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useServerInfo } from '../utils/useServerInfo';
@ -11,11 +11,13 @@ import CheckBox from '../components/CheckBox/CheckBox';
import ProgressBar from '../components/ProgressBar/ProgressBar';
import { formatFileSize } from '../utils';
import FileEventEditor, { FileEventData } from '../components/FileEventEditor/FileEventEditor';
import pLimit from 'p-limit';
type TransferStats = {
enabled: boolean;
size: number;
transferred: number;
rate: number;
};
/*
@ -41,7 +43,7 @@ function Upload() {
const [transfers, setTransfers] = useState<{ [key: string]: TransferStats }>({});
const [files, setFiles] = useState<File[]>([]);
const [cleanPrivateData, setCleanPrivateData] = useState(true);
const [transferSpeed, setTransferSpeed] = useState<number | undefined>();
const limit = pLimit(3);
const [fileEventsToPublish, setFileEventsToPublish] = useState<FileEventData[]>([]);
@ -98,14 +100,50 @@ function Upload() {
// TODO use https://github.com/davejm/client-compress
// for image resizing
const fileDimensions: { [key: string]: ImageSize } = {};
const fileDimensions: { [key: string]: FileEventData } = {};
for (const file of filesToUpload) {
let data = { content: file.name, url: [] as string[] } as FileEventData;
if (file.type.startsWith('image/')) {
const dimensions = await getImageSize(file);
fileDimensions[file.name] = dimensions;
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };
}
fileDimensions[file.name] = data;
}
const startTransfer = async (server: Server, primary: boolean) => {
const serverUrl = serverInfo[server.name].url;
let serverTransferred = 0;
for (const file of filesToUpload) {
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
const newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
setTransfers(ut => ({
...ut,
[server.name]: {
...ut[server.name],
transferred: serverTransferred + progressEvent.loaded,
rate: progressEvent.rate || 0,
},
}));
});
serverTransferred += file.size;
setTransfers(ut => ({
...ut,
[server.name]: { ...ut[server.name], transferred: serverTransferred, rate: 0 },
}));
fileDimensions[file.name] = {
...fileDimensions[file.name],
x: newBlob.sha256,
url: primary ? [newBlob.url, ...fileDimensions[file.name].url] : [...fileDimensions[file.name].url, newBlob.url],
size: newBlob.size,
m: newBlob.type,
};
}
queryClient.invalidateQueries({ queryKey: ['blobs', server.name] });
};
if (filesToUpload && filesToUpload.length) {
// sum files sizes
const totalSize = filesToUpload.reduce((acc, f) => acc + f.size, 0);
@ -121,51 +159,14 @@ function Upload() {
return newTransfers;
});
for (const server of servers) {
if (!transfers[server.name]?.enabled) {
continue;
}
const serverUrl = serverInfo[server.name].url;
let serverTransferred = 0;
for (const file of filesToUpload) {
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
const enabledServers = servers.filter(s => transfers[s.name]?.enabled);
const primaryServerName = servers[0].name;
const newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
setTransferSpeed(progressEvent.rate);
setTransfers(ut => ({
...ut,
[server.name]: { ...ut[server.name], transferred: serverTransferred + progressEvent.loaded },
}));
});
await Promise.all(enabledServers.map(s => limit(() => startTransfer(s, s.name == primaryServerName))));
serverTransferred += file.size;
setTransfers(ut => ({
...ut,
[server.name]: { ...ut[server.name], transferred: serverTransferred },
}));
console.log(newBlob);
const dim = fileDimensions[file.name];
const fed: FileEventData = {
content: file.name,
x: newBlob.sha256,
url: newBlob.url,
size: `${newBlob.size}`,
};
if (newBlob.type) {
fed.m = newBlob.type;
}
if (dim) {
fed.dim = `${dim.width}x${dim.height}`;
}
setFileEventsToPublish(fetp => [...fetp, fed]);
}
queryClient.invalidateQueries({ queryKey: ['blobs', server.name] });
setFiles([]);
// TODO reset input control value??
}
setFiles([]);
// TODO reset input control value??
setFileEventsToPublish(Object.values(fileDimensions));
}
};
@ -176,6 +177,23 @@ function Upload() {
useEffect(() => {
clearTransfers();
/*
setFileEventsToPublish([
{
content: '_DSF3852.jpg',
dim: '1365x2048',
m: 'image/jpeg',
size: 599988,
url: [
'https://test-store.slidestr.net/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
'https://media-server.slidestr.net/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
'https://cdn.satellite.earth/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659.jpg',
],
x: 'd32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
},
]);*/
}, [servers]);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -198,7 +216,6 @@ function Upload() {
};
const sizeOfFilesToUpload = useMemo(() => files.reduce((acc, file) => (acc += file.size), 0), [files]);
return (
<>
<h2 className=" py-4">Upload</h2>
@ -219,14 +236,16 @@ function Upload() {
<CheckBox
name={s.name}
checked={transfers[s.name]?.enabled || false}
setChecked={c => setTransfers(ut => ({ ...ut, [s.name]: { enabled: c, transferred: 0, size: 0 } }))}
setChecked={c =>
setTransfers(ut => ({ ...ut, [s.name]: { enabled: c, transferred: 0, size: 0, rate: 0 } }))
}
label={s.name}
></CheckBox>
{transfers[s.name]?.enabled ? (
<ProgressBar
value={transfers[s.name].transferred}
max={transfers[s.name].size}
description={transferSpeed ? '' + formatFileSize(transferSpeed) + '/s' : ''}
description={transfers[s.name].rate > 0 ? '' + formatFileSize(transfers[s.name].rate) + '/s' : ''}
/>
) : (
<div></div>
@ -276,7 +295,7 @@ function Upload() {
</div>
{fileEventsToPublish.length > 0 && (
<>
<h2>Publish events</h2>
<h2 className="py-4">Publish events</h2>
{fileEventsToPublish.map(fe => (
<FileEventEditor data={fe} />
))}

40
src/useEvents.ts Normal file
View File

@ -0,0 +1,40 @@
import { useState, useEffect, useMemo } from 'react';
import { NDKEvent, NDKFilter, NDKRelaySet, NDKSubscriptionOptions } from '@nostr-dev-kit/ndk';
import uniqBy from 'lodash/uniqBy';
import { useNDK } from './ndk';
import { sha256 } from '@noble/hashes/sha256';
export interface SubscriptionOptions extends NDKSubscriptionOptions {
disable?: boolean;
}
export default function useEvents(filter: NDKFilter | NDKFilter[], opts?: SubscriptionOptions, relays?: string[]) {
const { ndk } = useNDK();
const [eose, setEose] = useState(false);
const [events, setEvents] = useState<NDKEvent[]>([]);
const id = useMemo(() => {
return sha256(new TextEncoder().encode(JSON.stringify(filter)));
}, [filter]);
useEffect(() => {
if (filter && !opts?.disable) {
const relaySet = relays?.length ?? 0 > 0 ? NDKRelaySet.fromRelayUrls(relays as string[], ndk) : undefined;
const sub = ndk.subscribe(filter, opts, relaySet);
sub.on('event', (ev: NDKEvent) => {
setEvents(evs => {
const newEvents = evs.concat([ev]).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
return uniqBy(newEvents, (e: NDKEvent) => e.tagId());
});
});
sub.on('eose', () => {
setEose(true);
});
return () => {
sub.stop();
};
}
}, [id, opts?.disable]);
return { id, eose, events };
}

View File

@ -0,0 +1,42 @@
import { useMemo } from 'react';
import useEvents from '../useEvents';
import groupBy from 'lodash/groupBy';
import { NDKFilter } from '@nostr-dev-kit/ndk';
import { useNDK } from '../ndk';
import { mapValues } from 'lodash';
export const KIND_FILE_META = 1063;
export const KIND_BLOSSOM_DRIVE = 30563;
const useFileMetaEventsByHash = () => {
const { user } = useNDK();
const fileMetaFilter = useMemo(
() => ({ kinds: [KIND_FILE_META, KIND_BLOSSOM_DRIVE], authors: [user?.pubkey] }) as NDKFilter,
[user?.pubkey]
);
const fileMetaSub = useEvents(fileMetaFilter);
/*
const fileMetaEventsByHash = useMemo(() => {
const allXTags = fileMetaSub.events.flatMap(ev => ev.tags.filter(t => t[0]=='x').flatMap(t => ({x:t[1], ev})));
console.log(allXTags);
return groupBy(allXTags, item => item.x)
}, [fileMetaSub.events]);
*/
const fileMetaEventsByHash = useMemo(
() => {
const allXTags = fileMetaSub.events.flatMap(ev => ev.tags.filter(t => t[0]=='x').flatMap(t => ({x:t[1], ev})));
const groupedByX= groupBy(allXTags, item => item.x);
return mapValues(groupedByX, v => v.map(e => e.ev));
},
[fileMetaSub]
);
console.log(fileMetaEventsByHash)
return fileMetaEventsByHash;
};
export default useFileMetaEventsByHash;