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/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 e52e940..323d5a6 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -4,7 +4,6 @@ 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 { useAppSelector, useBodyScrollDisable, @@ -15,7 +14,7 @@ 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, @@ -27,6 +26,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 @@ -377,7 +377,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) @@ -450,9 +450,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/components/Zap.tsx b/src/components/Zap.tsx index e7c7005..c393947 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, @@ -320,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() @@ -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 0ec3771..33e35da 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -3,17 +3,18 @@ import NDK, { NDKEvent, NDKFilter, NDKKind, + NDKList, 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' -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, @@ -33,23 +34,34 @@ 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, + 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 + }> + publish: (event: NDKEvent) => Promise + getNSFWList: () => Promise + getMuteLists: (pubkey?: string) => Promise<{ + admin: MuteLists + user: MuteLists + }> } // Create the context with an initial value of `null` @@ -72,6 +84,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 +125,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { ], cacheAdapter: dexieAdapter }) + addAdminRelays(ndk) ndk.connect() @@ -110,33 +148,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 +163,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 +189,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 +213,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] } @@ -265,10 +234,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) => { @@ -285,8 +254,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 + }) } /** @@ -299,7 +282,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 ) => { @@ -329,6 +312,178 @@ 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 + } + } + + 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 [] + }) + } + + /** + * 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 ( { fetchEvent, fetchEventsFromUserRelays, fetchEventFromUserRelays, - findMetadata + findMetadata, + getTotalZapAmount, + publish, + getNSFWList, + getMuteLists }} > {children} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b7b89e4..4e84779 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1 @@ -export * from './metadata' -export * from './relay' 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/relay.ts b/src/controllers/relay.ts deleted file mode 100644 index 2fdd6ae..0000000 --- a/src/controllers/relay.ts +++ /dev/null @@ -1,561 +0,0 @@ -import { Event, Filter, kinds, nip57, Relay } from 'nostr-tools' -import { - extractZapAmount, - 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 - } - - /** - * 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, - 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/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/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/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 9ebc63f..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 { RelayController, 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 = { @@ -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/layout/header.tsx b/src/layout/header.tsx index faae13e..011b7cf 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, @@ -277,8 +276,8 @@ const TipButtonWithDialog = React.memo(() => { useBodyScrollDisable(isOpen) 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 ccf172b..8a39cc4 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, useBodyScrollDisable, @@ -29,7 +28,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, @@ -58,7 +57,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], @@ -66,7 +65,7 @@ export const ModPage = () => { kinds: [kind] } - fetchEvent(filter, relays) + fetchEvent(filter) .then((event) => { if (event) { const extracted = extractModData(event) @@ -217,7 +216,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) @@ -350,7 +349,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) } @@ -391,7 +390,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) } @@ -457,7 +456,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) } @@ -498,7 +497,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) } @@ -668,7 +667,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, @@ -715,8 +714,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`) @@ -767,7 +766,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 @@ -780,7 +779,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 3c92b25..f238524 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, @@ -53,6 +49,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, @@ -88,7 +85,8 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { created_at: now(), tags: [ ['e', modDetails.id], - ['a', modDetails.aTag] + ['a', modDetails.aTag], + ['p', modDetails.author] ] } @@ -111,28 +109,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) { @@ -145,36 +167,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 } @@ -502,22 +495,21 @@ 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() useBodyScrollDisable(isOpen) 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 e40a07c..996c8d2 100644 --- a/src/pages/mod/internal/zap/index.tsx +++ b/src/pages/mod/internal/zap/index.tsx @@ -1,6 +1,10 @@ import { ZapSplit } from 'components/Zap' -import { RelayController } from 'controllers' -import { useAppSelector, useBodyScrollDisable, useDidMount } from 'hooks' +import { + useAppSelector, + useBodyScrollDisable, + useDidMount, + useNDKContext +} from 'hooks' import { useState } from 'react' import { toast } from 'react-toastify' import { ModDetails } from 'types' @@ -12,22 +16,21 @@ 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() useBodyScrollDisable(isOpen) 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) 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) 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/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..1a32d28 100644 --- a/src/pages/settings/relay.tsx +++ b/src/pages/settings/relay.tsx @@ -1,21 +1,23 @@ -import { NDKRelayList } 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, - RelayController, - UserRelaysType -} from 'controllers' -import { useAppSelector, useDidMount } from 'hooks' +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' 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) @@ -23,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) }) @@ -38,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 @@ -78,11 +74,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 +133,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 +204,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 +369,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/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' +} 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) {