From e9ff853b81bc8851988096206ca767f2da9843cd Mon Sep 17 00:00:00 2001 From: florian <> Date: Sat, 27 Jul 2024 19:51:38 +0200 Subject: [PATCH] chore: more upload improvements --- package.json | 38 +- src/components/BlobList/BlobList.tsx | 16 +- .../FileEventEditor/FileEventEditor.tsx | 536 +++++++++--------- src/components/FileEventEditor/dvm.ts | 4 +- .../FileEventEditor/usePublishing.ts | 20 +- src/components/Layout/Layout.tsx | 4 +- src/components/MimeTypeIcon.tsx | 12 + src/components/ServerList/Server.tsx | 12 +- src/components/ServerList/ServerList.css | 12 +- src/components/UploadFileSelection.tsx | 10 +- src/main.tsx | 5 +- src/pages/Transfer.tsx | 2 +- src/pages/Upload.tsx | 159 +++++- src/utils/blossom.ts | 7 +- src/utils/utils.ts | 6 + 15 files changed, 496 insertions(+), 347 deletions(-) create mode 100644 src/components/MimeTypeIcon.tsx diff --git a/package.json b/package.json index 2b691c8..f64d7a1 100644 --- a/package.json +++ b/package.json @@ -12,47 +12,47 @@ "analyze": "vite-bundle-visualizer" }, "dependencies": { - "@heroicons/react": "^2.1.4", + "@heroicons/react": "^2.1.5", "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "^2.8.2", "@nostr-dev-kit/ndk-cache-dexie": "^2.4.2", - "@tanstack/react-query": "^5.50.1", - "@tanstack/react-query-devtools": "^5.50.1", + "@tanstack/react-query": "^5.51.15", + "@tanstack/react-query-devtools": "^5.51.15", "add": "^2.0.6", "axios": "^1.7.2", "blossom-client-sdk": "^0.9.0", "blurhash": "^2.0.5", - "dayjs": "^1.11.11", + "dayjs": "^1.11.12", "id3js": "^2.1.1", "lodash": "^4.17.21", - "nostr-tools": "^2.7.0", - "p-limit": "^6.0.0", + "nostr-tools": "^2.7.1", + "p-limit": "^6.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-pdf": "^9.1.0", - "react-router-dom": "^6.24.1" + "react-router-dom": "^6.25.1" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.50.1", - "@types/lodash": "^4.17.6", + "@tanstack/eslint-plugin-query": "^5.51.15", + "@types/lodash": "^4.17.7", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.15.0", - "@typescript-eslint/parser": "^7.15.0", + "@typescript-eslint/eslint-plugin": "^7.17.0", + "@typescript-eslint/parser": "^7.17.0", "@vitejs/plugin-react-swc": "^3.7.0", "autoprefixer": "^10.4.19", "daisyui": "latest", - "eslint": "^8.56.0", + "eslint": "^9.8.0", "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "postcss": "^8.4.39", - "prettier": "^3.3.2", - "tailwindcss": "^3.4.4", - "typescript": "^5.5.3", - "vite": "^5.3.3", + "eslint-plugin-react-refresh": "^0.4.9", + "postcss": "^8.4.40", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4", + "vite": "^5.3.5", "vite-bundle-visualizer": "^1.2.1" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "4.18.0" + "@rollup/rollup-linux-x64-gnu": "4.19.1" } } diff --git a/src/components/BlobList/BlobList.tsx b/src/components/BlobList/BlobList.tsx index f6550eb..91679af 100644 --- a/src/components/BlobList/BlobList.tsx +++ b/src/components/BlobList/BlobList.tsx @@ -2,13 +2,9 @@ import { useState, useMemo } from 'react'; import { BlobDescriptor } from 'blossom-client-sdk'; import { ClipboardDocumentIcon, - DocumentIcon, ExclamationTriangleIcon, - FilmIcon, FolderIcon, FolderPlusIcon, - MusicalNoteIcon, - PhotoIcon, PlusIcon, TrashIcon, XMarkIcon, @@ -24,6 +20,7 @@ import BlobListTypeMenu, { ListMode } from './BlobListTypeMenu'; import useFileMetaEventsByHash from '../../utils/useFileMetaEvents'; import './BlobList.css'; import { useBlobSelection } from './useBlobSelection'; +import MimeTypeIcon from '../MimeTypeIcon'; type BlobListProps = { blobs: BlobDescriptor[]; @@ -75,15 +72,6 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => ); - const getMimeTypeIcon = (type: string | undefined) => { - if (!type) return ; - if (type.startsWith('image/')) return ; - if (type.startsWith('video/')) return ; - if (type.startsWith('audio/')) return ; - if (type === 'application/pdf') return ; - return ; - }; - const Badges = ({ blob }: { blob: BlobDescriptor }) => { const events = fileMetaEventsByHash[blob.sha256]; if (!events) return null; @@ -208,7 +196,7 @@ const BlobList = ({ blobs, onDelete, title, className = '' }: BlobListProps) => onChange={e => handleSelectBlob(blob.sha256, e)} onClick={e => e.stopPropagation()} /> - {getMimeTypeIcon(blob.type)} + diff --git a/src/components/FileEventEditor/FileEventEditor.tsx b/src/components/FileEventEditor/FileEventEditor.tsx index 2563b66..6871e6b 100644 --- a/src/components/FileEventEditor/FileEventEditor.tsx +++ b/src/components/FileEventEditor/FileEventEditor.tsx @@ -1,14 +1,12 @@ -import { useEffect, useState } from 'react'; -import { formatFileSize } from '../../utils/utils'; +import { useEffect } from 'react'; +import { extractDomain, formatFileSize } from '../../utils/utils'; import { fetchId3Tag } from '../../utils/id3'; import useVideoThumbnailDvm from './dvm'; -import { usePublishing } from './usePublishing'; -import { BlobDescriptor } from 'blossom-client-sdk'; -import { transferBlob } from '../../utils/transfer'; -import { useNDK } from '../../utils/ndk'; import TagInput from '../TagInput'; import { allGenres } from '../../utils/genres'; -import { useServerInfo } from '../../utils/useServerInfo'; +import { ServerIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline'; +import MimeTypeIcon from '../MimeTypeIcon'; +import { NostrEvent } from '@nostr-dev-kit/ndk'; export type FileEventData = { originalFile: File; @@ -21,6 +19,7 @@ export type FileEventData = { m?: string; size: number; thumbnails?: string[]; + selectedThumbnail?: string; publishedThumbnail?: string; blurHash?: string; tags: string[]; @@ -32,17 +31,32 @@ export type FileEventData = { year?: string; genre?: string; subgenre?: string; + + publish: { + file?: boolean; + audio?: boolean; + video?: boolean; + }; + events: NostrEvent[]; }; -const FileEventEditor = ({ data }: { data: FileEventData }) => { - const { signEventTemplate } = useNDK(); - const { serverInfo } = useServerInfo(); - const [fileEventData, setFileEventData] = useState(data); - const [selectedThumbnail, setSelectedThumbnail] = useState(); +const copyToClipboard = (text: string) => { + navigator.clipboard + .writeText(text) + .then(() => {}) + .catch(err => { + console.error('Failed to copy: ', err); + }); +}; - const { createDvmThumbnailRequest, thumbnailRequestEventId } = useVideoThumbnailDvm(setFileEventData); - const { publishAudioEvent, publishFileEvent, publishVideoEvent } = usePublishing(); - const [jsonOutput, setJsonOutput] = useState(''); +const FileEventEditor = ({ + fileEventData, + setFileEventData, +}: { + fileEventData: FileEventData; + setFileEventData: (fe: FileEventData) => void; +}) => { + const { createDvmThumbnailRequest, thumbnailRequestEventId } = useVideoThumbnailDvm(fileEventData, setFileEventData); const isAudio = fileEventData.m?.startsWith('audio/'); const isVideo = fileEventData.m?.startsWith('video/'); @@ -74,292 +88,260 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => { title: id3.title, year: id3.year, thumbnails: res.coverFull ? [res.coverFull] : [], + selectedThumbnail: res.coverFull, }); - setSelectedThumbnail(res.coverFull); }); } }, [fileEventData]); - function extractDomain(url: string): string | null { - const regex = /^(https?:\/\/)([^/]+)/; - const match = url.match(regex); - return match ? match[2]?.toLocaleLowerCase() : null; - } - - const publishSelectedThumbnailToAllOwnServers = async (): Promise => { - // TODO investigate why mimetype is not set for reuploaded thumbnail (on mediaserver) - const servers = fileEventData.url.map(extractDomain); - - // upload selected thumbnail to the same blossom servers as the video - let uploadedThumbnails: BlobDescriptor[] = []; - if (selectedThumbnail) { - uploadedThumbnails = ( - await Promise.all( - servers.map(s => { - if (s && selectedThumbnail) { - console.log(s); - console.log(serverInfo); - return transferBlob(selectedThumbnail, serverInfo[s], signEventTemplate); - } - }) - ) - ).filter(t => t !== undefined) as BlobDescriptor[]; - - return uploadedThumbnails.length > 0 ? uploadedThumbnails[0] : undefined; // TODO do we need multiple thumbsnails?? or server URLs? - } - }; - useEffect(() => { - if (selectedThumbnail == undefined) { + if (fileEventData.selectedThumbnail == undefined) { if (fileEventData.thumbnails && fileEventData.thumbnails?.length > 0) { - setSelectedThumbnail(fileEventData.thumbnails[0]); + setFileEventData({ + ...fileEventData, + selectedThumbnail: fileEventData.thumbnails[0], + }); } } - }, [fileEventData.thumbnails, selectedThumbnail]); - // TODO add tags editor + }, [fileEventData.thumbnails, fileEventData.selectedThumbnail]); + return ( - <> -
- {fileEventData.m?.startsWith('video/') && ( +
+
+
+ {fileEventData.publish.file !== undefined && ( +
+ +
+ )} + {fileEventData.publish.video !== undefined && ( +
+ +
+ )} + {fileEventData.publish.audio !== undefined && ( +
+ +
+ )} +
+
+ {fileEventData.m?.startsWith('video/') && ( + <> + {thumbnailRequestEventId && + (fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? ( +
+ ) : ( +
+ Creating previews +
+ ))} + + )} + {isAudio && (fileEventData.publishedThumbnail || fileEventData.selectedThumbnail) && ( +
+ +
+ )} + + {fileEventData.m?.startsWith('image/') && ( +
+ +
+ )} +
+ {(isAudio || isVideo) && ( <> - {thumbnailRequestEventId && - (fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? ( -
-
- {fileEventData.thumbnails.map((t, i) => ( -
- -
- ))} -
- -
- ) : ( -
- Creating previews -
- ))} + Title + setFileEventData({ ...fileEventData, title: e.target.value })} + > )} - {isAudio && (fileEventData.publishedThumbnail || selectedThumbnail) && ( -
- -
+ {isAudio && ( + <> + Artist + {fileEventData.artist} + + )} + {isAudio && ( + <> + Album + {fileEventData.album} + + )} + {isAudio && ( + <> + Year + {fileEventData.year} + )} - {fileEventData.m?.startsWith('image/') && ( -
- -
- )} -
- {(isAudio || isVideo) && ( - <> - Title - setFileEventData(ed => ({ ...ed, title: e.target.value }))} - > - - )} - {isAudio && ( - <> - Artist - {fileEventData.artist} - - )} - {isAudio && ( - <> - Album - {fileEventData.album} - - )} - {isAudio && ( - <> - Year - {fileEventData.year} - - )} - - Summary / Description - - {isAudio && ( - <> - Genre -
- + {isAudio && ( + <> + Genre +
+ + - -
- - )} - Tags - setFileEventData(ed => ({ ...ed, tags }))} - > + +
+ + )} + Tags + setFileEventData({ ...fileEventData, tags })} + > - Type - {fileEventData.m} + Type + + {fileEventData.m} + - {fileEventData.dim && ( - <> - Dimensions - {fileEventData.dim} - - )} + {fileEventData.dim && ( + <> + Dimensions + {fileEventData.dim} + + )} - File size - {fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'} - URL -
- {fileEventData.url.map((text, i) => ( -
- {text} -
- ))} -
+ File size + {fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'} + URLs + -
{' '} -
-
- DEVELOPMENT ZONE! These publish buttons do not work yet. Events are only shown in the browser console. -
-
- - - -
-
{jsonOutput}
- +
); }; diff --git a/src/components/FileEventEditor/dvm.ts b/src/components/FileEventEditor/dvm.ts index db662b0..a5a3a5c 100644 --- a/src/components/FileEventEditor/dvm.ts +++ b/src/components/FileEventEditor/dvm.ts @@ -46,7 +46,7 @@ const ensureDecrypted = async (dvm: NDKUser, event: NDKEvent): Promise>) => { +const useVideoThumbnailDvm = (fileEventData: FileEventData, setFileEventData: (data: FileEventData) => void) => { const [thumbnailRequestEventId, setThumbnailRequestEventId] = useState(); const { ndk, user } = useNDK(); const dvm = ndk.getUser({ npub: NPUB_DVM_THUMBNAIL_CREATION }); @@ -68,7 +68,7 @@ const useVideoThumbnailDvm = (setFileEventData: React.Dispatch t[0] === 'thumb').map(t => t[1]); const dim = firstEvent.tags.find(t => t[0] === 'dim')?.[1]; const duration = firstEvent.tags.find(t => t[0] === 'duration')?.[1]; - setFileEventData(ed => ({ ...ed, thumbnails: urls, dim, duration })); + setFileEventData({ ...fileEventData, thumbnails: urls, dim, duration }); } }; doASync(); diff --git a/src/components/FileEventEditor/usePublishing.ts b/src/components/FileEventEditor/usePublishing.ts index 0f3b5c7..88ffa12 100644 --- a/src/components/FileEventEditor/usePublishing.ts +++ b/src/components/FileEventEditor/usePublishing.ts @@ -8,7 +8,7 @@ import { KIND_AUDIO, KIND_FILE_META, KIND_VIDEO_HORIZONTAL, KIND_VIDEO_VERTICAL export const usePublishing = () => { const { ndk, user } = useNDK(); - const publishFileEvent = async (data: FileEventData): Promise => { + const publishFileEvent = async (data: FileEventData): Promise => { const e: NostrEvent = { created_at: dayjs().unix(), content: data.content, @@ -45,11 +45,11 @@ export const usePublishing = () => { const ev = new NDKEvent(ndk, e); await ev.sign(); console.log(ev.rawEvent()); - // await ev.publish(); - return JSON.stringify(ev.rawEvent(), null, 2); + //await ev.publish(); + return ev.rawEvent(); }; - const publishAudioEvent = async (data: FileEventData): Promise => { + const publishAudioEvent = async (data: FileEventData): Promise => { const e: NostrEvent = { created_at: dayjs().unix(), content: `${data.artist} - ${data.title}`, @@ -88,16 +88,16 @@ export const usePublishing = () => { } } - // published_at + e.tags.push(['published_at', `${dayjs().unix()}`]); const ev = new NDKEvent(ndk, e); await ev.sign(); console.log(ev.rawEvent()); - // await ev.publish(); - return JSON.stringify(ev.rawEvent(), null, 2); + //await ev.publish(); + return ev.rawEvent(); }; - const publishVideoEvent = async (data: FileEventData): Promise => { + const publishVideoEvent = async (data: FileEventData): Promise => { const videoIsHorizontal = data.width == undefined || data.height == undefined || data.width > data.height; const e: NostrEvent = { @@ -138,8 +138,8 @@ export const usePublishing = () => { const ev = new NDKEvent(ndk, e); await ev.sign(); console.log(ev.rawEvent()); - // await ev.publish(); - return JSON.stringify(ev.rawEvent(), null, 2); + //await ev.publish(); + return ev.rawEvent(); }; return { diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 8e98b1a..0cbb96a 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -26,7 +26,7 @@ export const Layout = () => { > Upload -
+ {publishCount > 0 && ( + + )} + + + )} + {uploadStep == 3 && ( + <> +
Published events
+
+ {fileEventsToPublish.map(fe => + fe.events.map(ev =>
{JSON.stringify(ev, null, 2)}
) + )} + ({ + url: fe.url[0], + type: fe.m, + sha256: fe.x, + size: fe.size, + uploaded: 0, + }))} + > +
)} diff --git a/src/utils/blossom.ts b/src/utils/blossom.ts index 7c4194e..ae5b9f1 100644 --- a/src/utils/blossom.ts +++ b/src/utils/blossom.ts @@ -53,7 +53,10 @@ export const uploadBlossomBlob = async ( return res.data; }; -export const downloadBlossomBlob = async (url: string, onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void) => { +export const downloadBlossomBlob = async ( + url: string, + onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void +) => { const response = await axios.get(url, { responseType: 'blob', onDownloadProgress, @@ -89,4 +92,4 @@ export const mirrordBlossomBlob = async ( } ); return res.data; -}; \ No newline at end of file +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index af4a42e..d460fa1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -44,3 +44,9 @@ export const formatDate = (unixTimeStamp: number): string => { if (ts == 0) return 'never'; return dayjs(ts * 1000).format('YYYY-MM-DD'); }; + +export function extractDomain(url: string): string | null { + const regex = /^(https?:\/\/)([^/]+)/; + const match = url.match(regex); + return match ? match[2]?.toLocaleLowerCase() : null; +}