feat: Improved upload and image post editor
This commit is contained in:
parent
5b399482ed
commit
caf95a2eeb
62
README.md
62
README.md
@ -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
|
Audio
|
||||||
export default {
|
Audio Event 31337, maybe Podcast 31338
|
||||||
// other rules...
|
Album Event 30029
|
||||||
parserOptions: {
|
Read ID3 INfo
|
||||||
ecmaVersion: 'latest',
|
Display in Metadata Editor
|
||||||
sourceType: 'module',
|
Post Audio Events
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
Option to select "Full Album" and POst Album (Playlist) Event
|
||||||
tsconfigRootDir: __dirname,
|
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
|
|
||||||
|
107
package-lock.json
generated
107
package-lock.json
generated
@ -20,7 +20,9 @@
|
|||||||
"blossom-client-sdk": "^0.4.0",
|
"blossom-client-sdk": "^0.4.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"id3js": "^2.1.1",
|
"id3js": "^2.1.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^2.4.0",
|
"nostr-tools": "^2.4.0",
|
||||||
|
"p-limit": "^5.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-pdf": "^7.7.1",
|
"react-pdf": "^7.7.1",
|
||||||
@ -28,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/eslint-plugin-query": "^5.28.6",
|
"@tanstack/eslint-plugin-query": "^5.28.6",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/react": "^18.2.74",
|
"@types/react": "^18.2.74",
|
||||||
"@types/react-dom": "^18.2.24",
|
"@types/react-dom": "^18.2.24",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
@ -1667,6 +1670,12 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
@ -3787,6 +3796,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -4246,6 +4260,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-limit": {
|
"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",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
@ -4260,14 +4303,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-locate": {
|
"node_modules/p-locate/node_modules/yocto-queue": {
|
||||||
"version": "5.0.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"p-limit": "^3.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@ -5847,12 +5887,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=12.20"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
@ -6809,6 +6848,12 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"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": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
@ -8340,6 +8385,11 @@
|
|||||||
"p-locate": "^5.0.0"
|
"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": {
|
"lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -8649,12 +8699,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"yocto-queue": "^0.1.0"
|
"yocto-queue": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-locate": {
|
"p-locate": {
|
||||||
@ -8664,6 +8713,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"p-limit": "^3.0.2"
|
"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": {
|
"parent-module": {
|
||||||
@ -9711,10 +9777,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"yocto-queue": {
|
"yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g=="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,9 @@
|
|||||||
"blossom-client-sdk": "^0.4.0",
|
"blossom-client-sdk": "^0.4.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"id3js": "^2.1.1",
|
"id3js": "^2.1.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^2.4.0",
|
"nostr-tools": "^2.4.0",
|
||||||
|
"p-limit": "^5.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-pdf": "^7.7.1",
|
"react-pdf": "^7.7.1",
|
||||||
@ -32,6 +34,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/eslint-plugin-query": "^5.28.6",
|
"@tanstack/eslint-plugin-query": "^5.28.6",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/react": "^18.2.74",
|
"@types/react": "^18.2.74",
|
||||||
"@types/react-dom": "^18.2.24",
|
"@types/react-dom": "^18.2.24",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
@ -3,16 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blob-list {
|
.blob-list {
|
||||||
@apply bg-base-200 p-4 text-neutral-content rounded-lg;
|
@apply 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog-list-header {
|
.blog-list-header {
|
||||||
@ -23,18 +14,10 @@
|
|||||||
@apply flex-grow;
|
@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 {
|
.blog-list-header svg {
|
||||||
@apply w-6 opacity-80 hover:opacity-100;
|
@apply w-6 opacity-80 hover:opacity-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-list .blob span a.pill {
|
.blob-list .table :where(th, td) {
|
||||||
@apply bg-base-200 p-1 px-2 rounded-2xl text-white;
|
padding: .25em;
|
||||||
}
|
}
|
@ -17,6 +17,10 @@ import * as id3 from 'id3js';
|
|||||||
import { ID3Tag, ID3TagV2 } from 'id3js/lib/id3Tag';
|
import { ID3Tag, ID3TagV2 } from 'id3js/lib/id3Tag';
|
||||||
import { useQueries } from '@tanstack/react-query';
|
import { useQueries } from '@tanstack/react-query';
|
||||||
import { useServerInfo } from '../../utils/useServerInfo';
|
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';
|
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 BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
|
||||||
const [mode, setMode] = useState<ListMode>('list');
|
const [mode, setMode] = useState<ListMode>('list');
|
||||||
const { distribution } = useServerInfo();
|
const { distribution } = useServerInfo();
|
||||||
|
const fileMetaEventsByHash = useFileMetaEventsByHash();
|
||||||
|
|
||||||
const images = useMemo(
|
const images = useMemo(
|
||||||
() => blobs.filter(b => b.type?.startsWith('image/')).sort((a, b) => (a.created > b.created ? -1 : 1)), // descending
|
() => 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>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`blog-list-header ${!title ? 'justify-end' : ''}`}>
|
<div className={`blog-list-header ${!title ? 'justify-end' : ''}`}>
|
||||||
@ -293,34 +338,42 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
|
|||||||
|
|
||||||
{mode == 'list' && (
|
{mode == 'list' && (
|
||||||
<div className="blob-list">
|
<div className="blob-list">
|
||||||
|
<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) => (
|
{blobs.map((blob: BlobDescriptor) => (
|
||||||
<div className="blob" key={blob.sha256}>
|
<tr className="hover" key={blob.sha256}>
|
||||||
<span>
|
<td className=" whitespace-nowrap">
|
||||||
<DocumentIcon />
|
<DocumentIcon />
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<a className="link link-primary" href={blob.url} target="_blank">
|
<a className="link link-primary" href={blob.url} target="_blank">
|
||||||
{blob.sha256}
|
{blob.sha256.slice(0, 15)}
|
||||||
</a>
|
</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>
|
</span>
|
||||||
{/*
|
</td>
|
||||||
<span>
|
<td>{formatFileSize(blob.size)}</td>
|
||||||
<a className="pill">🌸 drive</a> <a className="pill">📝 post</a>
|
<td>{blob.type && `${blob.type}`}</td>
|
||||||
</span>
|
<td>{formatDate(blob.created)}</td>
|
||||||
*/}
|
<td className=" whitespace-nowrap">
|
||||||
<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>
|
<Actions blob={blob}></Actions>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -2,14 +2,16 @@ import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
|
|||||||
import { useNDK } from '../../ndk';
|
import { useNDK } from '../../ndk';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import { formatFileSize } from '../../utils';
|
||||||
|
|
||||||
export type FileEventData = {
|
export type FileEventData = {
|
||||||
content: string;
|
content: string;
|
||||||
url: string;
|
url: string[];
|
||||||
dim?: string;
|
dim?: string;
|
||||||
x: string;
|
x: string;
|
||||||
m?: string;
|
m?: string;
|
||||||
size?: string;
|
size: number;
|
||||||
//summary: string;
|
//summary: string;
|
||||||
//alt: string;
|
//alt: string;
|
||||||
};
|
};
|
||||||
@ -23,8 +25,8 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
created_at: dayjs().unix(),
|
created_at: dayjs().unix(),
|
||||||
content: data.content,
|
content: data.content,
|
||||||
tags: [
|
tags: [
|
||||||
|
...uniq(data.url).map(du => ['url', du]),
|
||||||
['x', data.x],
|
['x', data.x],
|
||||||
['url', data.url],
|
|
||||||
//['summary', data.summary],
|
//['summary', data.summary],
|
||||||
//['alt', data.alt],
|
//['alt', data.alt],
|
||||||
],
|
],
|
||||||
@ -33,7 +35,7 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data.size) {
|
if (data.size) {
|
||||||
e.tags.push(['size', data.size]);
|
e.tags.push(['size', `${data.size}`]);
|
||||||
}
|
}
|
||||||
if (data.dim) {
|
if (data.dim) {
|
||||||
e.tags.push(['dim', data.dim]);
|
e.tags.push(['dim', data.dim]);
|
||||||
@ -45,21 +47,45 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
const ev = new NDKEvent(ndk, e);
|
const ev = new NDKEvent(ndk, e);
|
||||||
await ev.sign();
|
await ev.sign();
|
||||||
console.log(ev.rawEvent());
|
console.log(ev.rawEvent());
|
||||||
await ev.publish();
|
// await ev.publish();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
||||||
<pre>{JSON.stringify(fileEventData, null, 2)}</pre>
|
{fileEventData.m?.startsWith('image/') && (
|
||||||
<img src={`https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${fileEventData.url}`}></img>
|
<div className="p-4 bg-base-300">
|
||||||
{fileEventData.dim ? `(${fileEventData.dim})` : ''}
|
<img
|
||||||
<div className="flex flex-col gap-4">
|
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
|
<textarea
|
||||||
value={fileEventData.content}
|
value={fileEventData.content}
|
||||||
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
||||||
className="textarea textarea-secondary"
|
className="textarea"
|
||||||
placeholder="Caption"
|
placeholder="Caption"
|
||||||
></textarea>
|
></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)}>
|
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
|
||||||
Publish
|
Publish
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.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 {
|
.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 {
|
.title img {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const ProgressBar = ({ value, max, description = '' }: { value: number; max: number; description?: string }) => {
|
const ProgressBar = ({ value, max, description = '' }: { value: number; max: number; description?: string }) => {
|
||||||
const percent = Math.floor((value * 100) / max);
|
const percent = Math.floor((value * 100) / max);
|
||||||
const showDescription = percent > 10 && percent < 100;
|
const showDescription = description.length > 0;
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-base-200 rounded-lg">
|
<div className="w-full bg-base-200 rounded-lg">
|
||||||
{max !== undefined && value !== undefined && max > 0 && (
|
{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" />
|
<progress className="progress w-full accent-primary" value={percent} max="100" />
|
||||||
<span>{percent}%</span>
|
<span>{percent}%</span>
|
||||||
<span>{showDescription ? description : ''}</span>
|
<span className="whitespace-nowrap overflow-ellipsis">{showDescription ? description : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeEvent, DragEvent, useEffect, useMemo, useState } from 'react';
|
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 { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
|
||||||
import { useNDK } from '../ndk';
|
import { useNDK } from '../ndk';
|
||||||
import { useServerInfo } from '../utils/useServerInfo';
|
import { useServerInfo } from '../utils/useServerInfo';
|
||||||
@ -11,11 +11,13 @@ import CheckBox from '../components/CheckBox/CheckBox';
|
|||||||
import ProgressBar from '../components/ProgressBar/ProgressBar';
|
import ProgressBar from '../components/ProgressBar/ProgressBar';
|
||||||
import { formatFileSize } from '../utils';
|
import { formatFileSize } from '../utils';
|
||||||
import FileEventEditor, { FileEventData } from '../components/FileEventEditor/FileEventEditor';
|
import FileEventEditor, { FileEventData } from '../components/FileEventEditor/FileEventEditor';
|
||||||
|
import pLimit from 'p-limit';
|
||||||
|
|
||||||
type TransferStats = {
|
type TransferStats = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
transferred: number;
|
transferred: number;
|
||||||
|
rate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -41,7 +43,7 @@ function Upload() {
|
|||||||
const [transfers, setTransfers] = useState<{ [key: string]: TransferStats }>({});
|
const [transfers, setTransfers] = useState<{ [key: string]: TransferStats }>({});
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [cleanPrivateData, setCleanPrivateData] = useState(true);
|
const [cleanPrivateData, setCleanPrivateData] = useState(true);
|
||||||
const [transferSpeed, setTransferSpeed] = useState<number | undefined>();
|
const limit = pLimit(3);
|
||||||
|
|
||||||
const [fileEventsToPublish, setFileEventsToPublish] = useState<FileEventData[]>([]);
|
const [fileEventsToPublish, setFileEventsToPublish] = useState<FileEventData[]>([]);
|
||||||
|
|
||||||
@ -98,14 +100,50 @@ function Upload() {
|
|||||||
|
|
||||||
// TODO use https://github.com/davejm/client-compress
|
// TODO use https://github.com/davejm/client-compress
|
||||||
// for image resizing
|
// for image resizing
|
||||||
const fileDimensions: { [key: string]: ImageSize } = {};
|
const fileDimensions: { [key: string]: FileEventData } = {};
|
||||||
for (const file of filesToUpload) {
|
for (const file of filesToUpload) {
|
||||||
|
let data = { content: file.name, url: [] as string[] } as FileEventData;
|
||||||
if (file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
const dimensions = await getImageSize(file);
|
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) {
|
if (filesToUpload && filesToUpload.length) {
|
||||||
// sum files sizes
|
// sum files sizes
|
||||||
const totalSize = filesToUpload.reduce((acc, f) => acc + f.size, 0);
|
const totalSize = filesToUpload.reduce((acc, f) => acc + f.size, 0);
|
||||||
@ -121,51 +159,14 @@ function Upload() {
|
|||||||
return newTransfers;
|
return newTransfers;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const server of servers) {
|
const enabledServers = servers.filter(s => transfers[s.name]?.enabled);
|
||||||
if (!transfers[server.name]?.enabled) {
|
const primaryServerName = servers[0].name;
|
||||||
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 newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
|
await Promise.all(enabledServers.map(s => limit(() => startTransfer(s, s.name == primaryServerName))));
|
||||||
setTransferSpeed(progressEvent.rate);
|
|
||||||
setTransfers(ut => ({
|
|
||||||
...ut,
|
|
||||||
[server.name]: { ...ut[server.name], transferred: serverTransferred + progressEvent.loaded },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
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([]);
|
setFiles([]);
|
||||||
// TODO reset input control value??
|
// TODO reset input control value??
|
||||||
}
|
setFileEventsToPublish(Object.values(fileDimensions));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,6 +177,23 @@ function Upload() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearTransfers();
|
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]);
|
}, [servers]);
|
||||||
|
|
||||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -198,7 +216,6 @@ function Upload() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sizeOfFilesToUpload = useMemo(() => files.reduce((acc, file) => (acc += file.size), 0), [files]);
|
const sizeOfFilesToUpload = useMemo(() => files.reduce((acc, file) => (acc += file.size), 0), [files]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2 className=" py-4">Upload</h2>
|
<h2 className=" py-4">Upload</h2>
|
||||||
@ -219,14 +236,16 @@ function Upload() {
|
|||||||
<CheckBox
|
<CheckBox
|
||||||
name={s.name}
|
name={s.name}
|
||||||
checked={transfers[s.name]?.enabled || false}
|
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}
|
label={s.name}
|
||||||
></CheckBox>
|
></CheckBox>
|
||||||
{transfers[s.name]?.enabled ? (
|
{transfers[s.name]?.enabled ? (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={transfers[s.name].transferred}
|
value={transfers[s.name].transferred}
|
||||||
max={transfers[s.name].size}
|
max={transfers[s.name].size}
|
||||||
description={transferSpeed ? '' + formatFileSize(transferSpeed) + '/s' : ''}
|
description={transfers[s.name].rate > 0 ? '' + formatFileSize(transfers[s.name].rate) + '/s' : ''}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
@ -276,7 +295,7 @@ function Upload() {
|
|||||||
</div>
|
</div>
|
||||||
{fileEventsToPublish.length > 0 && (
|
{fileEventsToPublish.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h2>Publish events</h2>
|
<h2 className="py-4">Publish events</h2>
|
||||||
{fileEventsToPublish.map(fe => (
|
{fileEventsToPublish.map(fe => (
|
||||||
<FileEventEditor data={fe} />
|
<FileEventEditor data={fe} />
|
||||||
))}
|
))}
|
||||||
|
40
src/useEvents.ts
Normal file
40
src/useEvents.ts
Normal 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 };
|
||||||
|
}
|
42
src/utils/useFileMetaEvents.ts
Normal file
42
src/utils/useFileMetaEvents.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user