diff --git a/index.html b/index.html index e4b78ea..ce2ce76 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + SIGit
diff --git a/public/Logo2.png b/public/Logo2.png deleted file mode 100644 index 407a4b4..0000000 Binary files a/public/Logo2.png and /dev/null differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..88d124a Binary files /dev/null and b/public/logo.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index b712993..c916977 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -21,7 +21,6 @@ import Username from '../username' import { Link, useLocation, useNavigate } from 'react-router-dom' import nostrichAvatar from '../../assets/images/avatar.png' -import nostrichLogo from '../../assets/images/nostr-logo.jpg' import { NostrController } from '../../controllers' import { appPrivateRoutes, @@ -114,7 +113,7 @@ export const AppBar = () => { - Logo navigate('/')} /> + Logo navigate('/')} /> {isAuthenticated && ( @@ -149,7 +148,7 @@ export const AppBar = () => { {!isAuthenticated && ( Logo navigate('/')} /> diff --git a/src/components/AppBar/style.module.scss b/src/components/AppBar/style.module.scss index 09f35bd..718b872 100644 --- a/src/components/AppBar/style.module.scss +++ b/src/components/AppBar/style.module.scss @@ -15,7 +15,7 @@ .logoWrapper { height: 50px; - width: 100px; + width: 155px; display: flex; align-items: center; cursor: pointer; diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index b2c116d..7dd1285 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -5,11 +5,15 @@ import { kinds, validateEvent, verifyEvent, - Event + Event, + EventTemplate, + nip19 } from 'nostr-tools' -import { ProfileMetadata, RelaySet } from '../types' +import { NostrJoiningBlock, 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 +167,130 @@ export class MetadataController { }) } + public getNostrJoiningBlockNumber = async ( + hexKey: string + ): Promise => { + 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 => { + 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) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index bd64e06..4ca9a4b 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,12 +1,20 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import { CircularProgress, List, ListItem, ListSubheader, TextField } from '@mui/material' +import { + CircularProgress, + 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' +import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { MetadataController, NostrController } from '../../controllers' -import { ProfileMetadata } from '../../types' +import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' import { useDispatch, useSelector } from 'react-redux' import { State } from '../../store/rootReducer' @@ -19,6 +27,8 @@ import { ApiController } from '../../controllers/ApiController' import { PostMediaResponse } from '../../types/media' export const ProfilePage = () => { + const theme = useTheme() + const { npub } = useParams() const dispatch: Dispatch = useDispatch() @@ -27,6 +37,8 @@ export const ProfilePage = () => { const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() + const [nostrJoiningBlock, setNostrJoiningBlock] = + useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [avatarLoading, setAvatarLoading] = useState(false) @@ -54,6 +66,18 @@ export const ProfilePage = () => { }, [npub, usersPubkey]) useEffect(() => { + if (pubkey) { + metadataController + .getNostrJoiningBlockNumber(pubkey) + .then((res) => { + setNostrJoiningBlock(res) + }) + .catch((err) => { + // todo: handle error + console.log('err :>> ', err) + }) + } + if (isUsersOwnProfile && metadataState) { const metadataContent = metadataController.extractProfileMetadataContent( metadataState as VerifiedEvent @@ -283,8 +307,7 @@ export const ProfilePage = () => { sx={{ marginTop: 1, display: 'flex', - flexDirection: 'column', - gap: 2 + flexDirection: 'column' }} > { }} focused /> + + {nostrJoiningBlock && ( + + On nostr since {nostrJoiningBlock.block.toLocaleString()} + + )} {editItem('name', 'Username')} diff --git a/src/types/nostr.ts b/src/types/nostr.ts index 87a9b27..67572d8 100644 --- a/src/types/nostr.ts +++ b/src/types/nostr.ts @@ -12,3 +12,8 @@ export interface RelaySet { read: string[] write: string[] } + +export interface NostrJoiningBlock { + block: number + encodedEventPointer: string +} diff --git a/src/utils/misc.ts b/src/utils/misc.ts index c54ce18..561f17e 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -75,7 +75,7 @@ export const sendDM = async ( ? 'Your signature is requested on the document below!' : 'You have received a signed document.' - const decryptionUrl = `${window.location.origin}/#${appPrivateRoutes.decryptZip}?file=${fileUrl}&key=${encryptionKey}` + const decryptionUrl = `${window.location.origin}/#${appPrivateRoutes.decryptZip}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}` const content = `${initialLine}\n\n${decryptionUrl}\n\nDirect download${fileUrl}`