feat: fetch mods using ndk
All checks were successful
Release to Staging / build_and_release (push) Successful in 43s
All checks were successful
Release to Staging / build_and_release (push) Successful in 43s
This commit is contained in:
parent
73e2868f52
commit
44acba8d26
64
package-lock.json
generated
64
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "5.0.3",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@reduxjs/toolkit": "2.2.6",
|
||||
"@tiptap/core": "2.6.6",
|
||||
"@tiptap/extension-link": "2.6.6",
|
||||
@ -19,6 +20,7 @@
|
||||
"bech32": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"date-fns": "3.6.0",
|
||||
"dexie": "4.0.8",
|
||||
"dompurify": "3.1.6",
|
||||
"file-saver": "2.0.5",
|
||||
"fslightbox-react": "1.7.6",
|
||||
@ -1043,22 +1045,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz",
|
||||
"integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz",
|
||||
"integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.4.0"
|
||||
"@noble/hashes": "1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",
|
||||
"integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@ -1129,6 +1134,18 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"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/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@ -1383,9 +1400,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz",
|
||||
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==",
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
|
||||
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
@ -2670,11 +2687,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@ -2699,6 +2716,11 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
|
||||
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ=="
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@ -3718,9 +3740,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/light-bolt11-decoder": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.1.1.tgz",
|
||||
"integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
|
||||
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
|
||||
"dependencies": {
|
||||
"@scure/base": "1.1.1"
|
||||
}
|
||||
@ -3884,9 +3906,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
|
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "5.0.3",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@reduxjs/toolkit": "2.2.6",
|
||||
"@tiptap/core": "2.6.6",
|
||||
"@tiptap/extension-link": "2.6.6",
|
||||
@ -21,6 +22,7 @@
|
||||
"bech32": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"date-fns": "3.6.0",
|
||||
"dexie": "4.0.8",
|
||||
"dompurify": "3.1.6",
|
||||
"file-saver": "2.0.5",
|
||||
"fslightbox-react": "1.7.6",
|
||||
|
226
src/contexts/NDKContext.tsx
Normal file
226
src/contexts/NDKContext.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
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<ModDetails[]>
|
||||
fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise<NDKEvent[]>
|
||||
}
|
||||
|
||||
// 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()
|
||||
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<ModDetails[]> => {
|
||||
const relays = new Set<string>()
|
||||
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<NDKEvent[]> => {
|
||||
const relays = new Set<string>()
|
||||
|
||||
// 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 (
|
||||
<NDKContext.Provider value={{ ndk, fetchMods, fetchEvents }}>
|
||||
{children}
|
||||
</NDKContext.Provider>
|
||||
)
|
||||
}
|
@ -5,3 +5,4 @@ export * from './useGames'
|
||||
export * from './useMuteLists'
|
||||
export * from './useNSFWList'
|
||||
export * from './useReactions'
|
||||
export * from './useNDKContext'
|
||||
|
19
src/hooks/useNDKContext.ts
Normal file
19
src/hooks/useNDKContext.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { NDKContext } 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'
|
||||
)
|
||||
|
||||
const { ndk, fetchEvents, fetchMods } = ndkContext
|
||||
|
||||
return {
|
||||
ndk,
|
||||
fetchEvents,
|
||||
fetchMods
|
||||
}
|
||||
}
|
@ -7,12 +7,15 @@ import 'react-toastify/dist/ReactToastify.css'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { store } from './store/index.ts'
|
||||
import { NDKContextProvider } from 'contexts/NDKContext.tsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
<NDKContextProvider>
|
||||
<App />
|
||||
</NDKContextProvider>
|
||||
<ToastContainer />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||
import { MAX_GAMES_PER_PAGE } from 'constants.ts'
|
||||
import { useDidMount, useGames } from 'hooks'
|
||||
import { useDidMount, useGames, useNDKContext } from 'hooks'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { GameCard } from '../components/GameCard'
|
||||
import '../styles/pagination.css'
|
||||
@ -8,10 +8,10 @@ import '../styles/search.css'
|
||||
import '../styles/styles.css'
|
||||
import { createSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { appRoutes } from 'routes'
|
||||
import { fetchMods } from 'utils'
|
||||
|
||||
export const GamesPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const { fetchMods } = useNDKContext()
|
||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||
const games = useGames()
|
||||
const [gamesWithMods, setGamesWithMods] = useState<string[]>([])
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Filter, nip19 } from 'nostr-tools'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
|
||||
@ -7,23 +7,23 @@ import { BlogCard } from '../components/BlogCard'
|
||||
import { GameCard } from '../components/GameCard'
|
||||
import { ModCard } from '../components/ModCard'
|
||||
import { LANDING_PAGE_DATA } from '../constants'
|
||||
import { RelayController } from '../controllers'
|
||||
import { useDidMount, useGames, useMuteLists, useNSFWList } from '../hooks'
|
||||
import {
|
||||
useDidMount,
|
||||
useGames,
|
||||
useMuteLists,
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from '../hooks'
|
||||
import { appRoutes, getModPageRoute } from '../routes'
|
||||
import { ModDetails } from '../types'
|
||||
import {
|
||||
extractModData,
|
||||
fetchMods,
|
||||
handleModImageError,
|
||||
log,
|
||||
LogType
|
||||
} from '../utils'
|
||||
import { extractModData, handleModImageError, log, LogType } from '../utils'
|
||||
|
||||
import '../styles/cardLists.css'
|
||||
import '../styles/SimpleSlider.css'
|
||||
import '../styles/styles.css'
|
||||
|
||||
// Import Swiper styles
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import 'swiper/css'
|
||||
import 'swiper/css/navigation'
|
||||
import 'swiper/css/pagination'
|
||||
@ -146,23 +146,24 @@ type SlideContentProps = {
|
||||
|
||||
const SlideContent = ({ naddr }: SlideContentProps) => {
|
||||
const navigate = useNavigate()
|
||||
const { fetchEvents } = useNDKContext()
|
||||
const [mod, setMod] = useState<ModDetails>()
|
||||
|
||||
useDidMount(() => {
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||
|
||||
const filter: Filter = {
|
||||
const ndkFilter: NDKFilter = {
|
||||
'#a': [identifier],
|
||||
authors: [pubkey],
|
||||
kinds: [kind]
|
||||
}
|
||||
|
||||
RelayController.getInstance()
|
||||
.fetchEvent(filter, relays)
|
||||
.then((event) => {
|
||||
if (event) {
|
||||
const extracted = extractModData(event)
|
||||
fetchEvents(ndkFilter, relays)
|
||||
.then((ndkEvents) => {
|
||||
if (ndkEvents.length > 0) {
|
||||
const ndkEvent = ndkEvents[0]
|
||||
const extracted = extractModData(ndkEvent)
|
||||
setMod(extracted)
|
||||
}
|
||||
})
|
||||
@ -220,21 +221,23 @@ type DisplayModProps = {
|
||||
const DisplayMod = ({ naddr }: DisplayModProps) => {
|
||||
const [mod, setMod] = useState<ModDetails>()
|
||||
|
||||
const { fetchEvents } = useNDKContext()
|
||||
|
||||
useDidMount(() => {
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||
|
||||
const filter: Filter = {
|
||||
const ndkFilter: NDKFilter = {
|
||||
'#a': [identifier],
|
||||
authors: [pubkey],
|
||||
kinds: [kind]
|
||||
}
|
||||
|
||||
RelayController.getInstance()
|
||||
.fetchEvent(filter, relays)
|
||||
.then((event) => {
|
||||
if (event) {
|
||||
const extracted = extractModData(event)
|
||||
fetchEvents(ndkFilter, relays)
|
||||
.then((ndkEvents) => {
|
||||
if (ndkEvents.length > 0) {
|
||||
const ndkEvent = ndkEvents[0]
|
||||
const extracted = extractModData(ndkEvent)
|
||||
setMod(extracted)
|
||||
}
|
||||
})
|
||||
@ -255,6 +258,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
|
||||
|
||||
const DisplayLatestMods = () => {
|
||||
const navigate = useNavigate()
|
||||
const { fetchMods } = useNDKContext()
|
||||
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
|
||||
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
|
||||
|
||||
@ -263,8 +267,7 @@ const DisplayLatestMods = () => {
|
||||
|
||||
useDidMount(() => {
|
||||
fetchMods({ source: window.location.host })
|
||||
.then((res) => {
|
||||
const mods = res.sort((a, b) => b.published_at - a.published_at)
|
||||
.then((mods) => {
|
||||
setLatestMods(mods)
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
useAppSelector,
|
||||
useFilteredMods,
|
||||
useMuteLists,
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from '../hooks'
|
||||
import { appRoutes } from '../routes'
|
||||
@ -23,9 +24,9 @@ import {
|
||||
NSFWFilter,
|
||||
SortBy
|
||||
} from '../types'
|
||||
import { fetchMods } from '../utils'
|
||||
|
||||
export const ModsPage = () => {
|
||||
const { fetchMods } = useNDKContext()
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
@ -50,7 +51,7 @@ export const ModsPage = () => {
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}, [filterOptions.source])
|
||||
}, [filterOptions.source, fetchMods])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setIsFetching(true)
|
||||
@ -69,7 +70,7 @@ export const ModsPage = () => {
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}, [filterOptions.source, mods])
|
||||
}, [filterOptions.source, mods, fetchMods])
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
setIsFetching(true)
|
||||
@ -87,7 +88,7 @@ export const ModsPage = () => {
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}, [filterOptions.source, mods])
|
||||
}, [filterOptions.source, mods, fetchMods])
|
||||
|
||||
const filteredModList = useFilteredMods(
|
||||
mods,
|
||||
|
106
src/utils/mod.ts
106
src/utils/mod.ts
@ -1,11 +1,7 @@
|
||||
import { Event, Filter, kinds } from 'nostr-tools'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { ModDetails, ModFormState } from '../types'
|
||||
import { getTagValue } from './nostr'
|
||||
import { ModFormState, ModDetails } from '../types'
|
||||
import { RelayController } from '../controllers'
|
||||
import { log, LogType } from './utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../constants'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
* Extracts and normalizes mod data from an event.
|
||||
@ -16,7 +12,7 @@ import DOMPurify from 'dompurify'
|
||||
* @param event - The event object from which to extract data.
|
||||
* @returns A `Partial<PageData>` object containing extracted data.
|
||||
*/
|
||||
export const extractModData = (event: Event): ModDetails => {
|
||||
export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
||||
// Helper function to safely get the first value of a tag or return a default value
|
||||
const getFirstTagValue = (tagIdentifier: string, defaultValue = '') => {
|
||||
const tagValue = getTagValue(event, tagIdentifier)
|
||||
@ -35,7 +31,7 @@ export const extractModData = (event: Event): ModDetails => {
|
||||
aTag: getFirstTagValue('a'),
|
||||
rTag: getFirstTagValue('r'),
|
||||
author: event.pubkey,
|
||||
edited_at: event.created_at,
|
||||
edited_at: event.created_at!,
|
||||
body: event.content,
|
||||
published_at: getIntTagValue('published_at'),
|
||||
game: getFirstTagValue('game'),
|
||||
@ -61,7 +57,9 @@ export const extractModData = (event: Event): ModDetails => {
|
||||
* @param events - The array of event objects to be processed.
|
||||
* @returns An array of `ModDetails` objects constructed from valid events.
|
||||
*/
|
||||
export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
|
||||
export const constructModListFromEvents = (
|
||||
events: Event[] | NDKEvent[]
|
||||
): ModDetails[] => {
|
||||
// Filter and extract mod details from events
|
||||
const modDetailsList: ModDetails[] = events
|
||||
.filter(isModDataComplete) // Filter out incomplete events
|
||||
@ -78,7 +76,7 @@ export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
|
||||
* @param event - The event object to be checked.
|
||||
* @returns `true` if the event contains all required data; `false` otherwise.
|
||||
*/
|
||||
export const isModDataComplete = (event: Event): boolean => {
|
||||
export const isModDataComplete = (event: Event | NDKEvent): boolean => {
|
||||
// Helper function to check if a tag value is present and not empty
|
||||
const hasTagValue = (tagIdentifier: string): boolean => {
|
||||
const value = getTagValue(event, tagIdentifier)
|
||||
@ -133,89 +131,3 @@ export const initializeFormState = (
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
interface FetchModsOptions {
|
||||
source?: string
|
||||
until?: number
|
||||
since?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const fetchMods = async ({
|
||||
source,
|
||||
until,
|
||||
since,
|
||||
limit
|
||||
}: FetchModsOptions): Promise<ModDetails[]> => {
|
||||
// Define the filter criteria for fetching mods
|
||||
const filter: Filter = {
|
||||
kinds: [kinds.ClassifiedListing], // 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
|
||||
}
|
||||
|
||||
// Fetch events from the relay using the defined filter
|
||||
return RelayController.getInstance()
|
||||
.fetchEvents(filter, []) // Pass the filter and an empty array of options
|
||||
.then((events) => {
|
||||
// Convert the fetched events into a list of mods
|
||||
const modList = constructModListFromEvents(events)
|
||||
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 [] as ModDetails[] // Return an empty array in case of an error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given HTML string and adds target="_blank" to all <a> tags.
|
||||
*
|
||||
* @param htmlString - The HTML string to sanitize and modify.
|
||||
* @returns The modified HTML string with sanitized content and updated links.
|
||||
*/
|
||||
export const sanitizeAndAddTargetBlank = (htmlString: string) => {
|
||||
// Step 1: Sanitize the HTML string using DOMPurify.
|
||||
// This removes any potentially dangerous content and ensures that the HTML is safe to use.
|
||||
const sanitizedHtml = DOMPurify.sanitize(htmlString, { ADD_ATTR: ['target'] })
|
||||
|
||||
// Step 2: Create a temporary container (a <div> element) to parse the sanitized HTML.
|
||||
// This allows us to manipulate the HTML content in a safe and controlled manner.
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.innerHTML = sanitizedHtml
|
||||
|
||||
// Step 3: Add target="_blank" to all <a> tags within the temporary container.
|
||||
// This ensures that all links open in a new tab when clicked.
|
||||
const links = tempDiv.querySelectorAll('a')
|
||||
links.forEach((link) => {
|
||||
link.setAttribute('target', '_blank')
|
||||
})
|
||||
|
||||
// Step 4: Convert the manipulated DOM back to an HTML string.
|
||||
// This string contains the sanitized content with the target="_blank" attribute added to all links.
|
||||
return tempDiv.innerHTML
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { toast } from 'react-toastify'
|
||||
import { RelayController } from '../controllers'
|
||||
import { log, LogType } from './utils'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
/**
|
||||
* Get the current time in seconds since the Unix epoch (January 1, 1970).
|
||||
@ -50,7 +51,7 @@ export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
|
||||
* @returns {string | null} The value(s) associated with the specified tag identifier, or `null` if the tag is not found.
|
||||
*/
|
||||
export const getTagValue = (
|
||||
event: Event,
|
||||
event: Event | NDKEvent,
|
||||
tagIdentifier: string
|
||||
): string[] | null => {
|
||||
// Find the tag in the event's tags array where the first element matches the tagIdentifier.
|
||||
@ -219,3 +220,24 @@ export const sendDMUsingRandomKey = async (
|
||||
// Return true indicating that the DM was successfully sent
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user