@@ -15,21 +17,8 @@ export const ModCard = ({ backgroundLink }: ModCardProps) => {
}}
>
diff --git a/src/controllers/metadata.ts b/src/controllers/metadata.ts
index c1cac3a..7ef3be4 100644
--- a/src/controllers/metadata.ts
+++ b/src/controllers/metadata.ts
@@ -1,6 +1,6 @@
import NDK, { NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
import { UserProfile } from '../types/user'
-import { hexToNpub } from '../utils'
+import { hexToNpub, log, LogType, npubToHex } from '../utils'
/**
* Singleton class to manage metadata operations using NDK.
@@ -8,12 +8,42 @@ import { hexToNpub } from '../utils'
export class MetadataController {
private static instance: MetadataController
private profileNdk: NDK
+ public adminNpubs: string[]
+ public adminRelays = new Set()
private constructor() {
this.profileNdk = new NDK({
explicitRelayUrls: ['wss://user.kindpag.es', 'wss://purplepag.es']
})
this.profileNdk.connect()
+
+ this.adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
+ }
+
+ private setAdminRelays = async () => {
+ const promises = this.adminNpubs.map((npub) => {
+ const hexKey = npubToHex(npub)
+ if (!hexKey) return null
+
+ return NDKRelayList.forUser(hexKey, this.profileNdk)
+ .then((ndkRelayList) => {
+ if (ndkRelayList) {
+ ndkRelayList.writeRelayUrls.forEach((url) =>
+ this.adminRelays.add(url)
+ )
+ }
+ })
+ .catch((err) => {
+ log(
+ true,
+ LogType.Error,
+ `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
+ err
+ )
+ })
+ })
+
+ await Promise.allSettled(promises)
}
/**
@@ -21,9 +51,11 @@ export class MetadataController {
*
* @returns The singleton instance of MetadataController.
*/
- public static getInstance(): MetadataController {
+ public static async getInstance(): Promise {
if (!MetadataController.instance) {
MetadataController.instance = new MetadataController()
+
+ MetadataController.instance.setAdminRelays()
}
return MetadataController.instance
}
diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts
index 85ff8f9..1065960 100644
--- a/src/controllers/relay.ts
+++ b/src/controllers/relay.ts
@@ -68,11 +68,11 @@ export class RelayController {
// todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done
+ const metadataController = await MetadataController.getInstance()
+
// Retrieve the list of write relays for the event's public key
// Use a timeout to handle cases where retrieving write relays takes too long
- const writeRelaysPromise = MetadataController.getInstance().findWriteRelays(
- event.pubkey
- )
+ const writeRelaysPromise = metadataController.findWriteRelays(event.pubkey)
log(this.debug, LogType.Info, `ℹ Finding user's write relays`)
@@ -85,6 +85,11 @@ export class RelayController {
return [] as string[] // Return an empty array if an error occurs
})
+ // push admin relay urls obtained from metadata controller to writeRelayUrls list
+ metadataController.adminRelays.forEach((url) => {
+ writeRelayUrls.push(url)
+ })
+
// Connect to all write relays obtained from MetadataController
const relayPromises = writeRelayUrls.map((relayUrl) =>
this.connectRelay(relayUrl)
@@ -141,17 +146,26 @@ export class RelayController {
}
/**
- * Asynchronously retrieves an event from a set of relays based on a provided filter.
+ * Asynchronously retrieves multiple event from a set of relays based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
*
* @param {Filter} filter - The filter criteria to find the event.
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
* @returns {Promise} - Returns a promise that resolves to the found event or null if not found.
*/
- fetchEvent = async (
+ fetchEvents = async (
filter: Filter,
relays: string[] = []
- ): Promise => {
+ ): Promise => {
+ // add app relay to relays array
+ relays.push(import.meta.env.VITE_APP_RELAY)
+
+ const metadataController = await MetadataController.getInstance()
+ // add admin relays to relays array
+ metadataController.adminRelays.forEach((url) => {
+ relays.push(url)
+ })
+
// Connect to all specified relays
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
await Promise.allSettled(relayPromises)
@@ -163,6 +177,7 @@ export class RelayController {
}
const events: Event[] = []
+ const eventIds = new Set() // To keep track of event IDs and avoid duplicates
// Create a promise for each relay subscription
const subPromises = this.connectedRelays.map((relay) => {
@@ -171,8 +186,17 @@ export class RelayController {
const sub = relay.subscribe([filter], {
// Handle incoming events
onevent: (e) => {
- log(this.debug, LogType.Info, `ℹ ${relay.url} : Received Event`, e)
- events.push(e)
+ // Add the event to the array if it's not a duplicate
+ if (!eventIds.has(e.id)) {
+ log(
+ this.debug,
+ LogType.Info,
+ `ℹ ${relay.url} : Received Event`,
+ e
+ )
+ eventIds.add(e.id) // Record the event ID
+ events.push(e) // Add the event to the array
+ }
},
// Handle the End-Of-Stream (EOSE) message
oneose: () => {
@@ -187,6 +211,24 @@ export class RelayController {
// Wait for all subscriptions to complete
await Promise.allSettled(subPromises)
+ // Return the most recent event, or null if no events were received
+ return events
+ }
+
+ /**
+ * Asynchronously retrieves an event from a set of relays based on a provided filter.
+ * If no relays are specified, it defaults to using connected relays.
+ *
+ * @param {Filter} filter - The filter criteria to find the event.
+ * @param {string[]} [relays] - An optional array of relay URLs to search for the event.
+ * @returns {Promise} - Returns a promise that resolves to the found event or null if not found.
+ */
+ fetchEvent = async (
+ filter: Filter,
+ relays: string[] = []
+ ): Promise => {
+ const events = await this.fetchEvents(filter, relays)
+
// Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at)
diff --git a/src/layout/header.tsx b/src/layout/header.tsx
index a7fd1f5..da0cec4 100644
--- a/src/layout/header.tsx
+++ b/src/layout/header.tsx
@@ -28,11 +28,12 @@ export const Header = () => {
} else {
dispatch(setIsAuth(true))
dispatch(setUser({ npub }))
- const metadataController = MetadataController.getInstance()
- metadataController.findMetadata(npub).then((userProfile) => {
- if (userProfile) {
- dispatch(setUser(userProfile))
- }
+ MetadataController.getInstance().then((metadataController) => {
+ metadataController.findMetadata(npub).then((userProfile) => {
+ if (userProfile) {
+ dispatch(setUser(userProfile))
+ }
+ })
})
}
}
diff --git a/src/pages/home.tsx b/src/pages/home.tsx
index 84c9ad2..71dbfd1 100644
--- a/src/pages/home.tsx
+++ b/src/pages/home.tsx
@@ -150,9 +150,21 @@ export const HomePage = () => {
Awesome Mods
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx
index e4205ff..a253136 100644
--- a/src/pages/mods.tsx
+++ b/src/pages/mods.tsx
@@ -3,180 +3,114 @@ import '../styles/pagination.css'
import '../styles/styles.css'
import '../styles/search.css'
import { ModCard } from '../components/ModCard'
+import { useDidMount } from '../hooks'
+import { Filter, kinds } from 'nostr-tools'
+import { RelayController } from '../controllers'
+import { constructModListFromEvents, log, LogType } from '../utils'
+import { toast } from 'react-toastify'
+import { LoadingSpinner } from '../components/LoadingSpinner'
+import { useState } from 'react'
+import { ModDetails } from '../types'
+
+enum SortByFilter {
+ Latest = 'Latest',
+ Oldest = 'Oldest',
+ Best_Rated = 'Best Rated',
+ Worst_Rated = 'Worst Rated'
+}
+
+enum NSFWFilter {
+ Hide_NSFW = 'Hide NSFW',
+ Show_NSFW = 'Show NSFW',
+ Only_NSFW = 'Only NSFW'
+}
+
+enum ModeratedFilter {
+ Moderated = 'Moderated',
+ Unmoderated = 'Unmoderated'
+}
export const ModsPage = () => {
+ const [isFetching, setIsFetching] = useState(true)
+ const [mods, setMods] = useState
([])
+
+ useDidMount(async () => {
+ const filter: Filter = {
+ kinds: [kinds.ClassifiedListing]
+ }
+
+ RelayController.getInstance()
+ .fetchEvents(filter, [])
+ .then((events) => {
+ const modList = constructModListFromEvents(events)
+ setMods(modList)
+ })
+ .catch((err) => {
+ log(
+ true,
+ LogType.Error,
+ 'An error occurred in fetching mods from relays',
+ err
+ )
+ toast.error('An error occurred in fetching mods from relays')
+ })
+ .finally(() => {
+ setIsFetching(false)
+ })
+ })
+
+ if (isFetching)
+ return
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+ {mods.map((mod, index) => (
+
+ ))}
-
+ )
+}
+
+const PageTitleRow = () => {
+ return (
+
+
@@ -184,3 +118,135 @@ export const ModsPage = () => {
)
}
+
+const Filters = () => {
+ return (
+
+
+
+
+
+
+
+ {Object.values(SortByFilter).map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+ {Object.values(ModeratedFilter).map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+ {Object.values(NSFWFilter).map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+ Show From: {window.location.host}
+
+
Show All
+
+
+
+
+
+ )
+}
+
+const Pagination = () => {
+ return (
+
+ )
+}
diff --git a/src/utils/mod.ts b/src/utils/mod.ts
index 7b70260..6cb04ee 100644
--- a/src/utils/mod.ts
+++ b/src/utils/mod.ts
@@ -42,3 +42,52 @@ export const extractModData = (event: Event): ModDetails => {
)
}
}
+
+/**
+ * Constructs a list of `ModDetails` objects from an array of events.
+ *
+ * This function filters out events that do not contain all required data,
+ * extracts the necessary information from the valid events, and constructs
+ * `ModDetails` objects.
+ *
+ * @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[] => {
+ // Filter and extract mod details from events
+ const modDetailsList: ModDetails[] = events
+ .filter(isModDataComplete) // Filter out incomplete events
+ .map((event) => extractModData(event)) // Extract data and construct ModDetails
+
+ return modDetailsList
+}
+
+/**
+ * Checks if the provided event contains all the required data for constructing a `ModDetails` object.
+ *
+ * This function verifies that the event has the necessary tags and values to construct a `ModDetails` object.
+ *
+ * @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 => {
+ // Helper function to check if a tag value is present and not empty
+ const hasTagValue = (tagIdentifier: string): boolean => {
+ const value = getTagValue(event, tagIdentifier)
+ return !!value && value.length > 0 && value[0].trim() !== ''
+ }
+
+ // Check if all required fields are present
+ return (
+ hasTagValue('t') &&
+ hasTagValue('published_at') &&
+ hasTagValue('game') &&
+ hasTagValue('title') &&
+ hasTagValue('featuredImageUrl') &&
+ hasTagValue('summary') &&
+ hasTagValue('nsfw') &&
+ getTagValue(event, 'screenshotsUrls') !== null &&
+ getTagValue(event, 'tags') !== null &&
+ getTagValue(event, 'downloadUrls') !== null
+ )
+}
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index db2f5bc..0aa99d1 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -52,3 +52,35 @@ export const getTagValue = (
// Return null if no matching tag is found.
return null
}
+
+/**
+ * @param hexKey hex private or public key
+ * @returns whether or not is key valid
+ */
+const validateHex = (hexKey: string) => {
+ return hexKey.match(/^[a-f0-9]{64}$/)
+}
+
+/**
+ * NPUB provided - it will convert NPUB to HEX
+ * HEX provided - it will return HEX
+ *
+ * @param pubKey in NPUB, HEX format
+ * @returns HEX format
+ */
+export const npubToHex = (pubKey: string): string | null => {
+ // If key is NPUB
+ if (pubKey.startsWith('npub1')) {
+ try {
+ return nip19.decode(pubKey).data as string
+ } catch (error) {
+ return null
+ }
+ }
+
+ // valid hex key
+ if (validateHex(pubKey)) return pubKey
+
+ // Not a valid hex key
+ return null
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 3e09383..a399374 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_APP_RELAY: string
+ readonly VITE_ADMIN_NPUBS: string
// more env variables...
}