mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-18 01:13:00 +01:00
feat: Added thumbnails from DVM (disabled for now)
This commit is contained in:
parent
9e34f686de
commit
40c3dfa907
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.vercel
|
||||
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
47
src/exif.ts
47
src/exif.ts
@ -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);
|
||||
});
|
||||
};
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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}` };
|
||||
|
@ -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);
|
@ -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/)),
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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?:\/\//, ''),
|
||||
|
Loading…
x
Reference in New Issue
Block a user