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 = () => {
- navigate('/')} />
+ navigate('/')} />
{isAuthenticated && (
@@ -149,7 +148,7 @@ export const AppBar = () => {
{!isAuthenticated && (
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}`