From 59f4fd6b291923f3c5f9f4dc282912b958df04b2 Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 21 Oct 2024 13:22:52 +0500 Subject: [PATCH 01/13] fix: add admin relays to ndk explicit relays asyncronously --- src/contexts/NDKContext.tsx | 147 ++++++++++++++---------------------- src/controllers/relay.ts | 69 ----------------- src/pages/search.tsx | 2 +- 3 files changed, 58 insertions(+), 160 deletions(-) diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 0ec3771..4c84f7f 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -33,12 +33,8 @@ type FetchModsOptions = { interface NDKContextType { ndk: NDK fetchMods: (opts: FetchModsOptions) => Promise - fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise - fetchEvent: ( - filter: NDKFilter, - relayUrls?: string[] - ) => Promise - + fetchEvents: (filter: NDKFilter) => Promise + fetchEvent: (filter: NDKFilter) => Promise fetchEventsFromUserRelays: ( filter: NDKFilter, hexKey: string, @@ -72,6 +68,31 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { } }, []) + const addAdminRelays = async (ndk: NDK) => { + const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') + adminNpubs.forEach((npub) => { + const hexKey = npubToHex(npub) + if (hexKey) { + getRelayListForUser(hexKey, ndk) + .then((ndkRelayList) => { + if (ndkRelayList) { + ndkRelayList.bothRelayUrls.forEach((url) => + ndk.addExplicitRelay(url) + ) + } + }) + .catch((err) => { + log( + true, + LogType.Error, + `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`, + err + ) + }) + } + }) + } + const ndk = useMemo(() => { localStorage.setItem('debug', '*') const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' }) @@ -88,6 +109,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { ], cacheAdapter: dexieAdapter }) + addAdminRelays(ndk) ndk.connect() @@ -110,33 +132,6 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { 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 @@ -152,11 +147,10 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { } return ndk - .fetchEvents( - filter, - { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, - NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) - ) + .fetchEvents(filter, { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) orderEventsChronologically(ndkEvents) @@ -179,56 +173,17 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { } /** - * 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. + * Asynchronously retrieves multiple event based on a provided filter. * * @param filter - The filter criteria to find the event. - * @param relays - An optional array of relay URLs to search for the event. * @returns Returns a promise that resolves to the found event or null if not found. */ - 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) - + const fetchEvents = async (filter: NDKFilter): Promise => { return ndk - .fetchEvents( - filter, - { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, - NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) - ) + .fetchEvents(filter, { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) return orderEventsChronologically(ndkEvents) @@ -242,15 +197,13 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { } /** - * 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. + * Asynchronously retrieves an event based on a provided filter. * * @param filter - The filter criteria to find the event. - * @param relaysUrls - An optional array of relay URLs to search for the event. * @returns Returns a promise that resolves to the found event or null if not found. */ - const fetchEvent = async (filter: NDKFilter, relayUrls: string[] = []) => { - const events = await fetchEvents(filter, relayUrls) + const fetchEvent = async (filter: NDKFilter) => { + const events = await fetchEvents(filter) if (events.length === 0) return null return events[0] } @@ -285,8 +238,22 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { return [] as string[] }) - // Fetch the event from the user's relays using the provided filter and relay URLs - return fetchEvents(filter, relayUrls) + return ndk + .fetchEvents( + filter, + { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, + NDKRelaySet.fromRelayUrls(relayUrls, 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 events', err) + toast.error('An error occurred in fetching events') // Show error notification + return [] // Return an empty array in case of an error + }) } /** diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index 2fdd6ae..64c22c2 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -372,75 +372,6 @@ export class RelayController { return publishedOnRelays } - /** - * Subscribes to events from multiple relays. - * - * This method connects to the specified relay URLs and subscribes to events - * using the provided filter. It handles incoming events through the given - * `eventHandler` callback and manages the subscription lifecycle. - * - * @param filter - The filter criteria to apply when subscribing to events. - * @param relayUrls - An optional array of relay URLs to connect to. The default relay URL (`APP_RELAY`) is added automatically. - * @param eventHandler - A callback function to handle incoming events. It receives an `Event` object. - * - */ - subscribeForEvents = async ( - filter: Filter, - relayUrls: string[] = [], - eventHandler: (event: Event) => void - ) => { - const appRelay = import.meta.env.VITE_APP_RELAY - if (!relayUrls.includes(appRelay)) { - /** - * NOTE: To avoid side-effects on external relayUrls array passed as argument - * re-assigned relayUrls with added sigit relay instead of just appending to same array - */ - relayUrls = [...relayUrls, appRelay] // Add app relay to relays array if not exists already - } - - // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) - ) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - - // Check if any relays are connected - if (relays.length === 0) { - throw new Error('No relay is connected to fetch events!') - } - - const processedEvents: string[] = [] // To keep track of processed events - - // Create a promise for each relay subscription - const subscriptions = relays.map((relay) => - relay.subscribe([filter], { - // Handle incoming events - onevent: (e) => { - // Process event only if it hasn't been processed before - if (!processedEvents.includes(e.id)) { - processedEvents.push(e.id) - eventHandler(e) // Call the event handler with the event - } - } - }) - ) - - return subscriptions - } - getTotalZapAmount = async ( user: string, eTag: string, diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 41aebcb..f9f3cac 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -392,7 +392,7 @@ const UsersResult = ({ } setIsFetching(true) - fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es']) + fetchEvents(filter) .then((events) => { const results = events.map((event) => { const ndkEvent = new NDKEvent(undefined, event) From 7f66c17f904688befda165476a94bfd3301b6a68 Mon Sep 17 00:00:00 2001 From: freakoverse Date: Mon, 21 Oct 2024 09:00:57 +0000 Subject: [PATCH 02/13] created a new class for NSFW tag for mod cards --- src/styles/cardMod.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css index 3028908..7dae7cc 100644 --- a/src/styles/cardMod.css +++ b/src/styles/cardMod.css @@ -143,3 +143,10 @@ align-items: center; color: rgba(255, 255, 255, 0.25); } + +.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW.IBMSMSMBSSTagsTagNSFWCard { + position: absolute; + bottom: 10px; + right: 10px; + backdrop-filter: blur(10px); +} From 9d50cdfd88f37c0fb17b963ba0fd77b757ac9096 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 11:11:35 +0200 Subject: [PATCH 03/13] fix: disable scroll on popup open Closes #61 --- src/components/ProfileSection.tsx | 11 ++++++++++- src/hooks/index.ts | 1 + src/hooks/useScrollDisable.ts | 11 +++++++++++ src/layout/header.tsx | 5 +++++ src/pages/mod/index.tsx | 9 ++++++++- 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useScrollDisable.ts diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 170c1e4..e52e940 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -5,7 +5,12 @@ import { useState } from 'react' import { Link } from 'react-router-dom' import { toast } from 'react-toastify' import { RelayController, UserRelaysType } from '../controllers' -import { useAppSelector, useDidMount, useNDKContext } from '../hooks' +import { + useAppSelector, + useBodyScrollDisable, + useDidMount, + useNDKContext +} from '../hooks' import { appRoutes, getProfilePageRoute } from '../routes' import '../styles/author.css' import '../styles/innerPage.css' @@ -254,6 +259,8 @@ export const ProfileQRButtonWithPopUp = ({ }: QRButtonWithPopUpProps) => { const [isOpen, setIsOpen] = useState(false) + useBodyScrollDisable(isOpen) + const nprofile = nip19.nprofileEncode({ pubkey }) @@ -335,6 +342,8 @@ type ZapButtonWithPopUpProps = { const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { const [isOpen, setIsOpen] = useState(false) + useBodyScrollDisable(isOpen) + return ( <>
{ + useEffect(() => { + if (disable) document.body.style.overflow = 'hidden' + + return () => { + document.body.style.overflow = '' + } + }, [disable]) +} diff --git a/src/layout/header.tsx b/src/layout/header.tsx index bc4c2b8..5f5fc10 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -10,6 +10,7 @@ import { MetadataController } from '../controllers' import { useAppDispatch, useAppSelector, + useBodyScrollDisable, useDidMount, useNDKContext } from '../hooks' @@ -260,6 +261,8 @@ const TipButtonWithDialog = React.memo(() => { const [adminNpub, setAdminNpub] = useState(null) const [isOpen, setIsOpen] = useState(false) + useBodyScrollDisable(isOpen) + useDidMount(async () => { const metadataController = await MetadataController.getInstance() setAdminNpub(metadataController.adminNpubs[0]) @@ -321,6 +324,8 @@ const TipButtonWithDialog = React.memo(() => { const RegisterButtonWithDialog = () => { const [showPopUp, setShowPopUp] = useState(false) + useBodyScrollDisable(showPopUp) + return ( <> { const [isBlocked, setIsBlocked] = useState(false) const [isAddedToNSFW, setIsAddedToNSFW] = useState(false) + useBodyScrollDisable(showReportPopUp) + useEffect(() => { if (userState.auth && userState.user?.pubkey) { const pubkey = userState.user.pubkey as string From 82b87b3e3284e3d7ecab72edc7b1f339ad06ed17 Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 21 Oct 2024 14:33:40 +0500 Subject: [PATCH 04/13] fix: move getTotalZapAmount from relay controller to ndkContext --- src/components/ModCard.tsx | 7 +- src/contexts/NDKContext.tsx | 71 +++++++++++-- src/controllers/relay.ts | 128 +---------------------- src/hooks/useNDKContext.ts | 20 +--- src/pages/mod/internal/comment/index.tsx | 17 ++- src/pages/mod/internal/zap/index.tsx | 20 ++-- 6 files changed, 87 insertions(+), 176 deletions(-) diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index 49fb142..c14372f 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -5,8 +5,7 @@ import { handleModImageError } from '../utils' import { ModDetails } from 'types' import { getModPageRoute } from 'routes' import { kinds, nip19 } from 'nostr-tools' -import { useDidMount, useReactions } from 'hooks' -import { RelayController } from 'controllers' +import { useDidMount, useNDKContext, useReactions } from 'hooks' import { toast } from 'react-toastify' import { useComments } from 'hooks/useComments' @@ -19,10 +18,10 @@ export const ModCard = React.memo((props: ModDetails) => { eTag: props.id, aTag: props.aTag }) + const { getTotalZapAmount } = useNDKContext() useDidMount(() => { - RelayController.getInstance() - .getTotalZapAmount(props.author, props.id, props.aTag) + getTotalZapAmount(props.author, props.id, props.aTag) .then((res) => { setTotalZappedAmount(res.accumulatedZapAmount) }) diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 4c84f7f..da9c2c4 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -5,7 +5,8 @@ import NDK, { NDKKind, NDKRelaySet, NDKSubscriptionCacheUsage, - NDKUser + NDKUser, + zapInvoiceFromEvent } from '@nostr-dev-kit/ndk' import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie' import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts' @@ -36,16 +37,25 @@ interface NDKContextType { fetchEvents: (filter: NDKFilter) => Promise fetchEvent: (filter: NDKFilter) => Promise fetchEventsFromUserRelays: ( - filter: NDKFilter, + filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType ) => Promise fetchEventFromUserRelays: ( - filter: NDKFilter, + filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType ) => Promise findMetadata: (pubkey: string) => Promise + getTotalZapAmount: ( + user: string, + eTag: string, + aTag?: string, + currentLoggedInUser?: string + ) => Promise<{ + accumulatedZapAmount: number + hasZapped: boolean + }> } // Create the context with an initial value of `null` @@ -218,10 +228,10 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { * @returns A promise that resolves with an array of events. */ const fetchEventsFromUserRelays = async ( - filter: NDKFilter, + filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType - ) => { + ): Promise => { // Find the user's relays. const relayUrls = await getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { @@ -266,7 +276,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { * @returns A promise that resolves to the fetched event or null if the operation fails. */ const fetchEventFromUserRelays = async ( - filter: NDKFilter, + filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType ) => { @@ -296,6 +306,52 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { return userProfile } + const getTotalZapAmount = async ( + user: string, + eTag: string, + aTag?: string, + currentLoggedInUser?: string + ) => { + const filters: NDKFilter[] = [ + { + kinds: [NDKKind.Zap], + '#e': [eTag], + '#p': [user] + } + ] + + if (aTag) { + filters.push({ + kinds: [NDKKind.Zap], + '#a': [aTag], + '#p': [user] + }) + } + + const zapEvents = await fetchEventsFromUserRelays( + filters, + user, + UserRelaysType.Read + ) + + let accumulatedZapAmount = 0 + let hasZapped = false + + zapEvents.forEach((zap) => { + const zapInvoice = zapInvoiceFromEvent(zap) + if (zapInvoice) { + accumulatedZapAmount += Math.round(zapInvoice.amount / 1000) + + if (!hasZapped) hasZapped = zapInvoice.zappee === currentLoggedInUser + } + }) + + return { + accumulatedZapAmount, + hasZapped + } + } + return ( { fetchEvent, fetchEventsFromUserRelays, fetchEventFromUserRelays, - findMetadata + findMetadata, + getTotalZapAmount }} > {children} diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index 64c22c2..e8855c6 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -1,11 +1,5 @@ -import { Event, Filter, kinds, nip57, Relay } from 'nostr-tools' -import { - extractZapAmount, - log, - LogType, - normalizeWebSocketURL, - timeout -} from '../utils' +import { Event, Relay } from 'nostr-tools' +import { log, LogType, normalizeWebSocketURL, timeout } from '../utils' import { MetadataController, UserRelaysType } from './metadata' /** @@ -371,122 +365,4 @@ export class RelayController { // Return the list of relay URLs where the event was successfully published return publishedOnRelays } - - getTotalZapAmount = async ( - user: string, - eTag: string, - aTag?: string, - currentLoggedInUser?: string - ) => { - const metadataController = await MetadataController.getInstance() - - const relayUrls = await metadataController.findUserRelays( - user, - UserRelaysType.Read - ) - - const appRelay = import.meta.env.VITE_APP_RELAY - if (!relayUrls.includes(appRelay)) { - relayUrls.push(appRelay) - } - - // Connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) - ) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - - let accumulatedZapAmount = 0 - let hasZapped = false - - const eventIds = new Set() // To keep track of event IDs and avoid duplicates - - const filters: Filter[] = [ - { - kinds: [kinds.Zap], - '#e': [eTag] - } - ] - - if (aTag) { - filters.push({ - kinds: [kinds.Zap], - '#a': [aTag] - }) - } - - // Create a promise for each relay subscription - const subPromises = relays.map((relay) => { - return new Promise((resolve) => { - // Subscribe to the relay with the specified filter - const sub = relay.subscribe(filters, { - // Handle incoming events - onevent: (e) => { - // Add the event to the array if it's not a duplicate - if (!eventIds.has(e.id)) { - eventIds.add(e.id) // Record the event ID - - const zapRequestStr = e.tags.find( - (t) => t[0] === 'description' - )?.[1] - if (!zapRequestStr) return - - const error = nip57.validateZapRequest(zapRequestStr) - if (error) return - - let zapRequest: Event | null = null - - try { - zapRequest = JSON.parse(zapRequestStr) - } catch (error) { - log( - true, - LogType.Error, - 'Error occurred in parsing zap request', - error - ) - } - - if (!zapRequest) return - - const amount = extractZapAmount(zapRequest) - accumulatedZapAmount += amount - - if (amount > 0) { - if (!hasZapped) { - hasZapped = zapRequest.pubkey === currentLoggedInUser - } - } - } - }, - // Handle the End-Of-Stream (EOSE) message - oneose: () => { - sub.close() // Close the subscription - resolve() // Resolve the promise when EOSE is received - } - }) - }) - }) - - // Wait for all subscriptions to complete - await Promise.allSettled(subPromises) - - return { - accumulatedZapAmount, - hasZapped - } - } } diff --git a/src/hooks/useNDKContext.ts b/src/hooks/useNDKContext.ts index b551d5e..f7383df 100644 --- a/src/hooks/useNDKContext.ts +++ b/src/hooks/useNDKContext.ts @@ -9,23 +9,5 @@ export const useNDKContext = () => { 'NDKContext should not be used in out component tree hierarchy' ) - const { - ndk, - fetchEvents, - fetchEvent, - fetchEventsFromUserRelays, - fetchEventFromUserRelays, - fetchMods, - findMetadata - } = ndkContext - - return { - ndk, - fetchEvents, - fetchEvent, - fetchEventsFromUserRelays, - fetchEventFromUserRelays, - fetchMods, - findMetadata - } + return { ...ndkContext } } diff --git a/src/pages/mod/internal/comment/index.tsx b/src/pages/mod/internal/comment/index.tsx index 2f5b721..aeeaf7b 100644 --- a/src/pages/mod/internal/comment/index.tsx +++ b/src/pages/mod/internal/comment/index.tsx @@ -496,20 +496,19 @@ const Reactions = (props: Event) => { const Zap = (props: Event) => { const [isOpen, setIsOpen] = useState(false) + const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [hasZapped, setHasZapped] = useState(false) const userState = useAppSelector((state) => state.user) - - const [totalZappedAmount, setTotalZappedAmount] = useState(0) + const { getTotalZapAmount } = useNDKContext() useDidMount(() => { - RelayController.getInstance() - .getTotalZapAmount( - props.pubkey, - props.id, - undefined, - userState.user?.pubkey as string - ) + getTotalZapAmount( + props.pubkey, + props.id, + undefined, + userState.user?.pubkey as string + ) .then((res) => { setTotalZappedAmount(res.accumulatedZapAmount) setHasZapped(res.hasZapped) diff --git a/src/pages/mod/internal/zap/index.tsx b/src/pages/mod/internal/zap/index.tsx index 93e8bad..a0169bb 100644 --- a/src/pages/mod/internal/zap/index.tsx +++ b/src/pages/mod/internal/zap/index.tsx @@ -1,6 +1,5 @@ import { ZapSplit } from 'components/Zap' -import { RelayController } from 'controllers' -import { useAppSelector, useDidMount } from 'hooks' +import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { useState } from 'react' import { toast } from 'react-toastify' import { ModDetails } from 'types' @@ -12,20 +11,19 @@ type ZapProps = { export const Zap = ({ modDetails }: ZapProps) => { const [isOpen, setIsOpen] = useState(false) + const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [hasZapped, setHasZapped] = useState(false) const userState = useAppSelector((state) => state.user) - - const [totalZappedAmount, setTotalZappedAmount] = useState(0) + const { getTotalZapAmount } = useNDKContext() useDidMount(() => { - RelayController.getInstance() - .getTotalZapAmount( - modDetails.author, - modDetails.id, - modDetails.aTag, - userState.user?.pubkey as string - ) + getTotalZapAmount( + modDetails.author, + modDetails.id, + modDetails.aTag, + userState.user?.pubkey as string + ) .then((res) => { setTotalZappedAmount(res.accumulatedZapAmount) setHasZapped(res.hasZapped) From 4bd7c77c05a13319a60f8533e76e325f51c658b4 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 11:43:49 +0200 Subject: [PATCH 05/13] fix: disable scroll on zap popup open --- src/pages/mod/internal/comment/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/mod/internal/comment/index.tsx b/src/pages/mod/internal/comment/index.tsx index 2f5b721..3c92b25 100644 --- a/src/pages/mod/internal/comment/index.tsx +++ b/src/pages/mod/internal/comment/index.tsx @@ -5,7 +5,13 @@ import { UserRelaysType } from 'controllers' import { formatDate } from 'date-fns' -import { useAppSelector, useDidMount, useNDKContext, useReactions } from 'hooks' +import { + useAppSelector, + useBodyScrollDisable, + useDidMount, + useNDKContext, + useReactions +} from 'hooks' import { useComments } from 'hooks/useComments' import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' import React, { @@ -502,6 +508,8 @@ const Zap = (props: Event) => { const [totalZappedAmount, setTotalZappedAmount] = useState(0) + useBodyScrollDisable(isOpen) + useDidMount(() => { RelayController.getInstance() .getTotalZapAmount( From 4214fe127fbcfb6a463643ee96ea9c125444ce0a Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 11:44:15 +0200 Subject: [PATCH 06/13] fix: disable scroll on zap popup open --- src/pages/mod/internal/zap/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/mod/internal/zap/index.tsx b/src/pages/mod/internal/zap/index.tsx index 93e8bad..e40a07c 100644 --- a/src/pages/mod/internal/zap/index.tsx +++ b/src/pages/mod/internal/zap/index.tsx @@ -1,6 +1,6 @@ import { ZapSplit } from 'components/Zap' import { RelayController } from 'controllers' -import { useAppSelector, useDidMount } from 'hooks' +import { useAppSelector, useBodyScrollDisable, useDidMount } from 'hooks' import { useState } from 'react' import { toast } from 'react-toastify' import { ModDetails } from 'types' @@ -18,6 +18,8 @@ export const Zap = ({ modDetails }: ZapProps) => { const [totalZappedAmount, setTotalZappedAmount] = useState(0) + useBodyScrollDisable(isOpen) + useDidMount(() => { RelayController.getInstance() .getTotalZapAmount( From e3b6aecfe840152d7a31fd0246eade6524d25896 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 11:44:26 +0200 Subject: [PATCH 07/13] fix: disable scroll on nostr-login popup open --- src/layout/header.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/layout/header.tsx b/src/layout/header.tsx index 5f5fc10..faae13e 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -28,6 +28,18 @@ export const Header = () => { const { findMetadata } = useNDKContext() const userState = useAppSelector((state) => state.user) + // Track nostr-login extension modal open state + const [isOpen, setIsOpen] = useState(false) + const handleOpen = () => setIsOpen(true) + const handleClose = () => setIsOpen(false) + useEffect(() => { + window.addEventListener('nlCloseModal', handleClose) + return () => { + window.removeEventListener('nlCloseModal', handleClose) + } + }, []) + useBodyScrollDisable(isOpen) + useEffect(() => { initNostrLogin({ darkMode: true, @@ -67,6 +79,7 @@ export const Header = () => { }, [dispatch, findMetadata]) const handleLogin = () => { + handleOpen() launchNostrLoginDialog() } From b69be4d7552315a082809759822fcbd3bd174e97 Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 21 Oct 2024 16:39:56 +0500 Subject: [PATCH 08/13] fix: removed relay controller and used ndk --- src/components/ModForm.tsx | 23 +- src/components/ProfileSection.tsx | 10 +- src/contexts/NDKContext.tsx | 19 +- src/controllers/index.ts | 1 - src/controllers/relay.ts | 368 ----------------------- src/hooks/useReactions.ts | 14 +- src/pages/mod/index.tsx | 25 +- src/pages/mod/internal/comment/index.tsx | 103 +++---- src/pages/settings/profile.tsx | 9 +- src/pages/settings/relay.tsx | 62 ++-- src/utils/nostr.ts | 27 +- 11 files changed, 154 insertions(+), 507 deletions(-) delete mode 100644 src/controllers/relay.ts diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 6e90b1a..763a3f9 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -13,8 +13,7 @@ import { toast } from 'react-toastify' import { FixedSizeList as List } from 'react-window' import { v4 as uuidv4 } from 'uuid' import { T_TAG_VALUE } from '../constants' -import { RelayController } from '../controllers' -import { useAppSelector, useGames } from '../hooks' +import { useAppSelector, useGames, useNDKContext } from '../hooks' import { appRoutes, getModPageRoute } from '../routes' import '../styles/styles.css' import { DownloadUrl, ModDetails, ModFormState } from '../types' @@ -29,6 +28,7 @@ import { } from '../utils' import { CheckboxField, InputError, InputField } from './Inputs' import { LoadingSpinner } from './LoadingSpinner' +import { NDKEvent } from '@nostr-dev-kit/ndk' interface FormErrors { game?: string @@ -54,6 +54,7 @@ type ModFormProps = { export const ModForm = ({ existingModData }: ModFormProps) => { const location = useLocation() const navigate = useNavigate() + const { ndk, publish } = useNDKContext() const games = useGames() const userState = useAppSelector((state) => state.user) @@ -243,9 +244,8 @@ export const ModForm = ({ existingModData }: ModFormProps) => { return } - const publishedOnRelays = await RelayController.getInstance().publish( - signedEvent as Event - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { @@ -763,8 +763,9 @@ const GameDropdown = ({

- Can't find the game you're looking for? You can temporarily publish the mod under '(Unlisted Game)' and - later edit it with the proper game name once we add it. + Can't find the game you're looking for? You can temporarily publish the + mod under '(Unlisted Game)' and later edit it with the proper game name + once we add it.

@@ -825,10 +826,12 @@ const GameDropdown = ({
-
+
{error && } -

Note: Please mention the game name in the body text of your mod post (e.g., 'This is a mod for Game Name') - so we know what to look for and add. +

+ Note: Please mention the game name in the body text of your mod post + (e.g., 'This is a mod for Game Name') so we know what to look for and + add.

) diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 170c1e4..972c8ef 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -4,7 +4,7 @@ import { QRCodeSVG } from 'qrcode.react' import { useState } from 'react' import { Link } from 'react-router-dom' import { toast } from 'react-toastify' -import { RelayController, UserRelaysType } from '../controllers' +import { UserRelaysType } from '../controllers' import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import { appRoutes, getProfilePageRoute } from '../routes' import '../styles/author.css' @@ -22,6 +22,7 @@ import { import { LoadingSpinner } from './LoadingSpinner' import { ZapPopUp } from './Zap' import placeholder from '../assets/img/DEGMods Placeholder Img.png' +import { NDKEvent } from '@nostr-dev-kit/ndk' type Props = { pubkey: string @@ -368,7 +369,7 @@ type FollowButtonProps = { } const FollowButton = ({ pubkey }: FollowButtonProps) => { - const { fetchEventFromUserRelays } = useNDKContext() + const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() const [isFollowing, setIsFollowing] = useState(false) const [isLoading, setIsLoading] = useState(false) @@ -441,9 +442,8 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => { if (!signedEvent) return false - const publishedOnRelays = await RelayController.getInstance().publish( - signedEvent as Event - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) if (publishedOnRelays.length === 0) { toast.error('Failed to publish event on any relay') diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index da9c2c4..0e92db9 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -56,6 +56,7 @@ interface NDKContextType { accumulatedZapAmount: number hasZapped: boolean }> + publish: (event: NDKEvent) => Promise } // Create the context with an initial value of `null` @@ -352,6 +353,21 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { } } + const publish = async (event: NDKEvent): Promise => { + if (!event.sig) throw new Error('Before publishing first sign the event!') + + return event + .publish(undefined, 30000) + .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 ( { fetchEventsFromUserRelays, fetchEventFromUserRelays, findMetadata, - getTotalZapAmount + getTotalZapAmount, + publish }} > {children} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b7b89e4..b028b93 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,2 @@ export * from './metadata' -export * from './relay' export * from './zap' diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts deleted file mode 100644 index e8855c6..0000000 --- a/src/controllers/relay.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { Event, Relay } from 'nostr-tools' -import { log, LogType, normalizeWebSocketURL, timeout } from '../utils' -import { MetadataController, UserRelaysType } from './metadata' - -/** - * Singleton class to manage relay operations. - */ -export class RelayController { - private static instance: RelayController - private events = new Map() - private debug = true - public connectedRelays: Relay[] = [] - - private constructor() {} - - /** - * Provides the singleton instance of RelayController. - * - * @returns The singleton instance of RelayController. - */ - public static getInstance(): RelayController { - if (!RelayController.instance) { - RelayController.instance = new RelayController() - } - return RelayController.instance - } - - public connectRelay = async (relayUrl: string) => { - const relay = this.connectedRelays.find( - (relay) => - normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl) - ) - if (relay) { - // already connected, skip - return relay - } - - return await Relay.connect(relayUrl) - .then((relay) => { - log(this.debug, LogType.Info, `✅ nostr (${relayUrl}): Connected!`) - this.connectedRelays.push(relay) - return relay - }) - .catch((err) => { - log( - this.debug, - LogType.Error, - `❌ nostr (${relayUrl}): Connection error!`, - err - ) - return null - }) - } - - /** - * Publishes an event to multiple relays. - * - * This method establishes a connection to the application relay specified by - * an environment variable and a set of relays obtained from the - * `MetadataController`. It attempts to publish the event to all connected - * relays and returns a list of URLs of relays where the event was successfully - * published. - * - * If the process of finding relays or publishing the event takes too long, - * it handles the timeout to prevent blocking the operation. - * - * @param event - The event to be published. - * @param userHexKey - The user's hexadecimal public key, used to retrieve their relays. - * If not provided, the event's public key will be used. - * @param userRelaysType - The type of relays to be retrieved (e.g., write relays). - * Defaults to `UserRelaysType.Write`. - * @returns A promise that resolves to an array of URLs of relays where the event - * was published, or an empty array if no relays were connected or the - * event could not be published. - */ - publish = async ( - event: Event, - userHexKey?: string, - userRelaysType?: UserRelaysType - ): Promise => { - // Connect to the application relay specified by an environment variable - const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY) - - // TODO: Implement logic to retrieve relays using `window.nostr.getRelays()` once it becomes available in nostr-login. - - // Retrieve an instance of MetadataController to find user relays - const metadataController = await MetadataController.getInstance() - - // Retrieve the list of relays for the specified user's public key - const relayUrls = await metadataController.findUserRelays( - userHexKey || event.pubkey, - userRelaysType || UserRelaysType.Write - ) - - // Add admin relay URLs from the metadata controller to the list of relay URLs - metadataController.adminRelays.forEach((url) => { - relayUrls.push(url) - }) - - // Attempt to connect to all write relays obtained from MetadataController - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) - ) - - // Wait for all relay connection attempts to settle (either fulfilled or rejected) - const results = await Promise.allSettled([ - appRelayPromise, - ...relayPromises - ]) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - - // If no relays are connected, log an error and return an empty array - if (relays.length === 0) { - log(this.debug, LogType.Error, 'No relay is connected!') - return [] - } - - const publishedOnRelays: string[] = [] // Track relays where the event was successfully published - - // Create promises to publish the event to each connected relay - const publishPromises = relays.map((relay) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Sending event:`, - event - ) - - return Promise.race([ - relay.publish(event), // Publish the event to the relay - timeout(30000) // Set a timeout to handle slow publishing operations - ]) - .then((res) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Publish result:`, - res - ) - publishedOnRelays.push(relay.url) // Add successful relay URL to the list - }) - .catch((err) => { - log( - this.debug, - LogType.Error, - `❌ nostr (${relay.url}): Publish error!`, - err - ) - }) - }) - - // Wait for all publish operations to complete (either fulfilled or rejected) - await Promise.allSettled(publishPromises) - - if (publishedOnRelays.length > 0) { - // If the event was successfully published to any relays, check if it contains an `aTag` - // If the `aTag` is present, cache the event locally - const aTag = event.tags.find((item) => item[0] === 'a') - if (aTag && aTag[1]) { - this.events.set(aTag[1], event) - } - } - - // Return the list of relay URLs where the event was successfully published - return publishedOnRelays - } - - /** - * Publishes an encrypted DM to receiver's read relays. - * - * This method connects to the application relay and a set of receiver's read relays - * obtained from the `MetadataController`. It then publishes the event to - * all connected relays and returns a list of relays where the event was successfully published. - * - * @param event - The event to be published. - * @returns A promise that resolves to an array of URLs of relays where the event was published, - * or an empty array if no relays were connected or the event could not be published. - */ - publishDM = async (event: Event, receiver: string): Promise => { - // Connect to the application relay specified by environment variable - const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY) - - // 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 read relays for the receiver - const readRelayUrls = await metadataController.findUserRelays( - receiver, - UserRelaysType.Read - ) - - // push admin relay urls obtained from metadata controller to readRelayUrls list - metadataController.adminRelays.forEach((url) => { - readRelayUrls.push(url) - }) - - // Connect to all write relays obtained from MetadataController - const relayPromises = readRelayUrls.map((relayUrl) => - this.connectRelay(relayUrl) - ) - - // Wait for all relay connections to settle (either fulfilled or rejected) - await Promise.allSettled([appRelayPromise, ...relayPromises]) - - // Check if any relays are connected; if not, log an error and return null - if (this.connectedRelays.length === 0) { - log(this.debug, LogType.Error, 'No relay is connected!') - return [] - } - - const publishedOnRelays: string[] = [] // List to track which relays successfully published the event - - // Create a promise for publishing the event to each connected relay - const publishPromises = this.connectedRelays.map((relay) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Sending event:`, - event - ) - - return Promise.race([ - relay.publish(event), // Publish the event to the relay - timeout(30000) // Set a timeout to handle cases where publishing takes too long - ]) - .then((res) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Publish result:`, - res - ) - publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays - }) - .catch((err) => { - log( - this.debug, - LogType.Error, - `❌ nostr (${relay.url}): Publish error!`, - err - ) - }) - }) - - // Wait for all publish operations to complete (either fulfilled or rejected) - await Promise.allSettled(publishPromises) - - // Return the list of relay URLs where the event was published - return publishedOnRelays - } - - /** - * Publishes an event to multiple relays. - * - * This method establishes a connection to the application relay specified by - * an environment variable and a set of relays provided as argument. - * It attempts to publish the event to all connected relays - * and returns a list of URLs of relays where the event was successfully published. - * - * If the process of publishing the event takes too long, - * it handles the timeout to prevent blocking the operation. - * - * @param event - The event to be published. - * @param relayUrls - The array of relayUrl where event should be published - * @returns A promise that resolves to an array of URLs of relays where the event - * was published, or an empty array if no relays were connected or the - * event could not be published. - */ - publishOnRelays = async ( - event: Event, - relayUrls: string[] - ): Promise => { - const appRelay = import.meta.env.VITE_APP_RELAY - - if (!relayUrls.includes(appRelay)) { - /** - * NOTE: To avoid side-effects on external relayUrls array passed as argument - * re-assigned relayUrls with added sigit relay instead of just appending to same array - */ - relayUrls = [...relayUrls, appRelay] // Add app relay to relays array if not exists already - } - - // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) - ) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - - // Check if any relays are connected - if (relays.length === 0) { - log(this.debug, LogType.Error, 'No relay is connected!') - return [] - } - - const publishedOnRelays: string[] = [] // Track relays where the event was successfully published - - // Create promises to publish the event to each connected relay - const publishPromises = relays.map((relay) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Sending event:`, - event - ) - - return Promise.race([ - relay.publish(event), // Publish the event to the relay - timeout(30000) // Set a timeout to handle slow publishing operations - ]) - .then((res) => { - log( - this.debug, - LogType.Info, - `⬆️ nostr (${relay.url}): Publish result:`, - res - ) - publishedOnRelays.push(relay.url) // Add successful relay URL to the list - }) - .catch((err) => { - log( - this.debug, - LogType.Error, - `❌ nostr (${relay.url}): Publish error!`, - err - ) - }) - }) - - // Wait for all publish operations to complete (either fulfilled or rejected) - await Promise.allSettled(publishPromises) - - if (publishedOnRelays.length > 0) { - // If the event was successfully published to any relays, check if it contains an `aTag` - // If the `aTag` is present, cache the event locally - const aTag = event.tags.find((item) => item[0] === 'a') - if (aTag && aTag[1]) { - this.events.set(aTag[1], event) - } - } - - // Return the list of relay URLs where the event was successfully published - return publishedOnRelays - } -} diff --git a/src/hooks/useReactions.ts b/src/hooks/useReactions.ts index 9ebc63f..60406c2 100644 --- a/src/hooks/useReactions.ts +++ b/src/hooks/useReactions.ts @@ -1,6 +1,6 @@ import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk' import { REACTIONS } from 'constants.ts' -import { RelayController, UserRelaysType } from 'controllers' +import { UserRelaysType } from 'controllers' import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useMemo, useState } from 'react' @@ -14,7 +14,7 @@ type UseReactionsParams = { } export const useReactions = (params: UseReactionsParams) => { - const { ndk, fetchEventsFromUserRelays } = useNDKContext() + const { ndk, fetchEventsFromUserRelays, publish } = useNDKContext() const [isReactionInProgress, setIsReactionInProgress] = useState(false) const [isDataLoaded, setIsDataLoaded] = useState(false) const [reactionEvents, setReactionEvents] = useState([]) @@ -119,13 +119,11 @@ export const useReactions = (params: UseReactionsParams) => { if (!signedEvent) return - setReactionEvents((prev) => [...prev, new NDKEvent(ndk, signedEvent)]) + const ndkEvent = new NDKEvent(ndk, signedEvent) - const publishedOnRelays = await RelayController.getInstance().publish( - signedEvent as Event, - params.pubkey, - UserRelaysType.Read - ) + setReactionEvents((prev) => [...prev, ndkEvent]) + + const publishedOnRelays = await publish(ndkEvent) if (publishedOnRelays.length === 0) { log( diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index a85d716..dd678fd 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -53,7 +53,7 @@ export const ModPage = () => { useDidMount(async () => { if (naddr) { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) - const { identifier, kind, pubkey, relays = [] } = decoded.data + const { identifier, kind, pubkey } = decoded.data const filter: NDKFilter = { '#a': [identifier], @@ -61,7 +61,7 @@ export const ModPage = () => { kinds: [kind] } - fetchEvent(filter, relays) + fetchEvent(filter) .then((event) => { if (event) { const extracted = extractModData(event) @@ -212,7 +212,7 @@ type GameProps = { } const Game = ({ naddr, game, author, aTag }: GameProps) => { - const { fetchEventFromUserRelays } = useNDKContext() + const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() const userState = useAppSelector((state) => state.user) const [isLoading, setIsLoading] = useState(false) @@ -343,7 +343,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => { setLoadingSpinnerDesc('Updating mute list event') - const isUpdated = await signAndPublish(unsignedEvent) + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) if (isUpdated) { setIsBlocked(true) } @@ -384,7 +384,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => { } setLoadingSpinnerDesc('Updating mute list event') - const isUpdated = await signAndPublish(unsignedEvent) + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) if (isUpdated) { setIsBlocked(false) } @@ -450,7 +450,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => { setLoadingSpinnerDesc('Updating nsfw list event') - const isUpdated = await signAndPublish(unsignedEvent) + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) if (isUpdated) { setIsAddedToNSFW(true) } @@ -491,7 +491,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => { } setLoadingSpinnerDesc('Updating nsfw list event') - const isUpdated = await signAndPublish(unsignedEvent) + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) if (isUpdated) { setIsAddedToNSFW(false) } @@ -661,7 +661,7 @@ type ReportPopupProps = { } const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { - const { fetchEventFromUserRelays } = useNDKContext() + const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() const userState = useAppSelector((state) => state.user) const [selectedOptions, setSelectedOptions] = useState({ actuallyCP: false, @@ -760,7 +760,7 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { } setLoadingSpinnerDesc('Updating mute list event') - const isUpdated = await signAndPublish(unsignedEvent) + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) if (isUpdated) handleClose() } else { const href = window.location.href @@ -773,7 +773,12 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { }) setLoadingSpinnerDesc('Sending report') - const isSent = await sendDMUsingRandomKey(message, reportingPubkey!) + const isSent = await sendDMUsingRandomKey( + message, + reportingPubkey!, + ndk, + publish + ) if (isSent) handleClose() } setIsLoading(false) diff --git a/src/pages/mod/internal/comment/index.tsx b/src/pages/mod/internal/comment/index.tsx index aeeaf7b..a82c87d 100644 --- a/src/pages/mod/internal/comment/index.tsx +++ b/src/pages/mod/internal/comment/index.tsx @@ -1,9 +1,5 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk' import { ZapPopUp } from 'components/Zap' -import { - MetadataController, - RelayController, - UserRelaysType -} from 'controllers' import { formatDate } from 'date-fns' import { useAppSelector, useDidMount, useNDKContext, useReactions } from 'hooks' import { useComments } from 'hooks/useComments' @@ -47,6 +43,7 @@ type Props = { } export const Comments = ({ modDetails, setCommentCount }: Props) => { + const { ndk, publish } = useNDKContext() const { commentEvents, setCommentEvents } = useComments(modDetails) const [filterOptions, setFilterOptions] = useState({ sort: SortByEnum.Latest, @@ -82,7 +79,8 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { created_at: now(), tags: [ ['e', modDetails.id], - ['a', modDetails.aTag] + ['a', modDetails.aTag], + ['p', modDetails.author] ] } @@ -105,28 +103,52 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { ...prev ]) - const publish = async () => { - const metadataController = await MetadataController.getInstance() - const modAuthorReadRelays = await metadataController.findUserRelays( - modDetails.author, - UserRelaysType.Read - ) - const commentatorWriteRelays = await metadataController.findUserRelays( - pubkey, - UserRelaysType.Write - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + publish(ndkEvent) + .then((publishedOnRelays) => { + if (publishedOnRelays.length === 0) { + setCommentEvents((prev) => + prev.map((event) => { + if (event.id === signedEvent.id) { + return { + ...event, + status: CommentEventStatus.Failed + } + } - const combinedRelays = [ - ...new Set(...modAuthorReadRelays, ...commentatorWriteRelays) - ] + return event + }) + ) + } else { + setCommentEvents((prev) => + prev.map((event) => { + if (event.id === signedEvent.id) { + return { + ...event, + status: CommentEventStatus.Published + } + } - const publishedOnRelays = - await RelayController.getInstance().publishOnRelays( - signedEvent, - combinedRelays - ) + return event + }) + ) + } - if (publishedOnRelays.length === 0) { + // when an event is successfully published remove the status from it after 15 seconds + setTimeout(() => { + setCommentEvents((prev) => + prev.map((event) => { + if (event.id === signedEvent.id) { + delete event.status + } + + return event + }) + ) + }, 15000) + }) + .catch((err) => { + console.error('An error occurred in publishing comment', err) setCommentEvents((prev) => prev.map((event) => { if (event.id === signedEvent.id) { @@ -139,36 +161,7 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { return event }) ) - } else { - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - return { - ...event, - status: CommentEventStatus.Published - } - } - - return event - }) - ) - } - - // when an event is successfully published remove the status from it after 15 seconds - setTimeout(() => { - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - delete event.status - } - - return event - }) - ) - }, 15000) - } - - publish() + }) return true } diff --git a/src/pages/settings/profile.tsx b/src/pages/settings/profile.tsx index 52f69f7..907d0f0 100644 --- a/src/pages/settings/profile.tsx +++ b/src/pages/settings/profile.tsx @@ -1,6 +1,6 @@ import { InputField } from 'components/Inputs' import { ProfileQRButtonWithPopUp } from 'components/ProfileSection' -import { useAppDispatch, useAppSelector } from 'hooks' +import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks' import { kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' @@ -14,7 +14,6 @@ import { profileFromEvent, serializeProfile } from '@nostr-dev-kit/ndk' -import { RelayController } from 'controllers' import { LoadingSpinner } from 'components/LoadingSpinner' import { setUser } from 'store/reducers/user' import placeholderMod from '../../assets/img/DEGMods Placeholder Img.png' @@ -43,6 +42,7 @@ const defaultFormState: FormState = { export const ProfileSettings = () => { const dispatch = useAppDispatch() const userState = useAppSelector((state) => state.user) + const { ndk, publish } = useNDKContext() const [isPublishing, setIsPublishing] = useState(false) const [formState, setFormState] = useState(defaultFormState) @@ -163,9 +163,8 @@ export const ProfileSettings = () => { return } - const publishedOnRelays = await RelayController.getInstance().publish( - signedEvent as Event - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { diff --git a/src/pages/settings/relay.tsx b/src/pages/settings/relay.tsx index ee79d94..90beed2 100644 --- a/src/pages/settings/relay.tsx +++ b/src/pages/settings/relay.tsx @@ -1,12 +1,8 @@ -import { NDKRelayList } from '@nostr-dev-kit/ndk' +import { NDKEvent, NDKRelayList, NDKRelayStatus } from '@nostr-dev-kit/ndk' import { InputField } from 'components/Inputs' import { LoadingSpinner } from 'components/LoadingSpinner' -import { - MetadataController, - RelayController, - UserRelaysType -} from 'controllers' -import { useAppSelector, useDidMount } from 'hooks' +import { MetadataController, UserRelaysType } from 'controllers' +import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' @@ -16,6 +12,7 @@ const READ_MARKER = 'read' const WRITE_MARKER = 'write' export const RelaySettings = () => { + const { ndk, publish } = useNDKContext() const userState = useAppSelector((state) => state.user) const [ndkRelayList, setNDKRelayList] = useState(null) const [isPublishing, setIsPublishing] = useState(false) @@ -78,11 +75,8 @@ export const RelaySettings = () => { return } - const publishedOnRelays = - await RelayController.getInstance().publishOnRelays( - signedEvent, - ndkRelayList.writeRelayUrls - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { @@ -140,11 +134,8 @@ export const RelaySettings = () => { return } - const publishedOnRelays = - await RelayController.getInstance().publishOnRelays( - signedEvent, - ndkRelayList.writeRelayUrls - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { @@ -214,11 +205,8 @@ export const RelaySettings = () => { return } - const publishedOnRelays = - await RelayController.getInstance().publishOnRelays( - signedEvent, - ndkRelayList.writeRelayUrls - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { @@ -382,17 +370,29 @@ const RelayListItem = ({ changeRelayType }: RelayItemProps) => { const [isConnected, setIsConnected] = useState(false) + const { ndk } = useNDKContext() useDidMount(() => { - RelayController.getInstance() - .connectRelay(relayUrl) - .then((relay) => { - if (relay && relay.connected) { - setIsConnected(true) - } else { - setIsConnected(false) - } - }) + const ndkPool = ndk.pool + + ndkPool.on('relay:connect', (relay) => { + if (relay.url === relayUrl) { + setIsConnected(true) + } + }) + + ndkPool.on('relay:disconnect', (relay) => { + if (relay.url === relayUrl) { + setIsConnected(false) + } + }) + + const relay = ndkPool.relays.get(relayUrl) + if (relay && relay.status >= NDKRelayStatus.CONNECTED) { + setIsConnected(true) + } else { + setIsConnected(false) + } }) return ( diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index f3711b0..130d023 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -9,9 +9,8 @@ import { UnsignedEvent } from 'nostr-tools' import { toast } from 'react-toastify' -import { RelayController } from '../controllers' import { log, LogType } from './utils' -import { NDKEvent } from '@nostr-dev-kit/ndk' +import NDK, { NDKEvent } from '@nostr-dev-kit/ndk' /** * Get the current time in seconds since the Unix epoch (January 1, 1970). @@ -123,7 +122,11 @@ export const extractZapAmount = (event: Event): number => { * @param unsignedEvent - The event object which needs to be signed before publishing. * @returns - A promise that resolves to boolean indicating whether the event was successfully signed and published */ -export const signAndPublish = async (unsignedEvent: UnsignedEvent) => { +export const signAndPublish = async ( + unsignedEvent: UnsignedEvent, + ndk: NDK, + publish: (event: NDKEvent) => Promise +) => { // Sign the event. This returns a signed event or null if signing fails. const signedEvent = await window.nostr ?.signEvent(unsignedEvent) @@ -138,11 +141,10 @@ export const signAndPublish = async (unsignedEvent: UnsignedEvent) => { // If the event couldn't be signed, exit the function and return null. if (!signedEvent) return false - // Publish the signed event to the relays using the RelayController. + // Publish the signed event to the relays. // This returns an array of relay URLs where the event was successfully published. - const publishedOnRelays = await RelayController.getInstance().publish( - signedEvent as Event - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing to the relays failed if (publishedOnRelays.length === 0) { @@ -170,7 +172,9 @@ export const signAndPublish = async (unsignedEvent: UnsignedEvent) => { */ export const sendDMUsingRandomKey = async ( message: string, - receiver: string + receiver: string, + ndk: NDK, + publish: (event: NDKEvent) => Promise ) => { // Generate a random secret key for encrypting the message const secretKey = generateSecretKey() @@ -201,11 +205,8 @@ export const sendDMUsingRandomKey = async ( // Finalize and sign the event using the generated secret key const signedEvent = finalizeEvent(unsignedEvent, secretKey) - // Publish the signed event (the encrypted DM) to the relays - const publishedOnRelays = await RelayController.getInstance().publishDM( - signedEvent, - receiver - ) + const ndkEvent = new NDKEvent(ndk, signedEvent) + const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing to the relays failed if (publishedOnRelays.length === 0) { From b0ebe7154a038b33ea330ee2f3b9119bbbfdc52e Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 21 Oct 2024 17:51:00 +0500 Subject: [PATCH 09/13] fix: removed metadata controller and used ndkContext --- src/components/ProfileSection.tsx | 3 +- src/components/Zap.tsx | 59 +++++++- src/contexts/NDKContext.tsx | 124 ++++++++++++++++- src/controllers/index.ts | 1 - src/controllers/metadata.ts | 217 ------------------------------ src/controllers/zap.ts | 18 ++- src/hooks/useComments.ts | 3 +- src/hooks/useMuteLists.ts | 19 +-- src/hooks/useNSFWList.ts | 7 +- src/hooks/useReactions.ts | 2 +- src/layout/header.tsx | 5 +- src/pages/home.tsx | 8 +- src/pages/mod/index.tsx | 7 +- src/pages/settings/index.tsx | 16 +-- src/pages/settings/relay.tsx | 21 ++- src/pages/submitMod.tsx | 4 +- src/types/user.ts | 6 + 17 files changed, 229 insertions(+), 291 deletions(-) delete mode 100644 src/controllers/metadata.ts diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 972c8ef..298ae9d 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -4,13 +4,12 @@ import { QRCodeSVG } from 'qrcode.react' import { useState } from 'react' import { Link } from 'react-router-dom' import { toast } from 'react-toastify' -import { UserRelaysType } from '../controllers' import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import { appRoutes, getProfilePageRoute } from '../routes' import '../styles/author.css' import '../styles/innerPage.css' import '../styles/socialPosts.css' -import { UserProfile } from '../types' +import { UserProfile, UserRelaysType } from '../types' import { copyTextToClipboard, hexToNpub, diff --git a/src/components/Zap.tsx b/src/components/Zap.tsx index e7c7005..80ec0fa 100644 --- a/src/components/Zap.tsx +++ b/src/components/Zap.tsx @@ -1,3 +1,4 @@ +import { getRelayListForUser } from '@nostr-dev-kit/ndk' import { QRCodeSVG } from 'qrcode.react' import React, { Dispatch, @@ -9,7 +10,7 @@ import React, { } from 'react' import Countdown, { CountdownRenderProps } from 'react-countdown' import { toast } from 'react-toastify' -import { MetadataController, ZapController } from '../controllers' +import { ZapController } from '../controllers' import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import '../styles/popup.css' import { PaymentRequest, UserProfile } from '../types' @@ -251,7 +252,7 @@ export const ZapPopUp = ({ setHasZapped, handleClose }: ZapPopUpProps) => { - const { findMetadata } = useNDKContext() + const { ndk, findMetadata } = useNDKContext() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [amount, setAmount] = useState(0) @@ -300,6 +301,20 @@ export const ZapPopUp = ({ return null } + // Find the receiver's read relays. + const receiverRelays = await getRelayListForUser(receiver, ndk) + .then((ndkRelayList) => { + if (ndkRelayList) return ndkRelayList.readRelayUrls + return [] // Return an empty array if ndkRelayList is undefined + }) + .catch((err) => { + console.error( + `An error occurred in getting zap receiver's read relays`, + err + ) + return [] as string[] + }) + const zapController = ZapController.getInstance() setLoadingSpinnerDesc('Creating zap request') @@ -308,6 +323,7 @@ export const ZapPopUp = ({ receiverMetadata.lud16, amount, receiverMetadata.pubkey as string, + receiverRelays, userHexKey, message, eventId, @@ -482,7 +498,7 @@ export const ZapSplit = ({ setHasZapped, handleClose }: ZapSplitProps) => { - const { findMetadata } = useNDKContext() + const { ndk, findMetadata } = useNDKContext() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [amount, setAmount] = useState(0) @@ -502,8 +518,8 @@ export const ZapSplit = ({ setAuthor(res) }) - const metadataController = await MetadataController.getInstance() - findMetadata(metadataController.adminNpubs[0]).then((res) => { + const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') + findMetadata(adminNpubs[0]).then((res) => { setAdmin(res) }) }) @@ -557,12 +573,30 @@ export const ZapSplit = ({ const invoices = new Map() if (authorShare > 0 && author?.pubkey && author?.lud16) { + // Find the receiver's read relays. + const authorRelays = await getRelayListForUser( + author.pubkey as string, + ndk + ) + .then((ndkRelayList) => { + if (ndkRelayList) return ndkRelayList.readRelayUrls + return [] // Return an empty array if ndkRelayList is undefined + }) + .catch((err) => { + console.error( + `An error occurred in getting zap receiver's read relays`, + err + ) + return [] as string[] + }) + setLoadingSpinnerDesc('Generating invoice for author') const invoice = await zapController .getLightningPaymentRequest( author.lud16, authorShare, author.pubkey as string, + authorRelays, userHexKey, message, eventId, @@ -579,12 +613,27 @@ export const ZapSplit = ({ } if (adminShare > 0 && admin?.pubkey && admin?.lud16) { + // Find the receiver's read relays. + const adminRelays = await getRelayListForUser(admin.pubkey as string, ndk) + .then((ndkRelayList) => { + if (ndkRelayList) return ndkRelayList.readRelayUrls + return [] // Return an empty array if ndkRelayList is undefined + }) + .catch((err) => { + console.error( + `An error occurred in getting zap receiver's read relays`, + err + ) + return [] as string[] + }) + setLoadingSpinnerDesc('Generating invoice for site owner') const invoice = await zapController .getLightningPaymentRequest( admin.lud16, adminShare, admin.pubkey as string, + adminRelays, userHexKey, message, eventId, diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 0e92db9..33e35da 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -3,6 +3,7 @@ import NDK, { NDKEvent, NDKFilter, NDKKind, + NDKList, NDKRelaySet, NDKSubscriptionCacheUsage, NDKUser, @@ -10,11 +11,10 @@ import NDK, { } 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 { UserRelaysType } from 'controllers' import { Dexie } from 'dexie' import { createContext, ReactNode, useEffect, useMemo } from 'react' import { toast } from 'react-toastify' -import { ModDetails, UserProfile } from 'types' +import { ModDetails, MuteLists, UserProfile, UserRelaysType } from 'types' import { constructModListFromEvents, hexToNpub, @@ -57,6 +57,11 @@ interface NDKContextType { hasZapped: boolean }> publish: (event: NDKEvent) => Promise + getNSFWList: () => Promise + getMuteLists: (pubkey?: string) => Promise<{ + admin: MuteLists + user: MuteLists + }> } // Create the context with an initial value of `null` @@ -368,6 +373,117 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { }) } + /** + * Retrieves a list of NSFW (Not Safe For Work) posts that were not specified as NSFW by post author but marked as NSFW by admin. + * + * @returns {Promise} - A promise that resolves to an array of NSFW post identifiers (e.g., URLs or IDs). + */ + const getNSFWList = async (): Promise => { + // Initialize an array to store the NSFW post identifiers + const nsfwPosts: string[] = [] + + const reportingNpub = import.meta.env.VITE_REPORTING_NPUB + + // Convert the public key (npub) to a hexadecimal format + const hexKey = npubToHex(reportingNpub) + + // If the conversion is successful and we have a hexKey + if (hexKey) { + // Fetch the event that contains the NSFW list + const nsfwListEvent = await fetchEvent({ + kinds: [NDKKind.ArticleCurationSet], + authors: [hexKey], + '#d': ['nsfw'] + }) + + if (nsfwListEvent) { + // Convert the event data to an NDKList, which is a structured list format + const list = NDKList.from(nsfwListEvent) + + // Iterate through the items in the list + list.items.forEach((item) => { + if (item[0] === 'a') { + // Add the identifier of the NSFW post to the nsfwPosts array + nsfwPosts.push(item[1]) + } + }) + } + } + + // Return the array of NSFW post identifiers + return nsfwPosts + } + + const getMuteLists = async ( + pubkey?: string + ): Promise<{ + admin: MuteLists + user: MuteLists + }> => { + const adminMutedAuthors = new Set() + const adminMutedPosts = new Set() + + const reportingNpub = import.meta.env.VITE_REPORTING_NPUB + + const adminHexKey = npubToHex(reportingNpub) + + if (adminHexKey) { + const muteListEvent = await fetchEvent({ + kinds: [NDKKind.MuteList], + authors: [adminHexKey] + }) + + if (muteListEvent) { + const list = NDKList.from(muteListEvent) + + list.items.forEach((item) => { + if (item[0] === 'p') { + adminMutedAuthors.add(item[1]) + } else if (item[0] === 'a') { + adminMutedPosts.add(item[1]) + } + }) + } + } + + const userMutedAuthors = new Set() + const userMutedPosts = new Set() + + if (pubkey) { + const userHexKey = npubToHex(pubkey) + + if (userHexKey) { + const muteListEvent = await fetchEvent({ + kinds: [NDKKind.MuteList], + authors: [userHexKey] + }) + + if (muteListEvent) { + const list = NDKList.from(muteListEvent) + + list.items.forEach((item) => { + if (item[0] === 'p') { + userMutedAuthors.add(item[1]) + } else if (item[0] === 'a') { + userMutedPosts.add(item[1]) + } + }) + } + } + } + + return { + admin: { + authors: Array.from(adminMutedAuthors), + replaceableEvents: Array.from(adminMutedPosts) + }, + user: { + authors: Array.from(userMutedAuthors), + replaceableEvents: Array.from(userMutedPosts) + } + } + } + return ( { fetchEventFromUserRelays, findMetadata, getTotalZapAmount, - publish + publish, + getNSFWList, + getMuteLists }} > {children} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b028b93..4e84779 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1 @@ -export * from './metadata' export * from './zap' diff --git a/src/controllers/metadata.ts b/src/controllers/metadata.ts deleted file mode 100644 index 98967d4..0000000 --- a/src/controllers/metadata.ts +++ /dev/null @@ -1,217 +0,0 @@ -import NDK, { getRelayListForUser, NDKList } from '@nostr-dev-kit/ndk' -import { kinds } from 'nostr-tools' -import { MuteLists } from '../types' -import { log, LogType, npubToHex, timeout } from '../utils' - -export enum UserRelaysType { - Read = 'readRelayUrls', - Write = 'writeRelayUrls', - Both = 'bothRelayUrls' -} - -/** - * Singleton class to manage metadata operations using NDK. - */ -export class MetadataController { - private static instance: MetadataController - private ndk: NDK - public adminNpubs: string[] - public adminRelays = new Set() - public reportingNpub: string - - private constructor() { - this.ndk = new NDK({ - explicitRelayUrls: [ - 'wss://user.kindpag.es', - 'wss://purplepag.es', - 'wss://relay.damus.io/', - import.meta.env.VITE_APP_RELAY - ] - }) - - this.ndk - .connect() - .then(() => { - console.log('NDK connected') - }) - .catch((err) => { - console.log('error in ndk connection', err) - }) - - this.adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') - this.reportingNpub = import.meta.env.VITE_REPORTING_NPUB - } - - private setAdminRelays = async () => { - const promises = this.adminNpubs.map((npub) => { - const hexKey = npubToHex(npub) - if (!hexKey) return null - - return getRelayListForUser(hexKey, this.ndk) - .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) - } - - /** - * Provides the singleton instance of MetadataController. - * - * @returns The singleton instance of MetadataController. - */ - public static async getInstance(): Promise { - if (!MetadataController.instance) { - MetadataController.instance = new MetadataController() - - await MetadataController.instance.setAdminRelays() - } - return MetadataController.instance - } - - public findUserRelays = async ( - hexKey: string, - userRelaysType: UserRelaysType = UserRelaysType.Both - ): Promise => { - log(true, LogType.Info, `ℹ Finding user's relays`, hexKey, userRelaysType) - - const ndkRelayListPromise = getRelayListForUser(hexKey, this.ndk) - - // Use Promise.race to either get the NDKRelayList instance or handle the timeout - return await Promise.race([ - ndkRelayListPromise, - timeout() // Custom timeout function that rejects after a specified time - ]) - .then((ndkRelayList) => { - if (ndkRelayList) return ndkRelayList[userRelaysType] - return [] // Return an empty array if ndkRelayList is undefined - }) - .catch((err) => { - log(true, LogType.Error, err) - return [] // Return an empty array if an error occurs - }) - } - - public getNDKRelayList = async (hexKey: string) => - getRelayListForUser(hexKey, this.ndk) - - public getMuteLists = async ( - pubkey?: string - ): Promise<{ - admin: MuteLists - user: MuteLists - }> => { - const adminMutedAuthors = new Set() - const adminMutedPosts = new Set() - - const adminHexKey = npubToHex(this.reportingNpub) - - if (adminHexKey) { - const muteListEvent = await this.ndk.fetchEvent({ - kinds: [kinds.Mutelist], - authors: [adminHexKey] - }) - - if (muteListEvent) { - const list = NDKList.from(muteListEvent) - - list.items.forEach((item) => { - if (item[0] === 'p') { - adminMutedAuthors.add(item[1]) - } else if (item[0] === 'a') { - adminMutedPosts.add(item[1]) - } - }) - } - } - - const userMutedAuthors = new Set() - const userMutedPosts = new Set() - - if (pubkey) { - const userHexKey = npubToHex(pubkey) - - if (userHexKey) { - const muteListEvent = await this.ndk.fetchEvent({ - kinds: [kinds.Mutelist], - authors: [userHexKey] - }) - - if (muteListEvent) { - const list = NDKList.from(muteListEvent) - - list.items.forEach((item) => { - if (item[0] === 'p') { - userMutedAuthors.add(item[1]) - } else if (item[0] === 'a') { - userMutedPosts.add(item[1]) - } - }) - } - } - } - - return { - admin: { - authors: Array.from(adminMutedAuthors), - replaceableEvents: Array.from(adminMutedPosts) - }, - user: { - authors: Array.from(userMutedAuthors), - replaceableEvents: Array.from(userMutedPosts) - } - } - } - - /** - * Retrieves a list of NSFW (Not Safe For Work) posts that were not specified as NSFW by post author but marked as NSFW by admin. - * - * @returns {Promise} - A promise that resolves to an array of NSFW post identifiers (e.g., URLs or IDs). - */ - public getNSFWList = async (): Promise => { - // Initialize an array to store the NSFW post identifiers - const nsfwPosts: string[] = [] - - // Convert the public key (npub) to a hexadecimal format - const hexKey = npubToHex(this.reportingNpub) - - // If the conversion is successful and we have a hexKey - if (hexKey) { - // Fetch the event that contains the NSFW list - const nsfwListEvent = await this.ndk.fetchEvent({ - kinds: [kinds.Curationsets], - authors: [hexKey], - '#d': ['nsfw'] - }) - - if (nsfwListEvent) { - // Convert the event data to an NDKList, which is a structured list format - const list = NDKList.from(nsfwListEvent) - - // Iterate through the items in the list - list.items.forEach((item) => { - if (item[0] === 'a') { - // Add the identifier of the NSFW post to the nsfwPosts array - nsfwPosts.push(item[1]) - } - }) - } - } - - // Return the array of NSFW post identifiers - return nsfwPosts - } -} diff --git a/src/controllers/zap.ts b/src/controllers/zap.ts index 8b74dd7..0ff300a 100644 --- a/src/controllers/zap.ts +++ b/src/controllers/zap.ts @@ -17,7 +17,6 @@ import { ZapRequest } from '../types' import { log, LogType, npubToHex } from '../utils' -import { MetadataController, UserRelaysType } from './metadata' /** * Singleton class to manage zap related operations. @@ -48,6 +47,7 @@ export class ZapController { * @param lud16 - LUD-16 of the recipient. * @param amount - payment amount (will be multiplied by 1000 to represent sats). * @param recipientPubKey - pubKey of the recipient. + * @param recipientRelays - relays on which zap receipt will be published. * @param senderPubkey - pubKey of of the sender. * @param content - optional content (comment). * @param eventId - event id, if zapping an event. @@ -59,6 +59,7 @@ export class ZapController { lud16: string, amount: number, recipientPubKey: string, + recipientRelays: string[], senderPubkey: string, content?: string, eventId?: string, @@ -88,6 +89,7 @@ export class ZapController { amount, content, recipientPubKey, + recipientRelays, senderPubkey, eventId, aTag @@ -273,6 +275,7 @@ export class ZapController { * @param amount - request amount (sats). * @param content - comment. * @param recipientPubKey - pubKey of the recipient. + * @param recipientRelays - relays on which zap receipt will be published. * @param senderPubkey - pubKey of of the sender. * @param eventId - event id, if zapping an event. * @param aTag - value of `a` tag. @@ -282,6 +285,7 @@ export class ZapController { amount: number, content = '', recipientPubKey: string, + recipientRelays: string[], senderPubkey: string, eventId?: string, aTag?: string @@ -290,21 +294,15 @@ export class ZapController { if (!recipientHexKey) throw 'Invalid recipient pubKey.' - const metadataController = await MetadataController.getInstance() - const receiverReadRelays = await metadataController.findUserRelays( - recipientHexKey, - UserRelaysType.Read - ) - - if (!receiverReadRelays.includes(this.appRelay)) { - receiverReadRelays.push(this.appRelay) + if (!recipientRelays.includes(this.appRelay)) { + recipientRelays.push(this.appRelay) } const zapRequest: ZapRequest = { kind: kinds.ZapRequest, content, tags: [ - ['relays', ...receiverReadRelays], + ['relays', ...recipientRelays], ['amount', `${amount}`], ['p', recipientHexKey] ], diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts index 5dd120a..6be5932 100644 --- a/src/hooks/useComments.ts +++ b/src/hooks/useComments.ts @@ -6,9 +6,8 @@ import { NDKSubscription, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk' -import { UserRelaysType } from 'controllers' import { useEffect, useState } from 'react' -import { CommentEvent, ModDetails } from 'types' +import { CommentEvent, ModDetails, UserRelaysType } from 'types' import { log, LogType } from 'utils' import { useNDKContext } from './useNDKContext' diff --git a/src/hooks/useMuteLists.ts b/src/hooks/useMuteLists.ts index 558bcb7..803da9e 100644 --- a/src/hooks/useMuteLists.ts +++ b/src/hooks/useMuteLists.ts @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react' import { MuteLists } from 'types' import { useAppSelector } from './redux' -import { MetadataController } from 'controllers' +import { useNDKContext } from './useNDKContext' export const useMuteLists = () => { + const { getMuteLists } = useNDKContext() const [muteLists, setMuteLists] = useState<{ admin: MuteLists user: MuteLists @@ -21,17 +22,11 @@ export const useMuteLists = () => { const userState = useAppSelector((state) => state.user) useEffect(() => { - const getMuteLists = async () => { - const pubkey = userState.user?.pubkey as string | undefined - - const metadataController = await MetadataController.getInstance() - metadataController.getMuteLists(pubkey).then((lists) => { - setMuteLists(lists) - }) - } - - getMuteLists() - }, [userState]) + const pubkey = userState.user?.pubkey as string | undefined + getMuteLists(pubkey).then((lists) => { + setMuteLists(lists) + }) + }, [userState, getMuteLists]) return muteLists } diff --git a/src/hooks/useNSFWList.ts b/src/hooks/useNSFWList.ts index 0712da6..9da98db 100644 --- a/src/hooks/useNSFWList.ts +++ b/src/hooks/useNSFWList.ts @@ -1,14 +1,13 @@ -import { MetadataController } from 'controllers' import { useState } from 'react' import { useDidMount } from './useDidMount' +import { useNDKContext } from './useNDKContext' export const useNSFWList = () => { + const { getNSFWList } = useNDKContext() const [nsfwList, setNSFWList] = useState([]) useDidMount(async () => { - const metadataController = await MetadataController.getInstance() - - metadataController.getNSFWList().then((list) => { + getNSFWList().then((list) => { setNSFWList(list) }) }) diff --git a/src/hooks/useReactions.ts b/src/hooks/useReactions.ts index 60406c2..574c3eb 100644 --- a/src/hooks/useReactions.ts +++ b/src/hooks/useReactions.ts @@ -1,10 +1,10 @@ import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk' import { REACTIONS } from 'constants.ts' -import { UserRelaysType } from 'controllers' import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useMemo, useState } from 'react' import { toast } from 'react-toastify' +import { UserRelaysType } from 'types' import { abbreviateNumber, log, LogType, now } from 'utils' type UseReactionsParams = { diff --git a/src/layout/header.tsx b/src/layout/header.tsx index bc4c2b8..c6c4498 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -6,7 +6,6 @@ import React, { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Banner } from '../components/Banner' import { ZapPopUp } from '../components/Zap' -import { MetadataController } from '../controllers' import { useAppDispatch, useAppSelector, @@ -261,8 +260,8 @@ const TipButtonWithDialog = React.memo(() => { const [isOpen, setIsOpen] = useState(false) useDidMount(async () => { - const metadataController = await MetadataController.getInstance() - setAdminNpub(metadataController.adminNpubs[0]) + const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') + setAdminNpub(adminNpubs[0]) }) return ( diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 79e5a15..5c15e25 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -152,7 +152,7 @@ const SlideContent = ({ naddr }: SlideContentProps) => { useDidMount(() => { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) - const { identifier, kind, pubkey, relays = [] } = decoded.data + const { identifier, kind, pubkey } = decoded.data const ndkFilter: NDKFilter = { '#a': [identifier], @@ -160,7 +160,7 @@ const SlideContent = ({ naddr }: SlideContentProps) => { kinds: [kind] } - fetchEvent(ndkFilter, relays) + fetchEvent(ndkFilter) .then((ndkEvent) => { if (ndkEvent) { const extracted = extractModData(ndkEvent) @@ -225,7 +225,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => { useDidMount(() => { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) - const { identifier, kind, pubkey, relays = [] } = decoded.data + const { identifier, kind, pubkey } = decoded.data const ndkFilter: NDKFilter = { '#a': [identifier], @@ -233,7 +233,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => { kinds: [kind] } - fetchEvent(ndkFilter, relays) + fetchEvent(ndkFilter) .then((ndkEvent) => { if (ndkEvent) { const extracted = extractModData(ndkEvent) diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index dd678fd..a10acc8 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -11,7 +11,6 @@ import { toast } from 'react-toastify' import { BlogCard } from '../../components/BlogCard' import { LoadingSpinner } from '../../components/LoadingSpinner' import { ProfileSection } from '../../components/ProfileSection' -import { MetadataController, UserRelaysType } from '../../controllers' import { useAppSelector, useDidMount, useNDKContext } from '../../hooks' import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import '../../styles/comments.css' @@ -24,7 +23,7 @@ import '../../styles/styles.css' import '../../styles/tabs.css' import '../../styles/tags.css' import '../../styles/write.css' -import { DownloadUrl, ModDetails } from '../../types' +import { DownloadUrl, ModDetails, UserRelaysType } from '../../types' import { abbreviateNumber, copyTextToClipboard, @@ -708,8 +707,8 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { return } - const metadataController = await MetadataController.getInstance() - const reportingPubkey = npubToHex(metadataController.reportingNpub) + const reportingNpub = import.meta.env.VITE_REPORTING_NPUB + const reportingPubkey = npubToHex(reportingNpub) if (reportingPubkey === hexPubkey) { setLoadingSpinnerDesc(`Finding user's mute list`) diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index 60aa2c8..4c0f618 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -1,5 +1,4 @@ import { AdminSVG, PreferenceSVG, ProfileSVG, RelaySVG } from 'components/SVGs' -import { MetadataController } from 'controllers' import { useAppSelector } from 'hooks' import { logout } from 'nostr-login' import { useEffect, useState } from 'react' @@ -57,15 +56,12 @@ const SettingTabs = () => { const userState = useAppSelector((state) => state.user) useEffect(() => { - MetadataController.getInstance().then((controller) => { - if (userState.auth && userState.user?.npub) { - setIsAdmin( - controller.adminNpubs.includes(userState.user.npub as string) - ) - } else { - setIsAdmin(false) - } - }) + const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') + if (userState.auth && userState.user?.npub) { + setIsAdmin(adminNpubs.includes(userState.user.npub as string)) + } else { + setIsAdmin(false) + } }, [userState]) const handleSignOut = () => { diff --git a/src/pages/settings/relay.tsx b/src/pages/settings/relay.tsx index 90beed2..1a32d28 100644 --- a/src/pages/settings/relay.tsx +++ b/src/pages/settings/relay.tsx @@ -1,11 +1,16 @@ -import { NDKEvent, NDKRelayList, NDKRelayStatus } from '@nostr-dev-kit/ndk' +import { + getRelayListForUser, + NDKEvent, + NDKRelayList, + NDKRelayStatus +} from '@nostr-dev-kit/ndk' import { InputField } from 'components/Inputs' import { LoadingSpinner } from 'components/LoadingSpinner' -import { MetadataController, UserRelaysType } from 'controllers' import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' +import { UserRelaysType } from 'types' import { log, LogType, normalizeWebSocketURL, now } from 'utils' const READ_MARKER = 'read' @@ -20,10 +25,8 @@ export const RelaySettings = () => { const [inputValue, setInputValue] = useState('') useEffect(() => { - const fetchRelayList = async (pubkey: string) => { - const metadataController = await MetadataController.getInstance() - metadataController - .getNDKRelayList(pubkey) + if (userState.auth && userState.user?.pubkey) { + getRelayListForUser(userState.user.pubkey as string, ndk) .then((res) => { setNDKRelayList(res) }) @@ -35,14 +38,10 @@ export const RelaySettings = () => { ) setNDKRelayList(null) }) - } - - if (userState.auth && userState.user?.pubkey) { - fetchRelayList(userState.user.pubkey as string) } else { setNDKRelayList(null) } - }, [userState]) + }, [userState, ndk]) const handleAdd = async (relayUrl: string) => { if (!ndkRelayList) return diff --git a/src/pages/submitMod.tsx b/src/pages/submitMod.tsx index 226b896..ba79874 100644 --- a/src/pages/submitMod.tsx +++ b/src/pages/submitMod.tsx @@ -29,7 +29,7 @@ export const SubmitModPage = () => { useDidMount(async () => { if (naddr) { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) - const { identifier, kind, pubkey, relays = [] } = decoded.data + const { identifier, kind, pubkey } = decoded.data const filter: NDKFilter = { '#a': [identifier], @@ -39,7 +39,7 @@ export const SubmitModPage = () => { setIsFetching(true) - fetchEvent(filter, relays) + fetchEvent(filter) .then((event) => { if (event) { const extracted = extractModData(event) diff --git a/src/types/user.ts b/src/types/user.ts index 551bd1f..059ba84 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,3 +1,9 @@ import { NDKUserProfile } from '@nostr-dev-kit/ndk' export type UserProfile = NDKUserProfile | null + +export enum UserRelaysType { + Read = 'readRelayUrls', + Write = 'writeRelayUrls', + Both = 'bothRelayUrls' +} From 545e6e6ec0d825cab40d7eb9db3df96ab3ab9f4c Mon Sep 17 00:00:00 2001 From: daniyal Date: Mon, 21 Oct 2024 17:52:11 +0500 Subject: [PATCH 10/13] chore: quick fix --- src/components/Zap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Zap.tsx b/src/components/Zap.tsx index 80ec0fa..c393947 100644 --- a/src/components/Zap.tsx +++ b/src/components/Zap.tsx @@ -336,7 +336,7 @@ export const ZapPopUp = ({ .finally(() => { setIsLoading(false) }) - }, [amount, message, userState, receiver, eventId, aTag]) + }, [amount, message, userState, receiver, eventId, aTag, ndk, findMetadata]) const handleGenerateQRCode = async () => { const pr = await generatePaymentRequest() From 4bf18f158422f26b9290e341abad54c01602b03f Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 15:21:09 +0200 Subject: [PATCH 11/13] fix: pagination scroll into view Fixes #73 --- src/pages/game.tsx | 11 ++++++++--- src/pages/games.tsx | 8 +++++++- src/pages/mods.tsx | 9 ++++++++- src/pages/search.tsx | 23 ++++++++++++++++++++--- src/utils/utils.ts | 11 +++++++++++ 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/pages/game.tsx b/src/pages/game.tsx index 2e53be3..7b94d74 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -14,7 +14,7 @@ import { useNDKContext, useNSFWList } from 'hooks' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { FilterOptions, @@ -23,9 +23,10 @@ import { NSFWFilter, SortBy } from 'types' -import { extractModData, isModDataComplete } from 'utils' +import { extractModData, isModDataComplete, scrollIntoView } from 'utils' export const GamePage = () => { + const scrollTargetRef = useRef(null) const params = useParams() const { name: gameName } = params const { ndk } = useNDKContext() @@ -61,6 +62,7 @@ export const GamePage = () => { const handlePageChange = (page: number) => { if (page >= 1 && page <= totalPages) { + scrollIntoView(scrollTargetRef.current) setCurrentPage(page) } } @@ -102,7 +104,10 @@ export const GamePage = () => { <>
-
+
diff --git a/src/pages/games.tsx b/src/pages/games.tsx index e8b8af5..08d576e 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -8,8 +8,10 @@ import '../styles/search.css' import '../styles/styles.css' import { createSearchParams, useNavigate } from 'react-router-dom' import { appRoutes } from 'routes' +import { scrollIntoView } from 'utils' export const GamesPage = () => { + const scrollTargetRef = useRef(null) const navigate = useNavigate() const { fetchMods } = useNDKContext() const searchTermRef = useRef(null) @@ -63,6 +65,7 @@ export const GamesPage = () => { const handlePageChange = (page: number) => { if (page >= 1 && page <= totalPages) { + scrollIntoView(scrollTargetRef.current) setCurrentPage(page) } } @@ -88,7 +91,10 @@ export const GamesPage = () => { return (
-
+
diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index 444eb08..bc217aa 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -24,8 +24,10 @@ import { NSFWFilter, SortBy } from '../types' +import { scrollIntoView } from 'utils' export const ModsPage = () => { + const scrollTargetRef = useRef(null) const { fetchMods } = useNDKContext() const [isFetching, setIsFetching] = useState(false) const [mods, setMods] = useState([]) @@ -66,6 +68,7 @@ export const ModsPage = () => { .then((res) => { setMods(res) setPage((prev) => prev + 1) + scrollIntoView(scrollTargetRef.current) }) .finally(() => { setIsFetching(false) @@ -84,6 +87,7 @@ export const ModsPage = () => { .then((res) => { setMods(res) setPage((prev) => prev - 1) + scrollIntoView(scrollTargetRef.current) }) .finally(() => { setIsFetching(false) @@ -103,7 +107,10 @@ export const ModsPage = () => { {isFetching && }
-
+
{ + const scrollTargetRef = useRef(null) const [searchParams] = useSearchParams() const muteLists = useMuteLists() @@ -88,7 +95,10 @@ export const SearchPage = () => { return (
-
+
@@ -141,6 +151,7 @@ export const SearchPage = () => { filterOptions={filterOptions} muteLists={muteLists} nsfwList={nsfwList} + el={scrollTargetRef.current} /> )} {searchKind === SearchKindEnum.Users && ( @@ -263,13 +274,15 @@ type ModsResultProps = { user: MuteLists } nsfwList: string[] + el: HTMLElement | null } const ModsResult = ({ filterOptions, searchTerm, muteLists, - nsfwList + nsfwList, + el }: ModsResultProps) => { const { ndk } = useNDKContext() const [mods, setMods] = useState([]) @@ -305,7 +318,9 @@ const ModsResult = ({ }, [ndk]) useEffect(() => { + scrollIntoView(el) setPage(1) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchTerm]) const filteredMods = useMemo(() => { @@ -334,10 +349,12 @@ const ModsResult = ({ ) const handleNext = () => { + scrollIntoView(el) setPage((prev) => prev + 1) } const handlePrev = () => { + scrollIntoView(el) setPage((prev) => prev - 1) } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b321c91..6c904e0 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -135,3 +135,14 @@ export const handleModImageError = ( ) => { e.currentTarget.src = import.meta.env.VITE_FALLBACK_MOD_IMAGE } + +export const scrollIntoView = (el: HTMLElement | null) => { + if (el) { + setTimeout(() => { + el.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }) + }, 100) + } +} From 2dd261161e20272f1413618f6614c12d31c06596 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 16:04:01 +0200 Subject: [PATCH 12/13] fix: add mod nsfw tag --- src/components/ModCard.tsx | 6 ++++++ src/styles/cardMod.css | 14 +++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index c14372f..6abd397 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -50,7 +50,13 @@ export const ModCard = React.memo((props: ModDetails) => { src={props.featuredImageUrl} onError={handleModImageError} className='cMMPicture' + alt={`featured image for mod ${props.title}`} /> + {props.nsfw && ( +
+

NSFW

+
+ )}

{props.title}

diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css index 7dae7cc..3a50b5c 100644 --- a/src/styles/cardMod.css +++ b/src/styles/cardMod.css @@ -96,6 +96,7 @@ -webkit-box-orient: vertical; overflow: hidden; -webkit-line-clamp: 2; + line-clamp: 2; font-size: 20px; line-height: 1.25; color: rgba(255, 255, 255, 0.75); @@ -107,6 +108,7 @@ -webkit-box-orient: vertical; overflow: hidden; -webkit-line-clamp: 2; + line-clamp: 2; color: rgba(255, 255, 255, 0.5); font-size: 15px; line-height: 1.5; @@ -119,11 +121,12 @@ justify-content: start; align-items: center; font-size: 14px; - background: rgba(255,255,255,0.05); + background: rgba(255, 255, 255, 0.05); display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; -webkit-line-clamp: 1; + line-clamp: 1; } .cMMFootReactions { @@ -145,8 +148,9 @@ } .IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW.IBMSMSMBSSTagsTagNSFWCard { - position: absolute; - bottom: 10px; - right: 10px; - backdrop-filter: blur(10px); + position: absolute; + bottom: 10px; + right: 10px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); } From 16d39c407dcd15857d76e2e9c3ba2f76f4d46dea Mon Sep 17 00:00:00 2001 From: freakoverse Date: Mon, 21 Oct 2024 14:09:24 +0000 Subject: [PATCH 13/13] Update src/styles/cardMod.css --- src/styles/cardMod.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css index 3a50b5c..3250d41 100644 --- a/src/styles/cardMod.css +++ b/src/styles/cardMod.css @@ -153,4 +153,5 @@ right: 10px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); + background: rgba(35, 35, 35, 0.85); }