import NDK, { getRelayListForUser, NDKEvent, NDKFilter, NDKKind, NDKRelaySet, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk' import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie' import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts' import { Dexie } from 'dexie' import { createContext, ReactNode, useEffect, useMemo } from 'react' import { toast } from 'react-toastify' import { ModDetails } from 'types' import { constructModListFromEvents, log, LogType, npubToHex, orderEventsChronologically } from 'utils' type FetchModsOptions = { source?: string until?: number since?: number limit?: number } interface NDKContextType { ndk: NDK fetchMods: (opts: FetchModsOptions) => Promise fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise } // Create the context with an initial value of `null` export const NDKContext = createContext(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() console.log(event.reason) 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' }) const ndk = new NDK({ enableOutboxModel: true, autoConnectUserRelays: true, autoFetchUserMutelist: true, explicitRelayUrls: [ 'wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.damus.io/', import.meta.env.VITE_APP_RELAY ], cacheAdapter: dexieAdapter }) ndk.connect() return ndk }, []) /** * Fetches a list of mods based on the provided source. * * @param source - The source URL to filter the mods. If it matches the current window location, * it adds a filter condition to the request. * @param until - Optional timestamp to filter events until this time. * @param since - Optional timestamp to filter events from this time. * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error, * it logs the error and shows a notification, then returns an empty array. */ const fetchMods = async ({ source, until, since, limit }: FetchModsOptions): Promise => { const relays = new Set() relays.add(import.meta.env.VITE_APP_RELAY) const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') const promises = adminNpubs.map((npub) => { const hexKey = npubToHex(npub) if (!hexKey) return null return getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { if (ndkRelayList) { ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url)) } }) .catch((err) => { log( true, LogType.Error, `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`, err ) }) }) await Promise.allSettled(promises) // Define the filter criteria for fetching mods const filter: NDKFilter = { kinds: [NDKKind.Classified], // Specify the kind of events to fetch limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 '#t': [T_TAG_VALUE], until, // Optional filter to fetch events until this timestamp since // Optional filter to fetch events from this timestamp } // If the source matches the current window location, add a filter condition if (source === window.location.host) { filter['#r'] = [window.location.host] // Add a tag filter for the current host } return ndk .fetchEvents( filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) ) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) orderEventsChronologically(ndkEvents) // Convert the fetched events into a list of mods const modList = constructModListFromEvents(ndkEvents) return modList // Return the list of mods }) .catch((err) => { // Log the error and show a notification if fetching fails log( true, LogType.Error, 'An error occurred in fetching mods from relays', err ) toast.error('An error occurred in fetching mods from relays') // Show error notification return [] // Return an empty array in case of an error }) } const fetchEvents = async ( filter: NDKFilter, relayUrls: string[] = [] ): Promise => { const relays = new Set() // add all the relays passed to relay set relayUrls.forEach((relayUrl) => { relays.add(relayUrl) }) relays.add(import.meta.env.VITE_APP_RELAY) const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') const promises = adminNpubs.map((npub) => { const hexKey = npubToHex(npub) if (!hexKey) return null return getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { if (ndkRelayList) { ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url)) } }) .catch((err) => { log( true, LogType.Error, `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`, err ) }) }) await Promise.allSettled(promises) return ndk .fetchEvents( filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) ) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) return orderEventsChronologically(ndkEvents) }) .catch((err) => { // Log the error and show a notification if fetching fails log( true, LogType.Error, 'An error occurred in fetching mods from relays', err ) toast.error('An error occurred in fetching mods from relays') // Show error notification return [] // Return an empty array in case of an error }) } return ( {children} ) }