chore: use-ndk #283

Merged
s merged 18 commits from use-ndk into staging 2025-01-06 11:10:49 +00:00
11 changed files with 590 additions and 64 deletions
Showing only changes of commit 3c061d5920 - Show all commits

102
package-lock.json generated
View File

@ -20,12 +20,14 @@
"@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
"@nostr-dev-kit/ndk": "2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"crypto-hash": "3.0.0",
"crypto-js": "^4.2.0",
"dexie": "4.0.8",
"dnd-core": "16.0.1",
"file-saver": "2.0.5",
"idb": "8.0.0",
@ -1710,65 +1712,79 @@
}
},
"node_modules/@nostr-dev-kit/ndk": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.5.0.tgz",
"integrity": "sha512-A2nRgjjLScDhGZGPWx8xUIJM66dJWScdWQoCn/tI1Gtwpple+C2Jp7C9t3mb0oF3bwd2nsV6qwS//wdrH8QvYQ==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
"integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
"dependencies": {
"@noble/curves": "^1.4.0",
"@noble/hashes": "^1.3.1",
"@noble/secp256k1": "^2.0.0",
"@scure/base": "^1.1.1",
"debug": "^4.3.4",
"light-bolt11-decoder": "^3.0.0",
"node-fetch": "^3.3.1",
"nostr-tools": "^1.15.0",
"nostr-tools": "^2.7.1",
"tseep": "^1.1.1",
"typescript-lru-cache": "^2.0.0",
"utf8-buffer": "^1.0.0",
"websocket-polyfill": "^0.0.3"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
"funding": {
"url": "https://paulmillr.com/funding/"
"node_modules/@nostr-dev-kit/ndk-cache-dexie": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
"integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
"dependencies": {
"@nostr-dev-kit/ndk": "2.10.0",
"debug": "^4.3.4",
"dexie": "^4.0.2",
"nostr-tools": "^2.4.0",
"typescript-lru-cache": "^2.0.0"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
"dependencies": {
"@noble/hashes": "1.3.1"
"@noble/hashes": "1.6.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
"engines": {
"node": ">= 16"
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
"integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==",
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"dependencies": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.1.0",
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
@ -1778,6 +1794,39 @@
}
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@pdf-lib/fontkit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
@ -3850,6 +3899,11 @@
"node": ">=8"
}
},
"node_modules/dexie": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ=="
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",

View File

@ -30,12 +30,14 @@
"@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
"@nostr-dev-kit/ndk": "2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"crypto-hash": "3.0.0",
"crypto-js": "^4.2.0",
"dexie": "4.0.8",
"dnd-core": "16.0.1",
"file-saver": "2.0.5",
"idb": "8.0.0",

243
src/contexts/NDKContext.tsx Normal file
View File

@ -0,0 +1,243 @@
import NDK, {
getRelayListForUser,
NDKEvent,
NDKFilter,
NDKRelaySet,
NDKSubscriptionCacheUsage,
NDKUser,
NDKUserProfile
} from '@nostr-dev-kit/ndk'
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
import { Dexie } from 'dexie'
import { createContext, ReactNode, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify'
import { UserRelaysType } from '../types'
import {
DEFAULT_LOOK_UP_RELAY_LIST,
hexToNpub,
orderEventsChronologically,
timeout
} from '../utils'
export interface NDKContextType {
ndk: NDK
fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
fetchEvent: (filter: NDKFilter) => Promise<NDKEvent | null>
fetchEventsFromUserRelays: (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
) => Promise<NDKEvent[]>
fetchEventFromUserRelays: (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
) => Promise<NDKEvent | null>
findMetadata: (pubkey: string) => Promise<NDKUserProfile | null>
publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise<string[]>
}
// Create the context with an initial value of `null`
export const NDKContext = createContext<NDKContextType | null>(null)
// Create a provider component to wrap around parts of your app
export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
event.preventDefault()
if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
console.log(
'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
)
await Dexie.delete('degmod-db')
// Must reload to open a brand new DB
window.location.reload()
}
}
}, [])
const ndk = useMemo(() => {
localStorage.setItem('debug', '*')
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' })
dexieAdapter.locking = true
const ndk = new NDK({
enableOutboxModel: true,
autoConnectUserRelays: true,
autoFetchUserMutelist: true,
explicitRelayUrls: [...DEFAULT_LOOK_UP_RELAY_LIST],
cacheAdapter: dexieAdapter
s marked this conversation as resolved
Review

We should probably have debug off or control it with ENV for production.

We should probably have debug off or control it with ENV for production.
})
ndk.connect()
return ndk
}, [])
/**
* Asynchronously retrieves multiple event based on a provided filter.
*
* @param filter - The filter criteria to find the event.
* @returns Returns a promise that resolves to the found event or null if not found.
*/
const fetchEvents = async (filter: NDKFilter): Promise<NDKEvent[]> => {
return ndk
.fetchEvents(filter, {
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
})
.then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet)
return orderEventsChronologically(ndkEvents)
})
.catch((err) => {
// Log the error and show a notification if fetching fails
console.error('An error occurred in fetching events', err)
toast.error('An error occurred in fetching events') // Show error notification
return [] // Return an empty array in case of an error
})
}
/**
* Asynchronously retrieves an event based on a provided filter.
*
* @param filter - The filter criteria to find the event.
* @returns Returns a promise that resolves to the found event or null if not found.
*/
const fetchEvent = async (filter: NDKFilter) => {
const events = await fetchEvents(filter)
if (events.length === 0) return null
return events[0]
}
/**
* Asynchronously retrieves multiple events from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the events using the provided filter.
*
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves with an array of events.
*/
const fetchEventsFromUserRelays = async (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
): Promise<NDKEvent[]> => {
// Find the user's relays (10s timeout).
const relayUrls = await Promise.race([
getRelayListForUser(hexKey, ndk),
timeout(3000)
])
.then((ndkRelayList) => {
if (ndkRelayList) return ndkRelayList[userRelaysType]
return [] // Return an empty array if ndkRelayList is undefined
})
.catch((err) => {
console.error(
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
err
)
return [] as string[]
})
return ndk
.fetchEvents(
filter,
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
relayUrls.length
? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
: undefined
)
.then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet)
return orderEventsChronologically(ndkEvents)
})
.catch((err) => {
// Log the error and show a notification if fetching fails
console.error('An error occurred in fetching events', err)
toast.error('An error occurred in fetching events') // Show error notification
return [] // Return an empty array in case of an error
})
}
/**
* Fetches an event from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the event using the provided filter.
*
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves to the fetched event or null if the operation fails.
*/
const fetchEventFromUserRelays = async (
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
) => {
const events = await fetchEventsFromUserRelays(
filter,
hexKey,
userRelaysType
)
if (events.length === 0) return null
return events[0]
}
/**
* Finds metadata for a given pubkey.
*
* @param hexKey - The pubkey to search for metadata.
* @returns A promise that resolves to the metadata event.
*/
const findMetadata = async (
pubkey: string
): Promise<NDKUserProfile | null> => {
const npub = hexToNpub(pubkey)
const user = new NDKUser({ npub })
user.ndk = ndk
return await user.fetchProfile()
}
const publish = async (
event: NDKEvent,
explicitRelayUrls?: string[]
): Promise<string[]> => {
if (!event.sig) throw new Error('Before publishing first sign the event!')
let ndkRelaySet: NDKRelaySet | undefined
if (explicitRelayUrls && explicitRelayUrls.length > 0) {
ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
}
return event
.publish(ndkRelaySet, 10000)
.then((res) => {
const relaysPublishedOn = Array.from(res)
return relaysPublishedOn.map((relay) => relay.url)
})
.catch((err) => {
console.error(`An error occurred in publishing event`, err)
return []
})
}
return (
<NDKContext.Provider
value={{
ndk,
fetchEvents,
fetchEvent,
fetchEventsFromUserRelays,
fetchEventFromUserRelays,
findMetadata,
publish
}}
>
{children}
</NDKContext.Provider>
)
}

View File

@ -1,2 +1,4 @@
export * from './store'
export * from './useDidMount'
export * from './useDvm'
export * from './useNDKContext'

98
src/hooks/useDvm.ts Normal file
View File

@ -0,0 +1,98 @@
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { EventTemplate } from 'nostr-tools'
import { NostrController } from '../controllers'
import { setRelayInfoAction } from '../store/actions'
import { RelayInfoObject } from '../types'
import { compareObjects, unixNow } from '../utils'
import { useAppDispatch, useAppSelector } from './store'
import { useNDKContext } from './useNDKContext'
export const useDvm = () => {
const dvmRelays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relayable.org'
]
const relayInfo = useAppSelector((state) => state.relays.info)
const { ndk, publish } = useNDKContext()
const dispatch = useAppDispatch()
/**
* Sets information about relays into relays.info app state.
* @param relayURIs - relay URIs to get information about
*/
const getRelayInfo = async (relayURIs: string[]) => {
// initialize job request
const jobEventTemplate: EventTemplate = {
content: '',
created_at: unixNow(),
kind: 68001,
tags: [
['i', `${JSON.stringify(relayURIs)}`],
['j', 'relay-info']
]
}
const nostrController = NostrController.getInstance()
// sign job request event
const jobSignedEvent = await nostrController.signEvent(jobEventTemplate)
// publish job request
const ndkEvent = new NDKEvent(ndk, jobSignedEvent)
await publish(ndkEvent, dvmRelays)
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))
})
}
// filter for getting DVM job's result
const sub = ndk.subscribe({
kinds: [68002 as number],
'#e': [jobSignedEvent.id],
'#p': [jobSignedEvent.pubkey]
})
// asynchronously get relay info from dvm job with 20 seconds timeout
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
if (!dvmJobResult) {
return Promise.reject(`Relay(s) information wasn't received`)
}
let newRelaysInfo: RelayInfoObject
try {
newRelaysInfo = JSON.parse(dvmJobResult)
} catch (error) {
return Promise.reject(`Invalid relay(s) information.`)
}
if (newRelaysInfo && !compareObjects(relayInfo, newRelaysInfo)) {
dispatch(setRelayInfoAction(newRelaysInfo))
}
}
return { getRelayInfo }
}

View File

@ -0,0 +1,13 @@
import { NDKContext, NDKContextType } from '../contexts/NDKContext'
import { useContext } from 'react'
export const useNDKContext = () => {
const ndkContext = useContext(NDKContext)
if (!ndkContext)
throw new Error(
'NDKContext should not be used in out component tree hierarchy'
)
return { ...ndkContext } as NDKContextType
}

View File

@ -11,6 +11,7 @@ import './index.css'
import store from './store/store.ts'
import { theme } from './theme'
import { saveState } from './utils'
import { NDKContextProvider } from './contexts/NDKContext'
store.subscribe(
_.throttle(() => {
@ -28,7 +29,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<CssVarsProvider theme={theme}>
<HashRouter>
<Provider store={store}>
<App />
<NDKContextProvider>
<App />
</NDKContextProvider>
<ToastContainer />
</Provider>
</HashRouter>

View File

@ -13,26 +13,45 @@ import Switch from '@mui/material/Switch'
import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { Container } from '../../../components/Container'
import { relayController } from '../../../controllers'
import { useAppDispatch, useAppSelector, useDidMount } from '../../../hooks'
import {
useAppDispatch,
useAppSelector,
useDidMount,
useDvm,
useNDKContext
} from '../../../hooks'
import { setRelayMapAction } from '../../../store/actions'
import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types'
import {
RelayConnectionState,
RelayFee,
RelayInfo,
RelayMap
} from '../../../types'
import {
capitalizeFirstLetter,
compareObjects,
getRelayInfo,
getRelayMap,
hexToNpub,
normalizeWebSocketURL,
publishRelayMap,
shorten
shorten,
timeout
} from '../../../utils'
import styles from './style.module.scss'
import { Footer } from '../../../components/Footer/Footer'
import {
getRelayListForUser,
NDKRelayList,
NDKRelayStatus
} from '@nostr-dev-kit/ndk'
export const RelaysPage = () => {
const dispatch = useAppDispatch()
const { ndk, publish } = useNDKContext()
const { getRelayInfo } = useDvm()
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
const dispatch = useAppDispatch()
const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null)
const [newRelayURI, setNewRelayURI] = useState<string>()
const [newRelayURIerror, setNewRelayURIerror] = useState<string>()
@ -42,22 +61,74 @@ export const RelaysPage = () => {
const webSocketPrefix = 'wss://'
useDidMount(() => {
// fetch relay list from relays
useEffect(() => {
if (usersPubkey) {
getRelayMap(usersPubkey).then((newRelayMap) => {
if (!compareObjects(relayMap, newRelayMap.map)) {
dispatch(setRelayMapAction(newRelayMap.map))
Promise.race([getRelayListForUser(usersPubkey, ndk), timeout(10000)])
.then((res) => {
setNDKRelayList(res)
})
.catch((err) => {
toast.error(
`An error occurred in fetching user relay list: ${
err.message || err
}`
)
setNDKRelayList(new NDKRelayList(ndk))
})
}
}, [usersPubkey, ndk])
// construct the RelayMap from newly received NDKRelayList event
// and compare it with existing relay map in redux store
// if there are any differences then update the redux store with
// new relay map
useEffect(() => {
if (ndkRelayList) {
const newRelayMap: RelayMap = {}
ndkRelayList.readRelayUrls.forEach((relayUrl) => {
const normalizedUrl = normalizeWebSocketURL(relayUrl)
newRelayMap[normalizedUrl] = {
read: true,
write: false
}
})
ndkRelayList.writeRelayUrls.forEach((relayUrl) => {
const normalizedUrl = normalizeWebSocketURL(relayUrl)
const existing = newRelayMap[normalizedUrl]
if (existing) {
existing.write = true
} else {
newRelayMap[normalizedUrl] = {
read: false,
write: true
}
}
})
if (!compareObjects(relayMap, newRelayMap)) {
dispatch(setRelayMapAction(newRelayMap))
}
}
})
// we want to run this effect only when ndkRelayList is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ndkRelayList])
useEffect(() => {
if (!relayMap) return
// Display notification if an empty relay map has been received
if (relayMap && Object.keys(relayMap).length === 0) {
if (Object.keys(relayMap).length === 0) {
relayRequirementWarning()
} else {
getRelayInfo(Object.keys(relayMap))
}
}, [relayMap])
}, [relayMap, getRelayInfo])
const relayRequirementWarning = () =>
toast.warning('At least one write relay is needed for SIGit to work.')
@ -85,7 +156,8 @@ export const RelaysPage = () => {
const relayMapPublishingRes = await publishRelayMap(
relayMapCopy,
usersPubkey,
[relay]
ndk,
publish
).catch((err) => handlePublishRelayMapError(err))
if (relayMapPublishingRes) {
@ -132,7 +204,9 @@ export const RelaysPage = () => {
// Publish updated relay map
const relayMapPublishingRes = await publishRelayMap(
relayMapCopy,
usersPubkey
usersPubkey,
ndk,
publish
).catch((err) => handlePublishRelayMapError(err))
if (relayMapPublishingRes) {
@ -161,9 +235,10 @@ export const RelaysPage = () => {
)
}
} else if (relayURI && usersPubkey) {
const relay = await relayController.connectRelay(relayURI)
const ndkRelay = ndk.pool.getRelay(relayURI)
await ndkRelay.connect(5000)
if (relay && relay.connected) {
if (ndkRelay.status >= NDKRelayStatus.CONNECTED) {
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
relayMapCopy[relayURI] = { write: true, read: true }
@ -171,7 +246,9 @@ export const RelaysPage = () => {
// Publish updated relay map
const relayMapPublishingRes = await publishRelayMap(
relayMapCopy,
usersPubkey
usersPubkey,
ndk,
publish
).catch((err) => handlePublishRelayMapError(err))
if (relayMapPublishingRes) {
@ -256,19 +333,36 @@ const RelayItem = ({
handleLeaveRelay,
handleRelayWriteChange
}: RelayItemProp) => {
const { ndk } = useNDKContext()
const [relayConnectionStatus, setRelayConnectionStatus] =
useState<RelayConnectionState>()
const [displayRelayInfo, setDisplayRelayInfo] = useState(false)
useDidMount(() => {
relayController.connectRelay(relayURI).then((relay) => {
if (relay && relay.connected) {
const ndkPool = ndk.pool
ndkPool.on('relay:connect', (relay) => {
if (relay.url === relayURI) {
setRelayConnectionStatus(RelayConnectionState.Connected)
} else {
}
})
ndkPool.on('relay:disconnect', (relay) => {
if (relay.url === relayURI) {
setRelayConnectionStatus(RelayConnectionState.NotConnected)
}
})
const relay = ndkPool.getRelay(relayURI)
if (relay) {
setRelayConnectionStatus(
relay.status >= NDKRelayStatus.CONNECTED
? RelayConnectionState.Connected
: RelayConnectionState.NotConnected
)
}
})
return (

View File

@ -1,3 +1,9 @@
export enum UserRelaysType {
Read = 'readRelayUrls',
Write = 'writeRelayUrls',
Both = 'bothRelayUrls'
}
export interface RelaySet {
read: string[]
write: string[]

View File

@ -35,6 +35,7 @@ import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
import { getHash } from './hash'
import { SIGIT_BLOSSOM } from './const.ts'
import { NDKEvent } from '@nostr-dev-kit/ndk'
/**
* Generates a `d` tag for userAppData
@ -989,3 +990,24 @@ export const getProfileUsername = (
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
length: 16
})
/**
* Orders an array of NDKEvent objects chronologically based on their `created_at` property.
*
* @param events - The array of NDKEvent objects to be sorted.
* @param reverse - Optional flag to reverse the sorting order.
* If true, sorts in ascending order (oldest first), otherwise sorts in descending order (newest first).
*
* @returns The sorted array of events.
*/
export function orderEventsChronologically(
events: NDKEvent[],
reverse: boolean = false
): NDKEvent[] {
events.sort((e1: NDKEvent, e2: NDKEvent) => {
if (reverse) return e1.created_at! - e2.created_at!
else return e2.created_at! - e1.created_at!
})
return events
}

View File

@ -10,6 +10,7 @@ import {
ONE_WEEK_IN_MS,
SIGIT_RELAY
} from './const'
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk'
const READ_MARKER = 'read'
const WRITE_MARKER = 'write'
@ -176,7 +177,8 @@ const getRelayMap = async (
const publishRelayMap = async (
relayMap: RelayMap,
npub: string,
extraRelaysToPublish?: string[]
ndk: NDK,
publish: (event: NDKEvent) => Promise<string[]>
): Promise<string> => {
const timestamp = unixNow()
const relayURIs = Object.keys(relayMap)
@ -205,21 +207,8 @@ const publishRelayMap = async (
const nostrController = NostrController.getInstance()
const signedEvent = await nostrController.signEvent(newRelayMapEvent)
let relaysToPublish = relayURIs
// Add extra relays if provided
if (extraRelaysToPublish) {
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
}
// If relay map is empty, use most popular relay URIs
if (!relaysToPublish.length) {
relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST
}
const publishResult = await relayController.publish(
signedEvent,
relaysToPublish
)
const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishResult = await publish(ndkEvent)
if (publishResult && publishResult.length) {
return Promise.resolve(