chore(git): merge pull request #89 from ndk-refactor into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 42s

Reviewed-on: #89
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
This commit is contained in:
enes 2024-10-21 13:39:56 +00:00
commit 988cc03f37
26 changed files with 526 additions and 1127 deletions

View File

@ -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)
})

View File

@ -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 = ({
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Game</label>
<p className='labelDescriptionMain'>
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.
</p>
<div className='dropdown dropdownMain'>
<div className='inputWrapperMain inputWrapperMainAlt'>
@ -825,10 +826,12 @@ const GameDropdown = ({
</List>
</div>
</div>
</div>
</div>
{error && <InputError message={error} />}
<p className='labelDescriptionMain'>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.
<p className='labelDescriptionMain'>
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.
</p>
</div>
)

View File

@ -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')

View File

@ -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<number>(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<number>(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<string, PaymentRequest>()
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,

View File

@ -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<ModDetails[]>
fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise<NDKEvent[]>
fetchEvent: (
filter: NDKFilter,
relayUrls?: string[]
) => Promise<NDKEvent | null>
fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
fetchEvent: (filter: NDKFilter) => Promise<NDKEvent | null>
fetchEventsFromUserRelays: (
filter: NDKFilter,
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
) => Promise<NDKEvent[]>
fetchEventFromUserRelays: (
filter: NDKFilter,
filter: NDKFilter | NDKFilter[],
hexKey: string,
userRelaysType: UserRelaysType
) => Promise<NDKEvent | null>
findMetadata: (pubkey: string) => Promise<UserProfile>
getTotalZapAmount: (
user: string,
eTag: string,
aTag?: string,
currentLoggedInUser?: string
) => Promise<{
accumulatedZapAmount: number
hasZapped: boolean
}>
publish: (event: NDKEvent) => Promise<string[]>
getNSFWList: () => Promise<string[]>
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<ModDetails[]> => {
const relays = new Set<string>()
relays.add(import.meta.env.VITE_APP_RELAY)
const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
const promises = adminNpubs.map((npub) => {
const hexKey = npubToHex(npub)
if (!hexKey) return null
return getRelayListForUser(hexKey, ndk)
.then((ndkRelayList) => {
if (ndkRelayList) {
ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
}
})
.catch((err) => {
log(
true,
LogType.Error,
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
err
)
})
})
await Promise.allSettled(promises)
// Define the filter criteria for fetching mods
const filter: NDKFilter = {
kinds: [NDKKind.Classified], // Specify the kind of events to fetch
@ -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<NDKEvent[]> => {
const relays = new Set<string>()
// add all the relays passed to relay set
relayUrls.forEach((relayUrl) => {
relays.add(relayUrl)
})
relays.add(import.meta.env.VITE_APP_RELAY)
const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
const promises = adminNpubs.map((npub) => {
const hexKey = npubToHex(npub)
if (!hexKey) return null
return getRelayListForUser(hexKey, ndk)
.then((ndkRelayList) => {
if (ndkRelayList) {
ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
}
})
.catch((err) => {
log(
true,
LogType.Error,
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
err
)
})
})
await Promise.allSettled(promises)
const fetchEvents = async (filter: NDKFilter): Promise<NDKEvent[]> => {
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<NDKEvent[]> => {
// 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<string[]> => {
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<string[]>} - A promise that resolves to an array of NSFW post identifiers (e.g., URLs or IDs).
*/
const getNSFWList = async (): Promise<string[]> => {
// 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<string>()
const adminMutedPosts = new Set<string>()
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<string>()
const userMutedPosts = new Set<string>()
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 (
<NDKContext.Provider
value={{
@ -338,7 +493,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
fetchEvent,
fetchEventsFromUserRelays,
fetchEventFromUserRelays,
findMetadata
findMetadata,
getTotalZapAmount,
publish,
getNSFWList,
getMuteLists
}}
>
{children}

View File

@ -1,3 +1 @@
export * from './metadata'
export * from './relay'
export * from './zap'

View File

@ -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<string>()
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<MetadataController> {
if (!MetadataController.instance) {
MetadataController.instance = new MetadataController()
await MetadataController.instance.setAdminRelays()
}
return MetadataController.instance
}
public findUserRelays = async (
hexKey: string,
userRelaysType: UserRelaysType = UserRelaysType.Both
): Promise<string[]> => {
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<string>()
const adminMutedPosts = new Set<string>()
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<string>()
const userMutedPosts = new Set<string>()
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<string[]>} - A promise that resolves to an array of NSFW post identifiers (e.g., URLs or IDs).
*/
public getNSFWList = async (): Promise<string[]> => {
// 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
}
}

View File

@ -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<string, Event>()
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<string[]> => {
// 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<Relay[]>((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<string[]> => {
// 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<string[]> => {
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<Relay[]>((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<Relay[]>((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<Relay[]>((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<string>() // 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<void>((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
}
}
}

View File

@ -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]
],

View File

@ -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'

View File

@ -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
}

View File

@ -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 }
}

View File

@ -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<string[]>([])
useDidMount(async () => {
const metadataController = await MetadataController.getInstance()
metadataController.getNSFWList().then((list) => {
getNSFWList().then((list) => {
setNSFWList(list)
})
})

View File

@ -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<NDKEvent[]>([])
@ -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(

View File

@ -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 (

View File

@ -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)

View File

@ -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)

View File

@ -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<FilterOptions>({
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)

View File

@ -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)

View File

@ -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)

View File

@ -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 = () => {

View File

@ -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<FormState>(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) {

View File

@ -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<NDKRelayList | null>(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 (

View File

@ -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)

View File

@ -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'
}

View File

@ -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<string[]>
) => {
// 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<string[]>
) => {
// 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) {