diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 09c7d8f..73384c5 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -9,7 +9,7 @@ import { } from '@mui/material' import styles from './style.module.scss' import React, { useEffect, useState } from 'react' -import { ProfileMetadata, User, UserRole, KeyboardCode } from '../../types' +import { User, UserRole, KeyboardCode } from '../../types' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { hexToNpub, npubToHex, getProfileUsername } from '../../utils' import { SigitFile } from '../../utils/file' @@ -21,6 +21,7 @@ import { useScale } from '../../hooks/useScale' import { AvatarIconButton } from '../UserAvatarIconButton' import { UserAvatar } from '../UserAvatar' import _ from 'lodash' +import { NDKUserProfile } from '@nostr-dev-kit/ndk' const DEFAULT_START_SIZE = { width: 140, @@ -33,7 +34,7 @@ interface HideSignersForDrawnField { interface Props { users: User[] - metadata: { [key: string]: ProfileMetadata } + userProfiles: { [key: string]: NDKUserProfile } sigitFiles: SigitFile[] setSigitFiles: React.Dispatch> selectedTool?: DrawTool @@ -563,10 +564,11 @@ export const DrawPDFFields = (props: Props) => { > {signers.map((signer, index) => { const npub = hexToNpub(signer.pubkey) - const metadata = props.metadata[signer.pubkey] + const profile = + props.userProfiles[signer.pubkey] const displayValue = getProfileUsername( npub, - metadata + profile ) // make current signers dropdown visible if ( @@ -585,7 +587,7 @@ export const DrawPDFFields = (props: Props) => { { const signer = signers.find((u) => u.pubkey === npubToHex(npub)) if (signer) { - const metadata = props.metadata[signer.pubkey] - displayValue = getProfileUsername(npub, metadata) + const profile = props.userProfiles[signer.pubkey] + displayValue = getProfileUsername(npub, profile) return (
{ const profile = useProfileMetadata(pubkey) const name = getProfileUsername(pubkey, profile) - const image = profile?.picture + const image = profile?.image return ( Promise - findMetadata: (pubkey: string) => Promise + findMetadata: ( + pubkey: string, + opts?: NDKSubscriptionOptions, + storeProfileEvent?: boolean + ) => Promise publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise } @@ -191,16 +196,22 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { * @returns A promise that resolves to the metadata event. */ const findMetadata = async ( - pubkey: string + pubkey: string, + opts?: NDKSubscriptionOptions, + storeProfileEvent?: boolean ): Promise => { const npub = hexToNpub(pubkey) const user = new NDKUser({ npub }) user.ndk = ndk - return await user.fetchProfile({ - cacheUsage: NDKSubscriptionCacheUsage.PARALLEL - }) + return await user.fetchProfile( + { + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, + ...(opts || {}) + }, + storeProfileEvent + ) } const publish = async ( diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 984afd3..1f7cf8e 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -1,35 +1,19 @@ -import { - Event, - Filter, - VerifiedEvent, - kinds, - validateEvent, - verifyEvent -} from 'nostr-tools' -import { toast } from 'react-toastify' +import { Event } from 'nostr-tools' import { EventEmitter } from 'tseep' -import { NostrController, relayController } from '.' -import { localCache } from '../services' import { ProfileMetadata, RelaySet } from '../types' import { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelaySet, - getUserRelaySet, - isOlderThanOneDay, - unixNow + getUserRelaySet } from '../utils' import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const' export class MetadataController extends EventEmitter { private static instance: MetadataController - private nostrController: NostrController - private specialMetadataRelay = 'wss://purplepag.es' - private pendingFetches = new Map>() // Track pending fetches constructor() { super() - this.nostrController = NostrController.getInstance() } public static getInstance(): MetadataController { @@ -39,105 +23,6 @@ export class MetadataController extends EventEmitter { return MetadataController.instance } - /** - * Asynchronously checks for more recent metadata events authored by a specific key. - * If a more recent metadata event is found, it is handled and returned. - * If no more recent event is found, the current event is returned. - * @param hexKey The hexadecimal key of the author to filter metadata events. - * @param currentEvent The current metadata event, if any, to compare with newer events. - * @returns A promise resolving to the most recent metadata event found, or null if none is found. - */ - private async checkForMoreRecentMetadata( - hexKey: string, - currentEvent: Event | null - ): Promise { - // Return the ongoing fetch promise if one exists for the same hexKey - if (this.pendingFetches.has(hexKey)) { - return this.pendingFetches.get(hexKey)! - } - - // Define the event filter to only include metadata events authored by the given key - const eventFilter: Filter = { - kinds: [kinds.Metadata], - authors: [hexKey] - } - - const fetchPromise = relayController - .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) - .catch((err) => { - console.error(err) - return null - }) - .finally(() => { - this.pendingFetches.delete(hexKey) - }) - - this.pendingFetches.set(hexKey, fetchPromise) - - const metadataEvent = await fetchPromise - - if ( - metadataEvent && - validateEvent(metadataEvent) && - verifyEvent(metadataEvent) - ) { - if ( - !currentEvent || - metadataEvent.created_at >= currentEvent.created_at - ) { - this.handleNewMetadataEvent(metadataEvent) - } - return metadataEvent - } - - // todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST - // try to query user relay list - - // if current event is null we should cache empty metadata event for provided hexKey - if (!currentEvent) { - const emptyMetadata = this.getEmptyMetadataEvent(hexKey) - this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent) - } - - return currentEvent - } - - /** - * Handle new metadata events and emit them to subscribers - */ - private async handleNewMetadataEvent(event: VerifiedEvent) { - // update the event in local cache - localCache.addUserMetadata(event) - // Emit the event to subscribers. - this.emit(event.pubkey, event.kind, event) - } - - /** - * Finds metadata for a given hexadecimal key. - * - * @param hexKey - The hexadecimal key to search for metadata. - * @returns A promise that resolves to the metadata event. - */ - public findMetadata = async (hexKey: string): Promise => { - // Attempt to retrieve the metadata event from the local cache - const cachedMetadataEvent = await localCache.getUserMetadata(hexKey) - - // If cached metadata is found, check its validity - if (cachedMetadataEvent) { - // Check if the cached metadata is older than one day - if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) { - // If older than one week, find the metadata from relays in background - this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) - } - - // Return the cached metadata event - return cachedMetadataEvent.event - } - - // If no cached metadata is found, retrieve it from relays - return this.checkForMoreRecentMetadata(hexKey, null) - } - /** * Based on the hexKey of the current user, this method attempts to retrieve a relay set. * @func findRelayListInCache first checks if there is already an up-to-date @@ -168,52 +53,4 @@ export class MetadataController extends EventEmitter { return null } } - - /** - * Function will not sign provided event if the SIG exists - */ - public publishMetadataEvent = async (event: Event) => { - let signedMetadataEvent = event - - if (event.sig.length < 1) { - const timestamp = unixNow() - - // Metadata event to publish to the wss://purplepag.es relay - const newMetadataEvent: Event = { - ...event, - created_at: timestamp - } - - signedMetadataEvent = - await this.nostrController.signEvent(newMetadataEvent) - } - - await relayController - .publish(signedMetadataEvent, [this.specialMetadataRelay]) - .then((relays) => { - if (relays.length) { - toast.success(`Metadata event published on: ${relays.join('\n')}`) - this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent) - } else { - toast.error('Could not publish metadata event to any relay!') - } - }) - .catch((err) => { - toast.error(err.message) - }) - } - - public validate = (event: Event) => validateEvent(event) && verifyEvent(event) - - public getEmptyMetadataEvent = (pubkey?: string): Event => { - return { - content: '', - created_at: new Date().valueOf(), - id: '', - kind: 0, - pubkey: pubkey || '', - sig: '', - tags: [] - } - } } diff --git a/src/hooks/useProfileMetadata.tsx b/src/hooks/useProfileMetadata.tsx index f746f0d..9532bb3 100644 --- a/src/hooks/useProfileMetadata.tsx +++ b/src/hooks/useProfileMetadata.tsx @@ -1,33 +1,18 @@ import { useEffect, useState } from 'react' -import { ProfileMetadata } from '../types/profile' -import { MetadataController } from '../controllers/MetadataController' -import { Event, kinds } from 'nostr-tools' + +import { NDKUserProfile } from '@nostr-dev-kit/ndk' +import { useNDKContext } from './useNDKContext' export const useProfileMetadata = (pubkey: string) => { - const [profileMetadata, setProfileMetadata] = useState() + const { findMetadata } = useNDKContext() + + const [userProfile, setUserProfile] = useState() useEffect(() => { - const metadataController = MetadataController.getInstance() - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) { - setProfileMetadata(metadataContent) - } - } - if (pubkey) { - metadataController.on(pubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(pubkey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) + findMetadata(pubkey) + .then((profile) => { + if (profile) setUserProfile(profile) }) .catch((err) => { console.error( @@ -36,11 +21,7 @@ export const useProfileMetadata = (pubkey: string) => { ) }) } + }, [pubkey, findMetadata]) - return () => { - metadataController.off(pubkey, handleMetadataEvent) - } - }, [pubkey]) - - return profileMetadata + return userProfile } diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 2738bcb..b373bf5 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Outlet, useNavigate, useSearchParams } from 'react-router-dom' -import { Event, getPublicKey, kinds, nip19 } from 'nostr-tools' +import { Event, getPublicKey, nip19 } from 'nostr-tools' import { init as initNostrLogin } from 'nostr-login' import { NostrLoginAuthOptions } from 'nostr-login/dist/types' @@ -9,9 +9,15 @@ import { NostrLoginAuthOptions } from 'nostr-login/dist/types' import { AppBar } from '../components/AppBar/AppBar' import { LoadingSpinner } from '../components/LoadingSpinner' -import { MetadataController, NostrController } from '../controllers' +import { NostrController } from '../controllers' -import { useAppDispatch, useAppSelector, useAuth, useLogout } from '../hooks' +import { + useAppDispatch, + useAppSelector, + useAuth, + useLogout, + useNDKContext +} from '../hooks' import { restoreState, @@ -38,6 +44,7 @@ export const MainLayout = () => { const navigate = useNavigate() const dispatch = useAppDispatch() const logout = useLogout() + const { findMetadata } = useNDKContext() const { authAndGetMetadataAndRelaysMap } = useAuth() const [isLoading, setIsLoading] = useState(true) @@ -143,8 +150,6 @@ export const MainLayout = () => { }, [dispatch]) useEffect(() => { - const metadataController = MetadataController.getInstance() - const restoredState = loadState() if (restoredState) { dispatch(restoreState(restoredState)) @@ -154,20 +159,19 @@ export const MainLayout = () => { if (loggedIn) { if (!loginMethod || !usersPubkey) return logout() - // Update user profile metadata, old state might be outdated - const handleMetadataEvent = (event: Event) => { - dispatch(setMetadataEvent(event)) - } - - metadataController.on(usersPubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) + findMetadata(usersPubkey, {}, true).then((profile) => { + if (profile && profile.profileEvent) { + try { + const event: Event = JSON.parse(profile.profileEvent) + dispatch(setMetadataEvent(event)) + } catch (error) { + console.error( + 'An error occurred in parsing profile event from profile obj', + error + ) + } } }) - - metadataController.findMetadata(usersPubkey).then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) } else { setIsLoading(false) } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index a65a559..e861545 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -10,7 +10,7 @@ import { import type { Identifier, XYCoord } from 'dnd-core' import saveAs from 'file-saver' import JSZip from 'jszip' -import { Event, kinds } from 'nostr-tools' +import { Event } from 'nostr-tools' import { useCallback, useEffect, useRef, useState } from 'react' import { DndProvider, useDrag, useDrop } from 'react-dnd' import { MultiBackend } from 'react-dnd-multi-backend' @@ -30,7 +30,6 @@ import { CreateSignatureEventContent, KeyboardCode, Meta, - ProfileMetadata, SignedEvent, User, UserRole @@ -80,12 +79,16 @@ import { Autocomplete } from '@mui/lab' import _, { truncate } from 'lodash' import * as React from 'react' import { AvatarIconButton } from '../../components/UserAvatarIconButton' +import { NDKUserProfile } from '@nostr-dev-kit/ndk' +import { useNDKContext } from '../../hooks/useNDKContext.ts' type FoundUser = Event & { npub: string } export const CreatePage = () => { const navigate = useNavigate() const location = useLocation() + const { findMetadata } = useNDKContext() + const { uploadedFiles } = location.state || {} const [currentFile, setCurrentFile] = useState() const isActive = (file: File) => file.name === currentFile?.name @@ -117,9 +120,10 @@ export const CreatePage = () => { const nostrController = NostrController.getInstance() - const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) + const [userProfiles, setUserProfiles] = useState<{ + [key: string]: NDKUserProfile + }>({}) + const [drawnFiles, setDrawnFiles] = useState([]) const [parsingPdf, setIsParsing] = useState(false) @@ -280,29 +284,15 @@ export const CreatePage = () => { useEffect(() => { users.forEach((user) => { - if (!(user.pubkey in metadata)) { - const metadataController = MetadataController.getInstance() - - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [user.pubkey]: metadataContent - })) - } - - metadataController.on(user.pubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(user.pubkey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) + if (!(user.pubkey in userProfiles)) { + findMetadata(user.pubkey) + .then((profile) => { + if (profile) { + setUserProfiles((prev) => ({ + ...prev, + [user.pubkey]: profile + })) + } }) .catch((err) => { console.error( @@ -312,7 +302,7 @@ export const CreatePage = () => { }) } }) - }, [metadata, users]) + }, [userProfiles, users, findMetadata]) useEffect(() => { if (uploadedFiles) { @@ -1204,7 +1194,7 @@ export const CreatePage = () => { ) : ( ( - {} - ) const [users, setUsers] = useState([]) useEffect(() => { @@ -104,45 +100,6 @@ export const DisplayMeta = ({ }) }, [signers, viewers]) - useEffect(() => { - const metadataController = MetadataController.getInstance() - - const hexKeys: string[] = [ - npubToHex(submittedBy)!, - ...users.map((user) => user.pubkey) - ] - - hexKeys.forEach((key) => { - if (!(key in metadata)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - - metadataController.on(key, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(key) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) - .catch((err) => { - console.error(`error occurred in finding metadata for: ${key}`, err) - }) - } - }) - }, [users, submittedBy, metadata]) - const downloadFile = async (fileName: string) => { const file = files[fileName] saveAs(file) @@ -229,7 +186,6 @@ export const DisplayMeta = ({ key={user.pubkey} meta={meta} user={user} - metadata={metadata} signedBy={signedBy} nextSigner={nextSigner} getPrevSignersSig={getPrevSignersSig} @@ -258,7 +214,6 @@ enum UserStatus { type DisplayUserProps = { meta: Meta user: User - metadata: { [key: string]: ProfileMetadata } signedBy: `npub1${string}`[] nextSigner?: string getPrevSignersSig: (usersNpub: string) => string | null