Merge pull request 'feat: show block number on user profile' (#18) from issue-1 into main
All checks were successful
Release / build_and_release (push) Successful in 1m5s

Reviewed-on: https://git.sigit.io/sig/it/pulls/18
Reviewed-by: Leonardo Davinci <m@noreply.git.sigit.io>
Reviewed-by: b <b@4j.cx>
This commit is contained in:
b 2024-05-10 10:58:14 +00:00
commit e479156270
3 changed files with 177 additions and 7 deletions

View File

@ -5,11 +5,15 @@ import {
kinds, kinds,
validateEvent, validateEvent,
verifyEvent, verifyEvent,
Event Event,
EventTemplate,
nip19
} from 'nostr-tools' } from 'nostr-tools'
import { ProfileMetadata, RelaySet } from '../types' import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
import { NostrController } from '.' import { NostrController } from '.'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { queryNip05 } from '../utils'
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
export class MetadataController { export class MetadataController {
private nostrController: NostrController private nostrController: NostrController
@ -163,5 +167,130 @@ export class MetadataController {
}) })
} }
public getNostrJoiningBlockNumber = async (
hexKey: string
): Promise<NostrJoiningBlock | null> => {
const relaySet = await this.findRelayListMetadata(hexKey)
const userRelays: string[] = []
// find user's relays
if (relaySet.write.length > 0) {
userRelays.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) {
userRelays.push(...nip05Profile.relays)
}
}
}
if (userRelays.length === 0) return null
// filter for finding user's first kind 1 event
const eventFilter: Filter = {
kinds: [kinds.ShortTextNote],
authors: [hexKey]
}
const pool = new SimplePool()
// find user's kind 1 events published on user's relays
const events = await pool.querySync(userRelays, eventFilter)
if (events && events.length) {
// sort events by created_at time in ascending order
events.sort((a, b) => a.created_at - b.created_at)
// get first ever event published on user's relays
const event = events[0]
const { created_at } = event
// initialize job request
const jobEventTemplate: EventTemplate = {
content: '',
created_at: Math.round(Date.now() / 1000),
kind: 68001,
tags: [
['i', `${created_at * 1000}`],
['j', 'blockChain-block-number']
]
}
// sign job request event
const jobSignedEvent = await this.nostrController.signEvent(
jobEventTemplate
)
const relays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relayable.org'
]
// publish job request
await this.nostrController.publishEvent(jobSignedEvent, relays)
console.log('jobSignedEvent :>> ', jobSignedEvent)
const subscribeWithTimeout = (
subscription: NDKSubscription,
timeoutMs: number
): Promise<string> => {
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)
// filter for getting DVM job's result
const sub = dvmNDK.subscribe({
kinds: [68002 as number],
'#e': [jobSignedEvent.id],
'#p': [jobSignedEvent.pubkey]
})
// asynchronously get block number from dvm job with 20 seconds timeout
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
const encodedEventPointer = nip19.neventEncode({
id: event.id,
relays: userRelays,
author: event.pubkey,
kind: event.kind
})
return {
block: parseInt(dvmJobResult),
encodedEventPointer
}
}
return null
}
public validate = (event: Event) => validateEvent(event) && verifyEvent(event) public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
} }

View File

@ -1,12 +1,19 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy' 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 { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import placeholderAvatar from '../../assets/images/nostr-logo.jpg'
import { MetadataController, NostrController } from '../../controllers' import { MetadataController, NostrController } from '../../controllers'
import { ProfileMetadata } from '../../types' import { NostrJoiningBlock, ProfileMetadata } from '../../types'
import styles from './style.module.scss' import styles from './style.module.scss'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { State } from '../../store/rootReducer' import { State } from '../../store/rootReducer'
@ -17,6 +24,8 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'
import { LoginMethods } from '../../store/auth/types' import { LoginMethods } from '../../store/auth/types'
export const ProfilePage = () => { export const ProfilePage = () => {
const theme = useTheme()
const { npub } = useParams() const { npub } = useParams()
const dispatch: Dispatch = useDispatch() const dispatch: Dispatch = useDispatch()
@ -25,6 +34,8 @@ export const ProfilePage = () => {
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const [pubkey, setPubkey] = useState<string>() const [pubkey, setPubkey] = useState<string>()
const [nostrJoiningBlock, setNostrJoiningBlock] =
useState<NostrJoiningBlock | null>(null)
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>() const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useSelector((state: State) => state.metadata)
@ -50,6 +61,18 @@ export const ProfilePage = () => {
}, [npub, usersPubkey]) }, [npub, usersPubkey])
useEffect(() => { useEffect(() => {
if (pubkey) {
metadataController
.getNostrJoiningBlockNumber(pubkey)
.then((res) => {
setNostrJoiningBlock(res)
})
.catch((err) => {
// todo: handle error
console.log('err :>> ', err)
})
}
if (isUsersOwnProfile && metadataState) { if (isUsersOwnProfile && metadataState) {
const metadataContent = metadataController.extractProfileMetadataContent( const metadataContent = metadataController.extractProfileMetadataContent(
metadataState as VerifiedEvent metadataState as VerifiedEvent
@ -206,8 +229,7 @@ export const ProfilePage = () => {
sx={{ sx={{
marginTop: 1, marginTop: 1,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column'
gap: 2
}} }}
> >
<img <img
@ -218,6 +240,20 @@ export const ProfilePage = () => {
src={profileMetadata.picture || placeholderAvatar} src={profileMetadata.picture || placeholderAvatar}
alt='Profile Image' alt='Profile Image'
/> />
{nostrJoiningBlock && (
<Typography
sx={{
color: theme.palette.getContrastText(
theme.palette.background.paper
)
}}
component={Link}
to={`https://njump.me/${nostrJoiningBlock.encodedEventPointer}`}
target='_blank'
>
On nostr since {nostrJoiningBlock.block.toLocaleString()}
</Typography>
)}
</ListItem> </ListItem>
{editItem('name', 'Username')} {editItem('name', 'Username')}

View File

@ -12,3 +12,8 @@ export interface RelaySet {
read: string[] read: string[]
write: string[] write: string[]
} }
export interface NostrJoiningBlock {
block: number
encodedEventPointer: string
}