diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index b2c116d..511b1a2 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -5,11 +5,14 @@ import { kinds, validateEvent, verifyEvent, - Event + Event, + EventTemplate } from 'nostr-tools' import { ProfileMetadata, RelaySet } from '../types' import { NostrController } from '.' import { toast } from 'react-toastify' +import { queryNip05 } from '../utils' +import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' export class MetadataController { private nostrController: NostrController @@ -163,5 +166,107 @@ export class MetadataController { }) } + public getNostrJoiningBlockNumber = async (hexKey: string) => { + const relaySet = await this.findRelayListMetadata(hexKey) + + const relays: string[] = [] + + if (relaySet.write.length > 0) { + relays.push(...relaySet.write) + } else { + const metadata = await this.findMetadata(hexKey) + const metadataContent = this.extractProfileMetadataContent(metadata) + + if (metadataContent?.nip05) { + const nip05Profile = await queryNip05(metadataContent.nip05) + + if (nip05Profile && nip05Profile.pubkey === hexKey) { + relays.push(...nip05Profile.relays) + } + } + } + + if (relays.length === 0) return null + + const eventFilter: Filter = { + kinds: [kinds.ShortTextNote], + authors: [hexKey] + } + + const pool = new SimplePool() + const events = await pool.querySync(relays, eventFilter) + if (events && events.length) { + events.sort((a, b) => a.created_at - b.created_at) + + const event = events[0] + const { created_at } = event + + const jobEventTemplate: EventTemplate = { + content: '', + created_at: Math.round(Date.now() / 1000), + kind: 68001, + tags: [ + ['i', `${created_at * 1000}`], + ['j', 'blockChain-block-number'] + ] + } + + const jobSignedEvent = await this.nostrController.signEvent( + jobEventTemplate + ) + + const relays = [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://relayable.org' + ] + + await this.nostrController.publishEvent(jobSignedEvent, relays) + + console.log('jobSignedEvent :>> ', jobSignedEvent) + + const subscribeWithTimeout = ( + subscription: NDKSubscription, + timeoutMs: number + ): Promise => { + return new Promise((resolve, reject) => { + const eventHandler = (event: NDKEvent) => { + subscription.stop() + resolve(event.content) + } + + subscription.on('event', eventHandler) + + // Set up a timeout to stop the subscription after a specified time + const timeout = setTimeout(() => { + subscription.stop() // Stop the subscription + reject(new Error('Subscription timed out')) // Reject the promise with a timeout error + }, timeoutMs) + + // Handle subscription close event + subscription.on('close', () => clearTimeout(timeout)) + }) + } + + const dvmNDK = new NDK({ + explicitRelayUrls: relays + }) + + await dvmNDK.connect(2000) + + const sub = dvmNDK.subscribe({ + kinds: [68002 as number], + '#e': [jobSignedEvent.id], + '#p': [jobSignedEvent.pubkey] + }) + + const dvmJobResult = await subscribeWithTimeout(sub, 10000) + + return parseInt(dvmJobResult) + } + + return null + } + public validate = (event: Event) => validateEvent(event) && verifyEvent(event) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index e3cb803..257419e 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,5 +1,12 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import { List, ListItem, ListSubheader, TextField } from '@mui/material' +import { + List, + ListItem, + ListSubheader, + TextField, + Typography, + useTheme +} from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' @@ -17,6 +24,8 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' export const ProfilePage = () => { + const theme = useTheme() + const { npub } = useParams() const dispatch: Dispatch = useDispatch() @@ -25,6 +34,7 @@ export const ProfilePage = () => { const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() + const [blockNumber, setBlockNumber] = useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const metadataState = useSelector((state: State) => state.metadata) @@ -50,6 +60,18 @@ export const ProfilePage = () => { }, [npub, usersPubkey]) useEffect(() => { + if (pubkey) { + metadataController + .getNostrJoiningBlockNumber(pubkey) + .then((res) => { + setBlockNumber(res) + }) + .catch((err) => { + // todo: handle error + console.log('err :>> ', err) + }) + } + if (isUsersOwnProfile && metadataState) { const metadataContent = metadataController.extractProfileMetadataContent( metadataState as VerifiedEvent @@ -206,8 +228,7 @@ export const ProfilePage = () => { sx={{ marginTop: 1, display: 'flex', - flexDirection: 'column', - gap: 2 + flexDirection: 'column' }} > { src={profileMetadata.picture || placeholderAvatar} alt='Profile Image' /> + {blockNumber && ( + + On nostr since {blockNumber.toLocaleString()} + + )} {editItem('name', 'Username')}