diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 94f5899..301f21d 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -14,71 +14,130 @@ import { NostrController } from '.' import { toast } from 'react-toastify' import { queryNip05 } from '../utils' import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' +import { EventEmitter } from 'tseep' +import { localCache } from '../services' -export class MetadataController { +export class MetadataController extends EventEmitter { private nostrController: NostrController private specialMetadataRelay = 'wss://purplepag.es' constructor() { + super() this.nostrController = NostrController.getInstance() } - public getEmptyMetadataEvent = (): Event => { - return { - content: '', - created_at: new Date().valueOf(), - id: '', - kind: 0, - pubkey: '', - sig: '', - tags: [] - } - } - - public findMetadata = async (hexKey: string) => { + /** + * 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 { + // Define the event filter to only include metadata events authored by the given key const eventFilter: Filter = { - kinds: [kinds.Metadata], - authors: [hexKey] + kinds: [kinds.Metadata], // Only metadata events + authors: [hexKey] // Authored by the specified key } const pool = new SimplePool() + // Try to get the metadata event from a special relay (wss://purplepag.es) const metadataEvent = await pool .get([this.specialMetadataRelay], eventFilter) .catch((err) => { - console.error(err) - return null + console.error(err) // Log any errors + return null // Return null if an error occurs }) + // If a valid metadata event is found from the special relay if ( metadataEvent && - validateEvent(metadataEvent) && - verifyEvent(metadataEvent) + validateEvent(metadataEvent) && // Validate the event + verifyEvent(metadataEvent) // Verify the event's authenticity ) { - return metadataEvent + // If there's no current event or the new metadata event is more recent + if (!currentEvent || metadataEvent.created_at > currentEvent.created_at) { + // Handle the new metadata event + this.handleNewMetadataEvent(metadataEvent) + return metadataEvent + } } + // If no valid metadata event is found from the special relay, get the most popular relays const mostPopularRelays = await this.nostrController.getMostPopularRelays() + // Query the most popular relays for metadata events const events = await pool .querySync(mostPopularRelays, eventFilter) .catch((err) => { - console.error(err) - - return null + console.error(err) // Log any errors + return null // Return null if an error occurs }) + // If events are found from the popular relays if (events && events.length) { - events.sort((a, b) => b.created_at - a.created_at) + events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending) + // Iterate through the events for (const event of events) { - if (validateEvent(event) && verifyEvent(event)) { + // If the event is valid, authentic, and more recent than the current event + if ( + validateEvent(event) && + verifyEvent(event) && + (!currentEvent || event.created_at > currentEvent.created_at) + ) { + // Handle the new metadata event + this.handleNewMetadataEvent(event) return event } } } - throw new Error('Mo metadata found.') + return currentEvent // Return the current event if no newer event is found + } + + /** + * 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) { + const oneDayInMS = 24 * 60 * 60 * 1000 // Number of milliseconds in one day + + // Check if the cached metadata is older than one day + if (Date.now() - cachedMetadataEvent.cachedAt > oneDayInMS) { + // If older than one day, 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) } public findRelayListMetadata = async (hexKey: string) => { @@ -142,7 +201,7 @@ export class MetadataController { throw new Error('No relay list metadata found.') } - public extractProfileMetadataContent = (event: VerifiedEvent) => { + public extractProfileMetadataContent = (event: Event) => { try { if (!event.content) return {} return JSON.parse(event.content) as ProfileMetadata @@ -175,6 +234,7 @@ export class MetadataController { .publishEvent(signedMetadataEvent, [this.specialMetadataRelay]) .then((relays) => { toast.success(`Metadata event published on: ${relays.join('\n')}`) + this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent) }) .catch((err) => { toast.error(err.message) @@ -193,6 +253,8 @@ export class MetadataController { userRelays.push(...relaySet.write) } else { const metadata = await this.findMetadata(hexKey) + if (!metadata) return null + const metadataContent = this.extractProfileMetadataContent(metadata) if (metadataContent?.nip05) { @@ -306,4 +368,16 @@ export class MetadataController { } public validate = (event: Event) => validateEvent(event) && verifyEvent(event) + + public getEmptyMetadataEvent = (): Event => { + return { + content: '', + created_at: new Date().valueOf(), + id: '', + kind: 0, + pubkey: '', + sig: '', + tags: [] + } + } } diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 9c4f4e4..c63e74d 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -18,8 +18,7 @@ import { MetadataController, NostrController } from '../controllers' import { LoginMethods } from '../store/auth/types' import { setUserRobotImage } from '../store/userRobotImage/action' import { State } from '../store/rootReducer' - -const metadataController = new MetadataController() +import { Event, kinds } from 'nostr-tools' export const MainLayout = () => { const dispatch: Dispatch = useDispatch() @@ -27,6 +26,8 @@ export const MainLayout = () => { const authState = useSelector((state: State) => state.auth) useEffect(() => { + const metadataController = new MetadataController() + const logout = () => { dispatch( setAuthState({ @@ -68,6 +69,20 @@ export const MainLayout = () => { nostrController.createNsecBunkerSigner(usersPubkey) }) } + + const handleMetadataEvent = (event: Event) => { + dispatch(setMetadataEvent(event)) + } + + metadataController.on(usersPubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + + metadataController.findMetadata(usersPubkey).then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(metadataEvent) + }) } } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 4289525..8b498f6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -49,6 +49,7 @@ import type { Identifier, XYCoord } from 'dnd-core' import { useDrag, useDrop } from 'react-dnd' import saveAs from 'file-saver' import CopyModal from '../../components/copyModal' +import { Event, kinds } from 'nostr-tools' export const CreatePage = () => { const navigate = useNavigate() @@ -517,16 +518,26 @@ const DisplayUser = ({ if (!(user.pubkey in metadata)) { const metadataController = new MetadataController() + 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) => { - const metadataContent = - metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [user.pubkey]: metadataContent - })) + if (metadataEvent) handleMetadataEvent(metadataEvent) }) .catch((err) => { console.error( diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index bc08206..e3c7fe4 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -2,7 +2,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import EditIcon from '@mui/icons-material/Edit' import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material' import { truncate } from 'lodash' -import { VerifiedEvent, nip19 } from 'nostr-tools' +import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { useSelector } from 'react-redux' import { Link, useNavigate, useParams } from 'react-router-dom' @@ -75,6 +75,20 @@ export const ProfilePage = () => { if (pubkey) { const getMetadata = async (pubkey: string) => { + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + if (metadataContent) { + setProfileMetadata(metadataContent) + } + } + + metadataController.on(pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + const metadataEvent = await metadataController .findMetadata(pubkey) .catch((err) => { @@ -82,13 +96,7 @@ export const ProfilePage = () => { return null }) - if (metadataEvent) { - const metadataContent = - metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) { - setProfileMetadata(metadataContent) - } - } + if (metadataEvent) handleMetadataEvent(metadataEvent) setIsLoading(false) } diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx index 9b68872..bdb8c69 100644 --- a/src/pages/settings/profile/index.tsx +++ b/src/pages/settings/profile/index.tsx @@ -11,7 +11,7 @@ import { Typography, useTheme } from '@mui/material' -import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' +import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' @@ -95,6 +95,20 @@ export const ProfileSettingsPage = () => { if (pubkey) { const getMetadata = async (pubkey: string) => { + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + if (metadataContent) { + setProfileMetadata(metadataContent) + } + } + + metadataController.on(pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + const metadataEvent = await metadataController .findMetadata(pubkey) .catch((err) => { @@ -102,13 +116,7 @@ export const ProfileSettingsPage = () => { return null }) - if (metadataEvent) { - const metadataContent = - metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) { - setProfileMetadata(metadataContent) - } - } + if (metadataEvent) handleMetadataEvent(metadataEvent) setIsLoading(false) } diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 269264f..3581947 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -20,7 +20,7 @@ import saveAs from 'file-saver' import JSZip from 'jszip' import _ from 'lodash' import { MuiFileInput } from 'mui-file-input' -import { Event, verifyEvent } from 'nostr-tools' +import { Event, kinds, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { useNavigate, useSearchParams } from 'react-router-dom' @@ -846,17 +846,27 @@ const DisplayMeta = ({ 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) => { - const metadataContent = - metadataController.extractProfileMetadataContent(metadataEvent) - - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [key]: metadataContent - })) + if (metadataEvent) handleMetadataEvent(metadataEvent) }) .catch((err) => { console.error(`error occurred in finding metadata for: ${key}`, err) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index e981252..a8e0d1f 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -10,7 +10,7 @@ import { } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { Event, verifyEvent } from 'nostr-tools' +import { Event, kinds, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' @@ -107,16 +107,26 @@ export const VerifyPage = () => { const pubkey = npubToHex(user)! if (!(pubkey in metadata)) { + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + if (metadataContent) + setMetadata((prev) => ({ + ...prev, + [pubkey]: metadataContent + })) + } + + metadataController.on(pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + metadataController .findMetadata(pubkey) .then((metadataEvent) => { - const metadataContent = - metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [pubkey]: metadataContent - })) + if (metadataEvent) handleMetadataEvent(metadataEvent) }) .catch((err) => { console.error(