feat: Added thumbnails from DVM (disabled for now)

This commit is contained in:
florian 2024-05-02 14:33:45 +02:00
parent 9e34f686de
commit 40c3dfa907
19 changed files with 170 additions and 97 deletions

3
.gitignore vendored
View File

@ -23,3 +23,6 @@ dist-ssr
*.sln
*.sw?
.vercel
package-lock.json
bun.lockb

View File

@ -22,10 +22,10 @@
"add": "^2.0.6",
"axios": "^1.6.8",
"blossom-client-sdk": "^0.4.0",
"dayjs": "^1.11.10",
"dayjs": "^1.11.11",
"id3js": "^2.1.1",
"lodash": "^4.17.21",
"nostr-tools": "^2.5.0",
"nostr-tools": "^2.5.1",
"p-limit": "^5.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -9,7 +9,7 @@ import {
TrashIcon,
} from '@heroicons/react/24/outline';
import { BlobDescriptor } from 'blossom-client-sdk';
import { formatDate, formatFileSize } from '../../utils';
import { formatDate, formatFileSize } from '../../utils/utils';
import './BlobList.css';
import { useEffect, useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
@ -138,7 +138,7 @@ const BlobList = ({ blobs, onDelete, title, className ='' }: BlobListProps) => {
} as EventPointer);
return (
<a target="_blank" href={`https://filestr.vercel.app/e/${nevent}`}>
<div className="badge badge-primary mr-2">published</div>
<div className="badge badge-primary mr-2">filemeta</div>
</a>
);
}

View File

@ -1,9 +1,10 @@
import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import { useNDK } from '../../ndk';
import { NDKEvent, NDKKind, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
import { useNDK } from '../../utils/ndk';
import dayjs from 'dayjs';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import uniq from 'lodash/uniq';
import { formatFileSize } from '../../utils';
import { formatFileSize } from '../../utils/utils';
import useEvents from '../../utils/useEvents';
export type FileEventData = {
content: string;
@ -12,14 +13,57 @@ export type FileEventData = {
x: string;
m?: string;
size: number;
thumbnails?: string[];
thumbnail?: string;
//summary: string;
//alt: string;
};
const ensureDecrypted = async (dvm: NDKUser, event: NDKEvent) => {
if (!event) return undefined;
const encrypted = event.tags.some(t => t[0] == 'encrypted');
if (encrypted) {
const decryptedContent = await event.ndk?.signer?.decrypt(dvm, event.content);
if (decryptedContent) {
return {
...event,
tags: event.tags.filter(t => t[0] !== 'encrypted').concat(JSON.parse(decryptedContent)),
};
}
}
return event;
};
const FileEventEditor = ({ data }: { data: FileEventData }) => {
const [fileEventData, setFileEventData] = useState(data);
const [thumbnailRequestEventId, setThumbnailRequestEventId] = useState<string | undefined>();
const { ndk, user } = useNDK();
const dvm = ndk.getUser({ npub: 'npub1q8cv87l47fql2xer2uyw509y5n5s9f53h76hvf9377efdptmsvusxf3n8s' });
const thumbnailDvmFilter = useMemo(
() => ({ kinds: [6204 as NDKKind], '#e': [thumbnailRequestEventId || ''] }),
[thumbnailRequestEventId]
);
const thumbnailSubscription = useEvents(thumbnailDvmFilter, {
closeOnEose: false,
disable: thumbnailRequestEventId == undefined,
});
useEffect(() => {
const doASync = async () => {
const firstEvent = await ensureDecrypted(dvm, thumbnailSubscription.events[0]);
if (firstEvent) {
const urls = firstEvent.tags.filter(t => t[0] === 'thumb').map(t => t[1]);
const dim = firstEvent.tags.find(t => t[0] === 'dim')?.[1];
setFileEventData(ed => ({ ...ed, thumbnails: urls, dim, thumbnail: urls[0] }));
}
};
doASync();
}, [thumbnailSubscription.events]);
const publishFileEvent = async (data: FileEventData) => {
const e: NostrEvent = {
created_at: dayjs().unix(),
@ -43,6 +87,10 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
if (data.m) {
e.tags.push(['m', data.m]);
}
if (data.thumbnail) {
e.tags.push(['thumb', data.thumbnail]);
e.tags.push(['image', data.thumbnail]);
}
const ev = new NDKEvent(ndk, e);
await ev.sign();
@ -50,18 +98,88 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
// await ev.publish();
};
const getThumbnails = async (data: FileEventData) => {
if (!ndk.signer) return;
const e: NostrEvent = {
created_at: dayjs().unix(),
content: await ndk.signer?.encrypt(
dvm,
JSON.stringify([
['i', data.url[0], 'url'],
['output', 'image/jpeg'],
['param', 'thumbnailCount', '3'],
['param', 'imageFormat', 'jpg'],
['relays', user?.relayUrls.join(',') || ndk.explicitRelayUrls?.join(',') || ''],
])
),
tags: [['p', dvm.pubkey], ['encrypted']],
/*tags: [
['i', data.url[0], 'url'],
['output', 'image/jpeg'],
['param', 'thumbnailCount', '5'],
['param', 'imageFormat', 'jpg'],
['relays', user?.relayUrls.join(',') || ndk.explicitRelayUrls?.join(',') || ''],
],*/
kind: 5204,
pubkey: user?.pubkey || '',
};
const ev = new NDKEvent(ndk, e);
await ev.sign();
console.log(ev.rawEvent());
setThumbnailRequestEventId(ev.id);
await ev.publish();
};
useEffect(() => {
if (fileEventData.m?.startsWith('video/') && fileEventData.thumbnails == undefined) {
// getThumbnails(fileEventData); skip for now, until the DVM is properly hosted
}
}, [fileEventData]);
return (
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
{fileEventData.m?.startsWith('video/') && (
<>
{thumbnailRequestEventId &&
(fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? (
<div className='w-2/6'>
<div className="carousel w-full">
{fileEventData.thumbnails.map((t, i) => (
<div id={`item${i + 1}`} key={`item${i + 1}`} className="carousel-item w-full">
<img src={t} className="w-full" />
</div>
))}
</div>
<div className="flex justify-center w-full py-2 gap-2">
{fileEventData.thumbnails.map((t, i) => (
<a
key={`link${i + 1}`}
href={`#item${i + 1}`}
onClick={() => setFileEventData(ed => ({ ...ed, thumbnail: t }))}
className={'btn btn-xs ' + (t==fileEventData.thumbnail ? 'btn-primary':'')}
>{`${i + 1}`}</a>
))}
</div>
</div>
) : (
<div>
Creating previews <span className="loading loading-spinner loading-md"></span>
</div>
))}
</>
)}
{fileEventData.m?.startsWith('image/') && (
<div className="p-4 bg-base-300">
<div className="p-4 bg-base-300 w-2/6">
<img
width={200}
height={200}
width={300}
height={300}
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' }}>
<div className="grid gap-4 w-4/6" style={{ gridTemplateColumns: '1fr 30em' }}>
<span className="font-bold">Type</span>
<span>{fileEventData.m}</span>
@ -82,7 +200,7 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
placeholder="Caption"
></textarea>
<span className="font-bold">URL</span>
<textarea value={fileEventData.url.join('\n')} className="textarea" placeholder="URL" />
<div className=''>{fileEventData.url.map((text,i) => <div key={i} className='break-words mb-2'>{text}</div>)}</div>
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
Publish
</button>

View File

@ -1,5 +1,5 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { useNDK } from '../../ndk';
import { useNDK } from '../../utils/ndk';
import './Layout.css';
import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
import { useEffect } from 'react';

View File

@ -11,7 +11,7 @@ import {
} from '@heroicons/react/24/outline';
import { Server as ServerType } from '../../utils/useUserServers';
import { ServerInfo } from '../../utils/useServerInfo';
import { formatDate, formatFileSize } from '../../utils';
import { formatDate, formatFileSize } from '../../utils/utils';
type ServerProps = {
server: ServerType;

View File

@ -1,47 +0,0 @@
/* Source: https://stackoverflow.com/a/77472484/47324 */
const cleanBuffer = (arrayBuffer: ArrayBuffer) => {
let dataView = new DataView(arrayBuffer);
const exifMarker = 0xffe1;
let offset = 2; // Skip the first two bytes (0xFFD8)
while (offset < dataView.byteLength) {
if (dataView.getUint16(offset) === exifMarker) {
// Found an EXIF marker
const segmentLength = dataView.getUint16(offset + 2, false) + 2;
// Update the arrayBuffer and dataView
arrayBuffer = removeSegment(arrayBuffer, offset, segmentLength);
dataView = new DataView(arrayBuffer);
} else {
// Move to the next marker
offset += 2 + dataView.getUint16(offset + 2, false);
}
}
return arrayBuffer;
};
const removeSegment = (buffer: ArrayBuffer, offset: number, length: number) => {
// Create a new buffer without the specified segment
const modifiedBuffer = new Uint8Array(buffer.byteLength - length);
modifiedBuffer.set(new Uint8Array(buffer.slice(0, offset)), 0);
modifiedBuffer.set(new Uint8Array(buffer.slice(offset + length)), offset);
return modifiedBuffer.buffer;
};
export const removeExifData = (file: File): Promise<File> => {
return new Promise(resolve => {
if (file && file.type.startsWith('image/')) {
const fr = new FileReader();
fr.onload = function (this: FileReader) {
const cleanedBuffer = cleanBuffer(this.result as ArrayBuffer);
const blob = new Blob([cleanedBuffer], { type: file.type });
const newFile = new File([blob], file.name, { type: file.type });
resolve(newFile);
};
fr.readAsArrayBuffer(file);
} else resolve(file);
});
};

View File

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { NDKContextProvider } from './ndk.tsx';
import { NDKContextProvider } from './utils/ndk.tsx';
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom';
import { Layout } from './components/Layout/Layout.tsx';
import Home from './pages/Home.tsx';

View File

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import './Home.css';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useNDK } from '../utils/ndk';
import BlobList from '../components/BlobList/BlobList';
import { useServerInfo } from '../utils/useServerInfo';
import { ServerList } from '../components/ServerList/ServerList';

View File

@ -9,9 +9,9 @@ import { ServerList } from '../components/ServerList/ServerList';
import { useServerInfo } from '../utils/useServerInfo';
import { useMemo, useState } from 'react';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useNDK } from '../utils/ndk';
import { useQueryClient } from '@tanstack/react-query';
import { formatFileSize } from '../utils';
import { formatFileSize } from '../utils/utils';
import BlobList from '../components/BlobList/BlobList';
import './Transfer.css';
import { useNavigate, useParams } from 'react-router-dom';

View File

@ -1,18 +1,17 @@
import { ChangeEvent, DragEvent, useEffect, useMemo, useRef, useState } from 'react';
import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useNDK } from '../utils/ndk';
import { useServerInfo } from '../utils/useServerInfo';
import { useQueryClient } from '@tanstack/react-query';
import { removeExifData } from '../exif';
import { removeExifData } from '../utils/exif';
import axios, { AxiosProgressEvent } from 'axios';
import { ArrowUpOnSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import CheckBox from '../components/CheckBox/CheckBox';
import ProgressBar from '../components/ProgressBar/ProgressBar';
import { formatFileSize } from '../utils';
import { formatFileSize } from '../utils/utils';
import FileEventEditor, { FileEventData } from '../components/FileEventEditor/FileEventEditor';
import pLimit from 'p-limit';
import { Server, useUserServers } from '../utils/useUserServers';
import useBlossomServerEvents from '../utils/useBlossomServerEvents';
type TransferStats = {
enabled: boolean;
@ -44,10 +43,11 @@ function Upload() {
const [cleanPrivateData, setCleanPrivateData] = useState(true);
const limit = pLimit(3);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const bs = useBlossomServerEvents();
// const bs = useBlossomServerEvents();
// console.log(bs);
const [fileEventsToPublish, setFileEventsToPublish] = useState<FileEventData[]>([]);
const [uploadBusy, setUploadBusy] = useState(false);
console.log(bs);
// const [resizeImages, setResizeImages] = useState(false);
// const [publishToNostr, setPublishToNostr] = useState(false);
@ -105,7 +105,7 @@ function Upload() {
// for image resizing
const fileDimensions: { [key: string]: FileEventData } = {};
for (const file of filesToUpload) {
let data = { content: file.name, url: [] as string[] } as FileEventData;
let data = { content: file.name.replace(/\.[a-zA-Z0-9]{3,4}$/,''), url: [] as string[] } as FileEventData;
if (file.type.startsWith('image/')) {
const dimensions = await getImageSize(file);
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };

View File

@ -17,8 +17,15 @@ type NDKContextType = {
publishSignedEvent: (signedEvent: SignedEvent) => Promise<void>;
};
const cacheAdapter = new NDKCacheAdapterDexie({ dbName: 'ndk-cache-2' });
const ndk = new NDK({
explicitRelayUrls: ['wss://nostrue.com/', 'wss://relay.damus.io/', 'wss://nos.lol/'],
cacheAdapter,
});
export const NDKContext = createContext<NDKContextType>({
ndk: new NDK({ explicitRelayUrls: [] }),
ndk,
logout: () => {},
loginWithExtension: () => Promise.reject(),
loginWithNostrAddress: () => Promise.reject(),
@ -26,12 +33,6 @@ export const NDKContext = createContext<NDKContextType>({
signEventTemplate: () => Promise.reject(),
publishSignedEvent: () => Promise.reject(),
});
const cacheAdapter = new NDKCacheAdapterDexie({ dbName: 'ndk-cache' });
const ndk = new NDK({
explicitRelayUrls: ['wss://nostrue.com/', 'wss://relay.damus.io/', 'wss://nos.lol/'],
cacheAdapter,
});
export const NDKContextProvider = ({ children }: { children: React.ReactElement }) => {
const [user, setUser] = useState(ndk.activeUser);

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react';
import useEvents from '../useEvents';
import useEvents from '../utils/useEvents';
import { NDKKind } from '@nostr-dev-kit/ndk';
import countBy from 'lodash/countBy';
import sortBy from 'lodash/sortBy';
@ -12,7 +12,7 @@ const useBlossomServerEvents = () => {
const blossomServers = useMemo(() => {
const allRTags = blossomServerEvents.events.flatMap(ev =>
ev.tags.filter(t => t[0] == 'r').flatMap(t => ({ name: t[1] }))
ev.tags.filter(t => t[0] == 'r' || t[0] == 'server').flatMap(t => ({ name: t[1] })) // TODO 'r' is deprecated
);
const cnt = countBy(
allRTags.filter(s => !s.name.match(/https?:\/\/localhost/)),

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import useEvents from '../useEvents';
import useEvents from '../utils/useEvents';
import groupBy from 'lodash/groupBy';
import { NDKFilter } from '@nostr-dev-kit/ndk';
import { useNDK } from '../ndk';
import { useNDK } from '../utils/ndk';
import { mapValues } from 'lodash';
export const KIND_FILE_META = 1063;

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useQueries } from '@tanstack/react-query';
import { BlobDescriptor, BlossomClient } from 'blossom-client-sdk';
import { useNDK } from '../ndk';
import { useNDK } from '../utils/ndk';
import { nip19 } from 'nostr-tools';
import { useUserServers } from './useUserServers';

View File

@ -1,15 +1,11 @@
import { useMemo } from 'react';
import { uniqAndSort } from '../utils';
import { useNDK } from '../ndk';
import { uniqAndSort } from '../utils/utils';
import { useNDK } from '../utils/ndk';
import { nip19 } from 'nostr-tools';
import { NDKKind } from '@nostr-dev-kit/ndk';
import useEvent from '../useEvent';
import useEvent from '../utils/useEvent';
const additionalServers = [
//'https://media-server.slidestr.net',
//'https://cdn.hzrd149.com',
'https://cdn.satellite.earth',
];
const additionalServers = ['https://cdn.satellite.earth'];
export type Server = {
name: string;
@ -25,9 +21,11 @@ export const useUserServers = (): Server[] => {
const servers = useMemo(() => {
const serverUrls = uniqAndSort(
[...(serverListEvent?.getMatchingTags('r').map(t => t[1]) || []), ...additionalServers].map(s =>
s.toLocaleLowerCase().replace(/\/$/, '')
)
[
...(serverListEvent?.getMatchingTags('r').map(t => t[1]) || []), // TODO 'r' is deprecated
...(serverListEvent?.getMatchingTags('server').map(t => t[1]) || []),
...additionalServers,
].map(s => s.toLocaleLowerCase().replace(/\/$/, ''))
);
return serverUrls.map(s => ({
name: s.replace(/https?:\/\//, ''),