Compare commits

..

No commits in common. "524b9f304f3d09877409b61942bf9332ec43d1c3" and "edc01e028dba00829a6e27551423e1cb94954d47" have entirely different histories.

33 changed files with 656 additions and 1137 deletions

View File

@ -5,8 +5,7 @@ import { handleModImageError } from '../utils'
import { ModDetails } from 'types' import { ModDetails } from 'types'
import { getModPageRoute } from 'routes' import { getModPageRoute } from 'routes'
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
import { useDidMount, useReactions } from 'hooks' import { useDidMount, useNDKContext, useReactions } from 'hooks'
import { RelayController } from 'controllers'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useComments } from 'hooks/useComments' import { useComments } from 'hooks/useComments'
@ -19,10 +18,10 @@ export const ModCard = React.memo((props: ModDetails) => {
eTag: props.id, eTag: props.id,
aTag: props.aTag aTag: props.aTag
}) })
const { getTotalZapAmount } = useNDKContext()
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() getTotalZapAmount(props.author, props.id, props.aTag)
.getTotalZapAmount(props.author, props.id, props.aTag)
.then((res) => { .then((res) => {
setTotalZappedAmount(res.accumulatedZapAmount) setTotalZappedAmount(res.accumulatedZapAmount)
}) })
@ -51,7 +50,13 @@ export const ModCard = React.memo((props: ModDetails) => {
src={props.featuredImageUrl} src={props.featuredImageUrl}
onError={handleModImageError} onError={handleModImageError}
className='cMMPicture' className='cMMPicture'
alt={`featured image for mod ${props.title}`}
/> />
{props.nsfw && (
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW IBMSMSMBSSTagsTagNSFWCard'>
<p>NSFW</p>
</div>
)}
</div> </div>
<div className='cMMBody'> <div className='cMMBody'>
<h3 className='cMMBodyTitle'>{props.title}</h3> <h3 className='cMMBodyTitle'>{props.title}</h3>

View File

@ -13,8 +13,7 @@ import { toast } from 'react-toastify'
import { FixedSizeList as List } from 'react-window' import { FixedSizeList as List } from 'react-window'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { T_TAG_VALUE } from '../constants' import { T_TAG_VALUE } from '../constants'
import { RelayController } from '../controllers' import { useAppSelector, useGames, useNDKContext } from '../hooks'
import { useAppSelector, useGames } from '../hooks'
import { appRoutes, getModPageRoute } from '../routes' import { appRoutes, getModPageRoute } from '../routes'
import '../styles/styles.css' import '../styles/styles.css'
import { DownloadUrl, ModDetails, ModFormState } from '../types' import { DownloadUrl, ModDetails, ModFormState } from '../types'
@ -29,6 +28,7 @@ import {
} from '../utils' } from '../utils'
import { CheckboxField, InputError, InputField } from './Inputs' import { CheckboxField, InputError, InputField } from './Inputs'
import { LoadingSpinner } from './LoadingSpinner' import { LoadingSpinner } from './LoadingSpinner'
import { NDKEvent } from '@nostr-dev-kit/ndk'
interface FormErrors { interface FormErrors {
game?: string game?: string
@ -54,6 +54,7 @@ type ModFormProps = {
export const ModForm = ({ existingModData }: ModFormProps) => { export const ModForm = ({ existingModData }: ModFormProps) => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const { ndk, publish } = useNDKContext()
const games = useGames() const games = useGames()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
@ -243,9 +244,8 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
return return
} }
const publishedOnRelays = await RelayController.getInstance().publish( const ndkEvent = new NDKEvent(ndk, signedEvent)
signedEvent as Event const publishedOnRelays = await publish(ndkEvent)
)
// Handle cases where publishing failed or succeeded // Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
@ -763,8 +763,9 @@ const GameDropdown = ({
<div className='inputLabelWrapperMain'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Game</label> <label className='form-label labelMain'>Game</label>
<p className='labelDescriptionMain'> <p className='labelDescriptionMain'>
Can't find the game you're looking for? You can temporarily publish the mod under '(Unlisted Game)' and Can't find the game you're looking for? You can temporarily publish the
later edit it with the proper game name once we add it. mod under '(Unlisted Game)' and later edit it with the proper game name
once we add it.
</p> </p>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<div className='inputWrapperMain inputWrapperMainAlt'> <div className='inputWrapperMain inputWrapperMainAlt'>
@ -825,10 +826,12 @@ const GameDropdown = ({
</List> </List>
</div> </div>
</div> </div>
</div> </div>
{error && <InputError message={error} />} {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') <p className='labelDescriptionMain'>
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.
</p> </p>
</div> </div>
) )

View File

@ -4,13 +4,17 @@ import { QRCodeSVG } from 'qrcode.react'
import { useState } from 'react' import { useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { RelayController, UserRelaysType } from '../controllers' import {
import { useAppSelector, useDidMount, useNDKContext } from '../hooks' useAppSelector,
useBodyScrollDisable,
useDidMount,
useNDKContext
} from '../hooks'
import { appRoutes, getProfilePageRoute } from '../routes' import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
import '../styles/innerPage.css' import '../styles/innerPage.css'
import '../styles/socialPosts.css' import '../styles/socialPosts.css'
import { UserProfile } from '../types' import { UserProfile, UserRelaysType } from '../types'
import { import {
copyTextToClipboard, copyTextToClipboard,
hexToNpub, hexToNpub,
@ -22,6 +26,7 @@ import {
import { LoadingSpinner } from './LoadingSpinner' import { LoadingSpinner } from './LoadingSpinner'
import { ZapPopUp } from './Zap' import { ZapPopUp } from './Zap'
import placeholder from '../assets/img/DEGMods Placeholder Img.png' import placeholder from '../assets/img/DEGMods Placeholder Img.png'
import { NDKEvent } from '@nostr-dev-kit/ndk'
type Props = { type Props = {
pubkey: string pubkey: string
@ -254,6 +259,8 @@ export const ProfileQRButtonWithPopUp = ({
}: QRButtonWithPopUpProps) => { }: QRButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen)
const nprofile = nip19.nprofileEncode({ const nprofile = nip19.nprofileEncode({
pubkey pubkey
}) })
@ -335,6 +342,8 @@ type ZapButtonWithPopUpProps = {
const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen)
return ( return (
<> <>
<div <div
@ -368,7 +377,7 @@ type FollowButtonProps = {
} }
const FollowButton = ({ pubkey }: FollowButtonProps) => { const FollowButton = ({ pubkey }: FollowButtonProps) => {
const { fetchEventFromUserRelays } = useNDKContext() const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
const [isFollowing, setIsFollowing] = useState(false) const [isFollowing, setIsFollowing] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -441,9 +450,8 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => {
if (!signedEvent) return false if (!signedEvent) return false
const publishedOnRelays = await RelayController.getInstance().publish( const ndkEvent = new NDKEvent(ndk, signedEvent)
signedEvent as Event const publishedOnRelays = await publish(ndkEvent)
)
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
toast.error('Failed to publish event on any relay') 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 { QRCodeSVG } from 'qrcode.react'
import React, { import React, {
Dispatch, Dispatch,
@ -9,7 +10,7 @@ import React, {
} from 'react' } from 'react'
import Countdown, { CountdownRenderProps } from 'react-countdown' import Countdown, { CountdownRenderProps } from 'react-countdown'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { MetadataController, ZapController } from '../controllers' import { ZapController } from '../controllers'
import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import { useAppSelector, useDidMount, useNDKContext } from '../hooks'
import '../styles/popup.css' import '../styles/popup.css'
import { PaymentRequest, UserProfile } from '../types' import { PaymentRequest, UserProfile } from '../types'
@ -251,7 +252,7 @@ export const ZapPopUp = ({
setHasZapped, setHasZapped,
handleClose handleClose
}: ZapPopUpProps) => { }: ZapPopUpProps) => {
const { findMetadata } = useNDKContext() const { ndk, findMetadata } = useNDKContext()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState<number>(0) const [amount, setAmount] = useState<number>(0)
@ -300,6 +301,20 @@ export const ZapPopUp = ({
return null 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() const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request') setLoadingSpinnerDesc('Creating zap request')
@ -308,6 +323,7 @@ export const ZapPopUp = ({
receiverMetadata.lud16, receiverMetadata.lud16,
amount, amount,
receiverMetadata.pubkey as string, receiverMetadata.pubkey as string,
receiverRelays,
userHexKey, userHexKey,
message, message,
eventId, eventId,
@ -320,7 +336,7 @@ export const ZapPopUp = ({
.finally(() => { .finally(() => {
setIsLoading(false) setIsLoading(false)
}) })
}, [amount, message, userState, receiver, eventId, aTag]) }, [amount, message, userState, receiver, eventId, aTag, ndk, findMetadata])
const handleGenerateQRCode = async () => { const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest() const pr = await generatePaymentRequest()
@ -482,7 +498,7 @@ export const ZapSplit = ({
setHasZapped, setHasZapped,
handleClose handleClose
}: ZapSplitProps) => { }: ZapSplitProps) => {
const { findMetadata } = useNDKContext() const { ndk, findMetadata } = useNDKContext()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState<number>(0) const [amount, setAmount] = useState<number>(0)
@ -502,8 +518,8 @@ export const ZapSplit = ({
setAuthor(res) setAuthor(res)
}) })
const metadataController = await MetadataController.getInstance() const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
findMetadata(metadataController.adminNpubs[0]).then((res) => { findMetadata(adminNpubs[0]).then((res) => {
setAdmin(res) setAdmin(res)
}) })
}) })
@ -557,12 +573,30 @@ export const ZapSplit = ({
const invoices = new Map<string, PaymentRequest>() const invoices = new Map<string, PaymentRequest>()
if (authorShare > 0 && author?.pubkey && author?.lud16) { 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') setLoadingSpinnerDesc('Generating invoice for author')
const invoice = await zapController const invoice = await zapController
.getLightningPaymentRequest( .getLightningPaymentRequest(
author.lud16, author.lud16,
authorShare, authorShare,
author.pubkey as string, author.pubkey as string,
authorRelays,
userHexKey, userHexKey,
message, message,
eventId, eventId,
@ -579,12 +613,27 @@ export const ZapSplit = ({
} }
if (adminShare > 0 && admin?.pubkey && admin?.lud16) { 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') setLoadingSpinnerDesc('Generating invoice for site owner')
const invoice = await zapController const invoice = await zapController
.getLightningPaymentRequest( .getLightningPaymentRequest(
admin.lud16, admin.lud16,
adminShare, adminShare,
admin.pubkey as string, admin.pubkey as string,
adminRelays,
userHexKey, userHexKey,
message, message,
eventId, eventId,

View File

@ -3,17 +3,18 @@ import NDK, {
NDKEvent, NDKEvent,
NDKFilter, NDKFilter,
NDKKind, NDKKind,
NDKList,
NDKRelaySet, NDKRelaySet,
NDKSubscriptionCacheUsage, NDKSubscriptionCacheUsage,
NDKUser NDKUser,
zapInvoiceFromEvent
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie' import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts' import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts'
import { UserRelaysType } from 'controllers'
import { Dexie } from 'dexie' import { Dexie } from 'dexie'
import { createContext, ReactNode, useEffect, useMemo } from 'react' import { createContext, ReactNode, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { ModDetails, UserProfile } from 'types' import { ModDetails, MuteLists, UserProfile, UserRelaysType } from 'types'
import { import {
constructModListFromEvents, constructModListFromEvents,
hexToNpub, hexToNpub,
@ -33,23 +34,34 @@ type FetchModsOptions = {
interface NDKContextType { interface NDKContextType {
ndk: NDK ndk: NDK
fetchMods: (opts: FetchModsOptions) => Promise<ModDetails[]> fetchMods: (opts: FetchModsOptions) => Promise<ModDetails[]>
fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise<NDKEvent[]> fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
fetchEvent: ( fetchEvent: (filter: NDKFilter) => Promise<NDKEvent | null>
filter: NDKFilter,
relayUrls?: string[]
) => Promise<NDKEvent | null>
fetchEventsFromUserRelays: ( fetchEventsFromUserRelays: (
filter: NDKFilter, filter: NDKFilter | NDKFilter[],
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => Promise<NDKEvent[]> ) => Promise<NDKEvent[]>
fetchEventFromUserRelays: ( fetchEventFromUserRelays: (
filter: NDKFilter, filter: NDKFilter | NDKFilter[],
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => Promise<NDKEvent | null> ) => Promise<NDKEvent | null>
findMetadata: (pubkey: string) => Promise<UserProfile> 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` // 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(() => { const ndk = useMemo(() => {
localStorage.setItem('debug', '*') localStorage.setItem('debug', '*')
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' }) const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' })
@ -88,6 +125,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
], ],
cacheAdapter: dexieAdapter cacheAdapter: dexieAdapter
}) })
addAdminRelays(ndk)
ndk.connect() ndk.connect()
@ -110,33 +148,6 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
since, since,
limit limit
}: FetchModsOptions): Promise<ModDetails[]> => { }: 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 // Define the filter criteria for fetching mods
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [NDKKind.Classified], // Specify the kind of events to fetch kinds: [NDKKind.Classified], // Specify the kind of events to fetch
@ -152,11 +163,10 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
} }
return ndk return ndk
.fetchEvents( .fetchEvents(filter, {
filter, closeOnEose: true,
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) })
)
.then((ndkEventSet) => { .then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet) const ndkEvents = Array.from(ndkEventSet)
orderEventsChronologically(ndkEvents) 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. * Asynchronously retrieves multiple event based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
* *
* @param filter - The filter criteria to find the event. * @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. * @returns Returns a promise that resolves to the found event or null if not found.
*/ */
const fetchEvents = async ( const fetchEvents = async (filter: NDKFilter): Promise<NDKEvent[]> => {
filter: NDKFilter,
relayUrls: string[] = []
): Promise<NDKEvent[]> => {
const relays = new Set<string>()
// add all the relays passed to relay set
relayUrls.forEach((relayUrl) => {
relays.add(relayUrl)
})
relays.add(import.meta.env.VITE_APP_RELAY)
const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
const promises = adminNpubs.map((npub) => {
const hexKey = npubToHex(npub)
if (!hexKey) return null
return getRelayListForUser(hexKey, ndk)
.then((ndkRelayList) => {
if (ndkRelayList) {
ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
}
})
.catch((err) => {
log(
true,
LogType.Error,
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
err
)
})
})
await Promise.allSettled(promises)
return ndk return ndk
.fetchEvents( .fetchEvents(filter, {
filter, closeOnEose: true,
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) })
)
.then((ndkEventSet) => { .then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet) const ndkEvents = Array.from(ndkEventSet)
return orderEventsChronologically(ndkEvents) 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. * Asynchronously retrieves an event based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
* *
* @param filter - The filter criteria to find the event. * @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. * @returns Returns a promise that resolves to the found event or null if not found.
*/ */
const fetchEvent = async (filter: NDKFilter, relayUrls: string[] = []) => { const fetchEvent = async (filter: NDKFilter) => {
const events = await fetchEvents(filter, relayUrls) const events = await fetchEvents(filter)
if (events.length === 0) return null if (events.length === 0) return null
return events[0] return events[0]
} }
@ -265,10 +234,10 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
* @returns A promise that resolves with an array of events. * @returns A promise that resolves with an array of events.
*/ */
const fetchEventsFromUserRelays = async ( const fetchEventsFromUserRelays = async (
filter: NDKFilter, filter: NDKFilter | NDKFilter[],
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => { ): Promise<NDKEvent[]> => {
// Find the user's relays. // Find the user's relays.
const relayUrls = await getRelayListForUser(hexKey, ndk) const relayUrls = await getRelayListForUser(hexKey, ndk)
.then((ndkRelayList) => { .then((ndkRelayList) => {
@ -285,8 +254,22 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
return [] as string[] return [] as string[]
}) })
// Fetch the event from the user's relays using the provided filter and relay URLs return ndk
return fetchEvents(filter, relayUrls) .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. * @returns A promise that resolves to the fetched event or null if the operation fails.
*/ */
const fetchEventFromUserRelays = async ( const fetchEventFromUserRelays = async (
filter: NDKFilter, filter: NDKFilter | NDKFilter[],
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => { ) => {
@ -329,6 +312,178 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
return userProfile 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 ( return (
<NDKContext.Provider <NDKContext.Provider
value={{ value={{
@ -338,7 +493,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
fetchEvent, fetchEvent,
fetchEventsFromUserRelays, fetchEventsFromUserRelays,
fetchEventFromUserRelays, fetchEventFromUserRelays,
findMetadata findMetadata,
getTotalZapAmount,
publish,
getNSFWList,
getMuteLists
}} }}
> >
{children} {children}

View File

@ -1,3 +1 @@
export * from './metadata'
export * from './relay'
export * from './zap' 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 ZapRequest
} from '../types' } from '../types'
import { log, LogType, npubToHex } from '../utils' import { log, LogType, npubToHex } from '../utils'
import { MetadataController, UserRelaysType } from './metadata'
/** /**
* Singleton class to manage zap related operations. * Singleton class to manage zap related operations.
@ -48,6 +47,7 @@ export class ZapController {
* @param lud16 - LUD-16 of the recipient. * @param lud16 - LUD-16 of the recipient.
* @param amount - payment amount (will be multiplied by 1000 to represent sats). * @param amount - payment amount (will be multiplied by 1000 to represent sats).
* @param recipientPubKey - pubKey of the recipient. * @param recipientPubKey - pubKey of the recipient.
* @param recipientRelays - relays on which zap receipt will be published.
* @param senderPubkey - pubKey of of the sender. * @param senderPubkey - pubKey of of the sender.
* @param content - optional content (comment). * @param content - optional content (comment).
* @param eventId - event id, if zapping an event. * @param eventId - event id, if zapping an event.
@ -59,6 +59,7 @@ export class ZapController {
lud16: string, lud16: string,
amount: number, amount: number,
recipientPubKey: string, recipientPubKey: string,
recipientRelays: string[],
senderPubkey: string, senderPubkey: string,
content?: string, content?: string,
eventId?: string, eventId?: string,
@ -88,6 +89,7 @@ export class ZapController {
amount, amount,
content, content,
recipientPubKey, recipientPubKey,
recipientRelays,
senderPubkey, senderPubkey,
eventId, eventId,
aTag aTag
@ -273,6 +275,7 @@ export class ZapController {
* @param amount - request amount (sats). * @param amount - request amount (sats).
* @param content - comment. * @param content - comment.
* @param recipientPubKey - pubKey of the recipient. * @param recipientPubKey - pubKey of the recipient.
* @param recipientRelays - relays on which zap receipt will be published.
* @param senderPubkey - pubKey of of the sender. * @param senderPubkey - pubKey of of the sender.
* @param eventId - event id, if zapping an event. * @param eventId - event id, if zapping an event.
* @param aTag - value of `a` tag. * @param aTag - value of `a` tag.
@ -282,6 +285,7 @@ export class ZapController {
amount: number, amount: number,
content = '', content = '',
recipientPubKey: string, recipientPubKey: string,
recipientRelays: string[],
senderPubkey: string, senderPubkey: string,
eventId?: string, eventId?: string,
aTag?: string aTag?: string
@ -290,21 +294,15 @@ export class ZapController {
if (!recipientHexKey) throw 'Invalid recipient pubKey.' if (!recipientHexKey) throw 'Invalid recipient pubKey.'
const metadataController = await MetadataController.getInstance() if (!recipientRelays.includes(this.appRelay)) {
const receiverReadRelays = await metadataController.findUserRelays( recipientRelays.push(this.appRelay)
recipientHexKey,
UserRelaysType.Read
)
if (!receiverReadRelays.includes(this.appRelay)) {
receiverReadRelays.push(this.appRelay)
} }
const zapRequest: ZapRequest = { const zapRequest: ZapRequest = {
kind: kinds.ZapRequest, kind: kinds.ZapRequest,
content, content,
tags: [ tags: [
['relays', ...receiverReadRelays], ['relays', ...recipientRelays],
['amount', `${amount}`], ['amount', `${amount}`],
['p', recipientHexKey] ['p', recipientHexKey]
], ],

View File

@ -6,3 +6,4 @@ export * from './useMuteLists'
export * from './useNSFWList' export * from './useNSFWList'
export * from './useReactions' export * from './useReactions'
export * from './useNDKContext' export * from './useNDKContext'
export * from './useScrollDisable'

View File

@ -6,9 +6,8 @@ import {
NDKSubscription, NDKSubscription,
NDKSubscriptionCacheUsage NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { UserRelaysType } from 'controllers'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { CommentEvent, ModDetails } from 'types' import { CommentEvent, ModDetails, UserRelaysType } from 'types'
import { log, LogType } from 'utils' import { log, LogType } from 'utils'
import { useNDKContext } from './useNDKContext' import { useNDKContext } from './useNDKContext'

View File

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { MuteLists } from 'types' import { MuteLists } from 'types'
import { useAppSelector } from './redux' import { useAppSelector } from './redux'
import { MetadataController } from 'controllers' import { useNDKContext } from './useNDKContext'
export const useMuteLists = () => { export const useMuteLists = () => {
const { getMuteLists } = useNDKContext()
const [muteLists, setMuteLists] = useState<{ const [muteLists, setMuteLists] = useState<{
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
@ -21,17 +22,11 @@ export const useMuteLists = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
useEffect(() => { useEffect(() => {
const getMuteLists = async () => { const pubkey = userState.user?.pubkey as string | undefined
const pubkey = userState.user?.pubkey as string | undefined getMuteLists(pubkey).then((lists) => {
setMuteLists(lists)
const metadataController = await MetadataController.getInstance() })
metadataController.getMuteLists(pubkey).then((lists) => { }, [userState, getMuteLists])
setMuteLists(lists)
})
}
getMuteLists()
}, [userState])
return muteLists return muteLists
} }

View File

@ -9,23 +9,5 @@ export const useNDKContext = () => {
'NDKContext should not be used in out component tree hierarchy' 'NDKContext should not be used in out component tree hierarchy'
) )
const { return { ...ndkContext }
ndk,
fetchEvents,
fetchEvent,
fetchEventsFromUserRelays,
fetchEventFromUserRelays,
fetchMods,
findMetadata
} = ndkContext
return {
ndk,
fetchEvents,
fetchEvent,
fetchEventsFromUserRelays,
fetchEventFromUserRelays,
fetchMods,
findMetadata
}
} }

View File

@ -1,14 +1,13 @@
import { MetadataController } from 'controllers'
import { useState } from 'react' import { useState } from 'react'
import { useDidMount } from './useDidMount' import { useDidMount } from './useDidMount'
import { useNDKContext } from './useNDKContext'
export const useNSFWList = () => { export const useNSFWList = () => {
const { getNSFWList } = useNDKContext()
const [nsfwList, setNSFWList] = useState<string[]>([]) const [nsfwList, setNSFWList] = useState<string[]>([])
useDidMount(async () => { useDidMount(async () => {
const metadataController = await MetadataController.getInstance() getNSFWList().then((list) => {
metadataController.getNSFWList().then((list) => {
setNSFWList(list) setNSFWList(list)
}) })
}) })

View File

@ -1,10 +1,10 @@
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk' import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'
import { REACTIONS } from 'constants.ts' import { REACTIONS } from 'constants.ts'
import { RelayController, UserRelaysType } from 'controllers'
import { useAppSelector, useDidMount, useNDKContext } from 'hooks' import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { Event, kinds, UnsignedEvent } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { UserRelaysType } from 'types'
import { abbreviateNumber, log, LogType, now } from 'utils' import { abbreviateNumber, log, LogType, now } from 'utils'
type UseReactionsParams = { type UseReactionsParams = {
@ -14,7 +14,7 @@ type UseReactionsParams = {
} }
export const useReactions = (params: UseReactionsParams) => { export const useReactions = (params: UseReactionsParams) => {
const { ndk, fetchEventsFromUserRelays } = useNDKContext() const { ndk, fetchEventsFromUserRelays, publish } = useNDKContext()
const [isReactionInProgress, setIsReactionInProgress] = useState(false) const [isReactionInProgress, setIsReactionInProgress] = useState(false)
const [isDataLoaded, setIsDataLoaded] = useState(false) const [isDataLoaded, setIsDataLoaded] = useState(false)
const [reactionEvents, setReactionEvents] = useState<NDKEvent[]>([]) const [reactionEvents, setReactionEvents] = useState<NDKEvent[]>([])
@ -119,13 +119,11 @@ export const useReactions = (params: UseReactionsParams) => {
if (!signedEvent) return if (!signedEvent) return
setReactionEvents((prev) => [...prev, new NDKEvent(ndk, signedEvent)]) const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishedOnRelays = await RelayController.getInstance().publish( setReactionEvents((prev) => [...prev, ndkEvent])
signedEvent as Event,
params.pubkey, const publishedOnRelays = await publish(ndkEvent)
UserRelaysType.Read
)
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
log( log(

View File

@ -0,0 +1,11 @@
import { useEffect } from 'react'
export const useBodyScrollDisable = (disable: boolean) => {
useEffect(() => {
if (disable) document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = ''
}
}, [disable])
}

View File

@ -6,10 +6,10 @@ import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Banner } from '../components/Banner' import { Banner } from '../components/Banner'
import { ZapPopUp } from '../components/Zap' import { ZapPopUp } from '../components/Zap'
import { MetadataController } from '../controllers'
import { import {
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
useBodyScrollDisable,
useDidMount, useDidMount,
useNDKContext useNDKContext
} from '../hooks' } from '../hooks'
@ -27,6 +27,18 @@ export const Header = () => {
const { findMetadata } = useNDKContext() const { findMetadata } = useNDKContext()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
// Track nostr-login extension modal open state
const [isOpen, setIsOpen] = useState(false)
const handleOpen = () => setIsOpen(true)
const handleClose = () => setIsOpen(false)
useEffect(() => {
window.addEventListener('nlCloseModal', handleClose)
return () => {
window.removeEventListener('nlCloseModal', handleClose)
}
}, [])
useBodyScrollDisable(isOpen)
useEffect(() => { useEffect(() => {
initNostrLogin({ initNostrLogin({
darkMode: true, darkMode: true,
@ -66,6 +78,7 @@ export const Header = () => {
}, [dispatch, findMetadata]) }, [dispatch, findMetadata])
const handleLogin = () => { const handleLogin = () => {
handleOpen()
launchNostrLoginDialog() launchNostrLoginDialog()
} }
@ -260,9 +273,11 @@ const TipButtonWithDialog = React.memo(() => {
const [adminNpub, setAdminNpub] = useState<string | null>(null) const [adminNpub, setAdminNpub] = useState<string | null>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen)
useDidMount(async () => { useDidMount(async () => {
const metadataController = await MetadataController.getInstance() const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
setAdminNpub(metadataController.adminNpubs[0]) setAdminNpub(adminNpubs[0])
}) })
return ( return (
@ -321,6 +336,8 @@ const TipButtonWithDialog = React.memo(() => {
const RegisterButtonWithDialog = () => { const RegisterButtonWithDialog = () => {
const [showPopUp, setShowPopUp] = useState(false) const [showPopUp, setShowPopUp] = useState(false)
useBodyScrollDisable(showPopUp)
return ( return (
<> <>
<a <a

View File

@ -14,7 +14,7 @@ import {
useNDKContext, useNDKContext,
useNSFWList useNSFWList
} from 'hooks' } from 'hooks'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { import {
FilterOptions, FilterOptions,
@ -23,9 +23,10 @@ import {
NSFWFilter, NSFWFilter,
SortBy SortBy
} from 'types' } from 'types'
import { extractModData, isModDataComplete } from 'utils' import { extractModData, isModDataComplete, scrollIntoView } from 'utils'
export const GamePage = () => { export const GamePage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null)
const params = useParams() const params = useParams()
const { name: gameName } = params const { name: gameName } = params
const { ndk } = useNDKContext() const { ndk } = useNDKContext()
@ -61,6 +62,7 @@ export const GamePage = () => {
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) { if (page >= 1 && page <= totalPages) {
scrollIntoView(scrollTargetRef.current)
setCurrentPage(page) setCurrentPage(page)
} }
} }
@ -102,7 +104,10 @@ export const GamePage = () => {
<> <>
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div
className='IBMSecMainGroup IBMSecMainGroupAlt'
ref={scrollTargetRef}
>
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='SearchMainWrapper'> <div className='SearchMainWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>

View File

@ -8,8 +8,10 @@ import '../styles/search.css'
import '../styles/styles.css' import '../styles/styles.css'
import { createSearchParams, useNavigate } from 'react-router-dom' import { createSearchParams, useNavigate } from 'react-router-dom'
import { appRoutes } from 'routes' import { appRoutes } from 'routes'
import { scrollIntoView } from 'utils'
export const GamesPage = () => { export const GamesPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null)
const navigate = useNavigate() const navigate = useNavigate()
const { fetchMods } = useNDKContext() const { fetchMods } = useNDKContext()
const searchTermRef = useRef<HTMLInputElement>(null) const searchTermRef = useRef<HTMLInputElement>(null)
@ -63,6 +65,7 @@ export const GamesPage = () => {
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) { if (page >= 1 && page <= totalPages) {
scrollIntoView(scrollTargetRef.current)
setCurrentPage(page) setCurrentPage(page)
} }
} }
@ -88,7 +91,10 @@ export const GamesPage = () => {
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div
className='IBMSecMainGroup IBMSecMainGroupAlt'
ref={scrollTargetRef}
>
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='SearchMainWrapper'> <div className='SearchMainWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>

View File

@ -152,7 +152,7 @@ const SlideContent = ({ naddr }: SlideContentProps) => {
useDidMount(() => { useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) 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 = { const ndkFilter: NDKFilter = {
'#a': [identifier], '#a': [identifier],
@ -160,7 +160,7 @@ const SlideContent = ({ naddr }: SlideContentProps) => {
kinds: [kind] kinds: [kind]
} }
fetchEvent(ndkFilter, relays) fetchEvent(ndkFilter)
.then((ndkEvent) => { .then((ndkEvent) => {
if (ndkEvent) { if (ndkEvent) {
const extracted = extractModData(ndkEvent) const extracted = extractModData(ndkEvent)
@ -225,7 +225,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
useDidMount(() => { useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) 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 = { const ndkFilter: NDKFilter = {
'#a': [identifier], '#a': [identifier],
@ -233,7 +233,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
kinds: [kind] kinds: [kind]
} }
fetchEvent(ndkFilter, relays) fetchEvent(ndkFilter)
.then((ndkEvent) => { .then((ndkEvent) => {
if (ndkEvent) { if (ndkEvent) {
const extracted = extractModData(ndkEvent) const extracted = extractModData(ndkEvent)

View File

@ -11,8 +11,12 @@ import { toast } from 'react-toastify'
import { BlogCard } from '../../components/BlogCard' import { BlogCard } from '../../components/BlogCard'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ProfileSection } from '../../components/ProfileSection' import { ProfileSection } from '../../components/ProfileSection'
import { MetadataController, UserRelaysType } from '../../controllers' import {
import { useAppSelector, useDidMount, useNDKContext } from '../../hooks' useAppSelector,
useBodyScrollDisable,
useDidMount,
useNDKContext
} from '../../hooks'
import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
import '../../styles/comments.css' import '../../styles/comments.css'
import '../../styles/downloads.css' import '../../styles/downloads.css'
@ -24,7 +28,7 @@ import '../../styles/styles.css'
import '../../styles/tabs.css' import '../../styles/tabs.css'
import '../../styles/tags.css' import '../../styles/tags.css'
import '../../styles/write.css' import '../../styles/write.css'
import { DownloadUrl, ModDetails } from '../../types' import { DownloadUrl, ModDetails, UserRelaysType } from '../../types'
import { import {
abbreviateNumber, abbreviateNumber,
copyTextToClipboard, copyTextToClipboard,
@ -53,7 +57,7 @@ export const ModPage = () => {
useDidMount(async () => { useDidMount(async () => {
if (naddr) { if (naddr) {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) 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 = { const filter: NDKFilter = {
'#a': [identifier], '#a': [identifier],
@ -61,7 +65,7 @@ export const ModPage = () => {
kinds: [kind] kinds: [kind]
} }
fetchEvent(filter, relays) fetchEvent(filter)
.then((event) => { .then((event) => {
if (event) { if (event) {
const extracted = extractModData(event) const extracted = extractModData(event)
@ -212,7 +216,7 @@ type GameProps = {
} }
const Game = ({ naddr, game, author, aTag }: GameProps) => { const Game = ({ naddr, game, author, aTag }: GameProps) => {
const { fetchEventFromUserRelays } = useNDKContext() const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -221,6 +225,8 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
const [isBlocked, setIsBlocked] = useState(false) const [isBlocked, setIsBlocked] = useState(false)
const [isAddedToNSFW, setIsAddedToNSFW] = useState(false) const [isAddedToNSFW, setIsAddedToNSFW] = useState(false)
useBodyScrollDisable(showReportPopUp)
useEffect(() => { useEffect(() => {
if (userState.auth && userState.user?.pubkey) { if (userState.auth && userState.user?.pubkey) {
const pubkey = userState.user.pubkey as string const pubkey = userState.user.pubkey as string
@ -343,7 +349,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
setLoadingSpinnerDesc('Updating mute list event') setLoadingSpinnerDesc('Updating mute list event')
const isUpdated = await signAndPublish(unsignedEvent) const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
if (isUpdated) { if (isUpdated) {
setIsBlocked(true) setIsBlocked(true)
} }
@ -384,7 +390,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
} }
setLoadingSpinnerDesc('Updating mute list event') setLoadingSpinnerDesc('Updating mute list event')
const isUpdated = await signAndPublish(unsignedEvent) const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
if (isUpdated) { if (isUpdated) {
setIsBlocked(false) setIsBlocked(false)
} }
@ -450,7 +456,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
setLoadingSpinnerDesc('Updating nsfw list event') setLoadingSpinnerDesc('Updating nsfw list event')
const isUpdated = await signAndPublish(unsignedEvent) const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
if (isUpdated) { if (isUpdated) {
setIsAddedToNSFW(true) setIsAddedToNSFW(true)
} }
@ -491,7 +497,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
} }
setLoadingSpinnerDesc('Updating nsfw list event') setLoadingSpinnerDesc('Updating nsfw list event')
const isUpdated = await signAndPublish(unsignedEvent) const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
if (isUpdated) { if (isUpdated) {
setIsAddedToNSFW(false) setIsAddedToNSFW(false)
} }
@ -661,7 +667,7 @@ type ReportPopupProps = {
} }
const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
const { fetchEventFromUserRelays } = useNDKContext() const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [selectedOptions, setSelectedOptions] = useState({ const [selectedOptions, setSelectedOptions] = useState({
actuallyCP: false, actuallyCP: false,
@ -708,8 +714,8 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
return return
} }
const metadataController = await MetadataController.getInstance() const reportingNpub = import.meta.env.VITE_REPORTING_NPUB
const reportingPubkey = npubToHex(metadataController.reportingNpub) const reportingPubkey = npubToHex(reportingNpub)
if (reportingPubkey === hexPubkey) { if (reportingPubkey === hexPubkey) {
setLoadingSpinnerDesc(`Finding user's mute list`) setLoadingSpinnerDesc(`Finding user's mute list`)
@ -760,7 +766,7 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
} }
setLoadingSpinnerDesc('Updating mute list event') setLoadingSpinnerDesc('Updating mute list event')
const isUpdated = await signAndPublish(unsignedEvent) const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
if (isUpdated) handleClose() if (isUpdated) handleClose()
} else { } else {
const href = window.location.href const href = window.location.href
@ -773,7 +779,12 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
}) })
setLoadingSpinnerDesc('Sending report') setLoadingSpinnerDesc('Sending report')
const isSent = await sendDMUsingRandomKey(message, reportingPubkey!) const isSent = await sendDMUsingRandomKey(
message,
reportingPubkey!,
ndk,
publish
)
if (isSent) handleClose() if (isSent) handleClose()
} }
setIsLoading(false) setIsLoading(false)

View File

@ -1,11 +1,13 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'
import { ZapPopUp } from 'components/Zap' import { ZapPopUp } from 'components/Zap'
import {
MetadataController,
RelayController,
UserRelaysType
} from 'controllers'
import { formatDate } from 'date-fns' import { formatDate } from 'date-fns'
import { useAppSelector, useDidMount, useNDKContext, useReactions } from 'hooks' import {
useAppSelector,
useBodyScrollDisable,
useDidMount,
useNDKContext,
useReactions
} from 'hooks'
import { useComments } from 'hooks/useComments' import { useComments } from 'hooks/useComments'
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import React, { import React, {
@ -47,6 +49,7 @@ type Props = {
} }
export const Comments = ({ modDetails, setCommentCount }: Props) => { export const Comments = ({ modDetails, setCommentCount }: Props) => {
const { ndk, publish } = useNDKContext()
const { commentEvents, setCommentEvents } = useComments(modDetails) const { commentEvents, setCommentEvents } = useComments(modDetails)
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortByEnum.Latest, sort: SortByEnum.Latest,
@ -82,7 +85,8 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => {
created_at: now(), created_at: now(),
tags: [ tags: [
['e', modDetails.id], ['e', modDetails.id],
['a', modDetails.aTag] ['a', modDetails.aTag],
['p', modDetails.author]
] ]
} }
@ -105,28 +109,52 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => {
...prev ...prev
]) ])
const publish = async () => { const ndkEvent = new NDKEvent(ndk, signedEvent)
const metadataController = await MetadataController.getInstance() publish(ndkEvent)
const modAuthorReadRelays = await metadataController.findUserRelays( .then((publishedOnRelays) => {
modDetails.author, if (publishedOnRelays.length === 0) {
UserRelaysType.Read setCommentEvents((prev) =>
) prev.map((event) => {
const commentatorWriteRelays = await metadataController.findUserRelays( if (event.id === signedEvent.id) {
pubkey, return {
UserRelaysType.Write ...event,
) status: CommentEventStatus.Failed
}
}
const combinedRelays = [ return event
...new Set(...modAuthorReadRelays, ...commentatorWriteRelays) })
] )
} else {
setCommentEvents((prev) =>
prev.map((event) => {
if (event.id === signedEvent.id) {
return {
...event,
status: CommentEventStatus.Published
}
}
const publishedOnRelays = return event
await RelayController.getInstance().publishOnRelays( })
signedEvent, )
combinedRelays }
)
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) => setCommentEvents((prev) =>
prev.map((event) => { prev.map((event) => {
if (event.id === signedEvent.id) { if (event.id === signedEvent.id) {
@ -139,36 +167,7 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => {
return event 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 return true
} }
@ -496,20 +495,21 @@ const Reactions = (props: Event) => {
const Zap = (props: Event) => { const Zap = (props: Event) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
const [hasZapped, setHasZapped] = useState(false) const [hasZapped, setHasZapped] = useState(false)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const { getTotalZapAmount } = useNDKContext()
const [totalZappedAmount, setTotalZappedAmount] = useState(0) useBodyScrollDisable(isOpen)
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() getTotalZapAmount(
.getTotalZapAmount( props.pubkey,
props.pubkey, props.id,
props.id, undefined,
undefined, userState.user?.pubkey as string
userState.user?.pubkey as string )
)
.then((res) => { .then((res) => {
setTotalZappedAmount(res.accumulatedZapAmount) setTotalZappedAmount(res.accumulatedZapAmount)
setHasZapped(res.hasZapped) setHasZapped(res.hasZapped)

View File

@ -1,6 +1,10 @@
import { ZapSplit } from 'components/Zap' import { ZapSplit } from 'components/Zap'
import { RelayController } from 'controllers' import {
import { useAppSelector, useDidMount } from 'hooks' useAppSelector,
useBodyScrollDisable,
useDidMount,
useNDKContext
} from 'hooks'
import { useState } from 'react' import { useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { ModDetails } from 'types' import { ModDetails } from 'types'
@ -12,20 +16,21 @@ type ZapProps = {
export const Zap = ({ modDetails }: ZapProps) => { export const Zap = ({ modDetails }: ZapProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
const [hasZapped, setHasZapped] = useState(false) const [hasZapped, setHasZapped] = useState(false)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const { getTotalZapAmount } = useNDKContext()
const [totalZappedAmount, setTotalZappedAmount] = useState(0) useBodyScrollDisable(isOpen)
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() getTotalZapAmount(
.getTotalZapAmount( modDetails.author,
modDetails.author, modDetails.id,
modDetails.id, modDetails.aTag,
modDetails.aTag, userState.user?.pubkey as string
userState.user?.pubkey as string )
)
.then((res) => { .then((res) => {
setTotalZappedAmount(res.accumulatedZapAmount) setTotalZappedAmount(res.accumulatedZapAmount)
setHasZapped(res.hasZapped) setHasZapped(res.hasZapped)

View File

@ -24,8 +24,10 @@ import {
NSFWFilter, NSFWFilter,
SortBy SortBy
} from '../types' } from '../types'
import { scrollIntoView } from 'utils'
export const ModsPage = () => { export const ModsPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null)
const { fetchMods } = useNDKContext() const { fetchMods } = useNDKContext()
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
@ -66,6 +68,7 @@ export const ModsPage = () => {
.then((res) => { .then((res) => {
setMods(res) setMods(res)
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
scrollIntoView(scrollTargetRef.current)
}) })
.finally(() => { .finally(() => {
setIsFetching(false) setIsFetching(false)
@ -84,6 +87,7 @@ export const ModsPage = () => {
.then((res) => { .then((res) => {
setMods(res) setMods(res)
setPage((prev) => prev - 1) setPage((prev) => prev - 1)
scrollIntoView(scrollTargetRef.current)
}) })
.finally(() => { .finally(() => {
setIsFetching(false) setIsFetching(false)
@ -103,7 +107,10 @@ export const ModsPage = () => {
{isFetching && <LoadingSpinner desc='Fetching mod details from relays' />} {isFetching && <LoadingSpinner desc='Fetching mod details from relays' />}
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div
className='IBMSecMainGroup IBMSecMainGroupAlt'
ref={scrollTargetRef}
>
<PageTitleRow /> <PageTitleRow />
<ModFilter <ModFilter
filterOptions={filterOptions} filterOptions={filterOptions}

View File

@ -43,7 +43,13 @@ import {
NSFWFilter, NSFWFilter,
SortBy SortBy
} from 'types' } from 'types'
import { extractModData, isModDataComplete, log, LogType } from 'utils' import {
extractModData,
isModDataComplete,
log,
LogType,
scrollIntoView
} from 'utils'
enum SearchKindEnum { enum SearchKindEnum {
Mods = 'Mods', Mods = 'Mods',
@ -52,6 +58,7 @@ enum SearchKindEnum {
} }
export const SearchPage = () => { export const SearchPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null)
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const muteLists = useMuteLists() const muteLists = useMuteLists()
@ -88,7 +95,10 @@ export const SearchPage = () => {
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div
className='IBMSecMainGroup IBMSecMainGroupAlt'
ref={scrollTargetRef}
>
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='SearchMainWrapper'> <div className='SearchMainWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
@ -141,6 +151,7 @@ export const SearchPage = () => {
filterOptions={filterOptions} filterOptions={filterOptions}
muteLists={muteLists} muteLists={muteLists}
nsfwList={nsfwList} nsfwList={nsfwList}
el={scrollTargetRef.current}
/> />
)} )}
{searchKind === SearchKindEnum.Users && ( {searchKind === SearchKindEnum.Users && (
@ -263,13 +274,15 @@ type ModsResultProps = {
user: MuteLists user: MuteLists
} }
nsfwList: string[] nsfwList: string[]
el: HTMLElement | null
} }
const ModsResult = ({ const ModsResult = ({
filterOptions, filterOptions,
searchTerm, searchTerm,
muteLists, muteLists,
nsfwList nsfwList,
el
}: ModsResultProps) => { }: ModsResultProps) => {
const { ndk } = useNDKContext() const { ndk } = useNDKContext()
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
@ -305,7 +318,9 @@ const ModsResult = ({
}, [ndk]) }, [ndk])
useEffect(() => { useEffect(() => {
scrollIntoView(el)
setPage(1) setPage(1)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm]) }, [searchTerm])
const filteredMods = useMemo(() => { const filteredMods = useMemo(() => {
@ -334,10 +349,12 @@ const ModsResult = ({
) )
const handleNext = () => { const handleNext = () => {
scrollIntoView(el)
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
} }
const handlePrev = () => { const handlePrev = () => {
scrollIntoView(el)
setPage((prev) => prev - 1) setPage((prev) => prev - 1)
} }
@ -392,7 +409,7 @@ const UsersResult = ({
} }
setIsFetching(true) setIsFetching(true)
fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es']) fetchEvents(filter)
.then((events) => { .then((events) => {
const results = events.map((event) => { const results = events.map((event) => {
const ndkEvent = new NDKEvent(undefined, event) const ndkEvent = new NDKEvent(undefined, event)

View File

@ -1,5 +1,4 @@
import { AdminSVG, PreferenceSVG, ProfileSVG, RelaySVG } from 'components/SVGs' import { AdminSVG, PreferenceSVG, ProfileSVG, RelaySVG } from 'components/SVGs'
import { MetadataController } from 'controllers'
import { useAppSelector } from 'hooks' import { useAppSelector } from 'hooks'
import { logout } from 'nostr-login' import { logout } from 'nostr-login'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -57,15 +56,12 @@ const SettingTabs = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
useEffect(() => { useEffect(() => {
MetadataController.getInstance().then((controller) => { const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
if (userState.auth && userState.user?.npub) { if (userState.auth && userState.user?.npub) {
setIsAdmin( setIsAdmin(adminNpubs.includes(userState.user.npub as string))
controller.adminNpubs.includes(userState.user.npub as string) } else {
) setIsAdmin(false)
} else { }
setIsAdmin(false)
}
})
}, [userState]) }, [userState])
const handleSignOut = () => { const handleSignOut = () => {

View File

@ -1,6 +1,6 @@
import { InputField } from 'components/Inputs' import { InputField } from 'components/Inputs'
import { ProfileQRButtonWithPopUp } from 'components/ProfileSection' 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 { kinds, nip19, UnsignedEvent, Event } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@ -14,7 +14,6 @@ import {
profileFromEvent, profileFromEvent,
serializeProfile serializeProfile
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { RelayController } from 'controllers'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { setUser } from 'store/reducers/user' import { setUser } from 'store/reducers/user'
import placeholderMod from '../../assets/img/DEGMods Placeholder Img.png' import placeholderMod from '../../assets/img/DEGMods Placeholder Img.png'
@ -43,6 +42,7 @@ const defaultFormState: FormState = {
export const ProfileSettings = () => { export const ProfileSettings = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const { ndk, publish } = useNDKContext()
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [formState, setFormState] = useState<FormState>(defaultFormState) const [formState, setFormState] = useState<FormState>(defaultFormState)
@ -163,9 +163,8 @@ export const ProfileSettings = () => {
return return
} }
const publishedOnRelays = await RelayController.getInstance().publish( const ndkEvent = new NDKEvent(ndk, signedEvent)
signedEvent as Event const publishedOnRelays = await publish(ndkEvent)
)
// Handle cases where publishing failed or succeeded // Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) { 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 { InputField } from 'components/Inputs'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
MetadataController,
RelayController,
UserRelaysType
} from 'controllers'
import { useAppSelector, useDidMount } from 'hooks'
import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { Event, kinds, UnsignedEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { UserRelaysType } from 'types'
import { log, LogType, normalizeWebSocketURL, now } from 'utils' import { log, LogType, normalizeWebSocketURL, now } from 'utils'
const READ_MARKER = 'read' const READ_MARKER = 'read'
const WRITE_MARKER = 'write' const WRITE_MARKER = 'write'
export const RelaySettings = () => { export const RelaySettings = () => {
const { ndk, publish } = useNDKContext()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null) const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null)
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
@ -23,10 +25,8 @@ export const RelaySettings = () => {
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
useEffect(() => { useEffect(() => {
const fetchRelayList = async (pubkey: string) => { if (userState.auth && userState.user?.pubkey) {
const metadataController = await MetadataController.getInstance() getRelayListForUser(userState.user.pubkey as string, ndk)
metadataController
.getNDKRelayList(pubkey)
.then((res) => { .then((res) => {
setNDKRelayList(res) setNDKRelayList(res)
}) })
@ -38,14 +38,10 @@ export const RelaySettings = () => {
) )
setNDKRelayList(null) setNDKRelayList(null)
}) })
}
if (userState.auth && userState.user?.pubkey) {
fetchRelayList(userState.user.pubkey as string)
} else { } else {
setNDKRelayList(null) setNDKRelayList(null)
} }
}, [userState]) }, [userState, ndk])
const handleAdd = async (relayUrl: string) => { const handleAdd = async (relayUrl: string) => {
if (!ndkRelayList) return if (!ndkRelayList) return
@ -78,11 +74,8 @@ export const RelaySettings = () => {
return return
} }
const publishedOnRelays = const ndkEvent = new NDKEvent(ndk, signedEvent)
await RelayController.getInstance().publishOnRelays( const publishedOnRelays = await publish(ndkEvent)
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded // Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
@ -140,11 +133,8 @@ export const RelaySettings = () => {
return return
} }
const publishedOnRelays = const ndkEvent = new NDKEvent(ndk, signedEvent)
await RelayController.getInstance().publishOnRelays( const publishedOnRelays = await publish(ndkEvent)
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded // Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
@ -214,11 +204,8 @@ export const RelaySettings = () => {
return return
} }
const publishedOnRelays = const ndkEvent = new NDKEvent(ndk, signedEvent)
await RelayController.getInstance().publishOnRelays( const publishedOnRelays = await publish(ndkEvent)
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded // Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
@ -382,17 +369,29 @@ const RelayListItem = ({
changeRelayType changeRelayType
}: RelayItemProps) => { }: RelayItemProps) => {
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
const { ndk } = useNDKContext()
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() const ndkPool = ndk.pool
.connectRelay(relayUrl)
.then((relay) => { ndkPool.on('relay:connect', (relay) => {
if (relay && relay.connected) { if (relay.url === relayUrl) {
setIsConnected(true) setIsConnected(true)
} else { }
setIsConnected(false) })
}
}) 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 ( return (

View File

@ -29,7 +29,7 @@ export const SubmitModPage = () => {
useDidMount(async () => { useDidMount(async () => {
if (naddr) { if (naddr) {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) 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 = { const filter: NDKFilter = {
'#a': [identifier], '#a': [identifier],
@ -39,7 +39,7 @@ export const SubmitModPage = () => {
setIsFetching(true) setIsFetching(true)
fetchEvent(filter, relays) fetchEvent(filter)
.then((event) => { .then((event) => {
if (event) { if (event) {
const extracted = extractModData(event) const extracted = extractModData(event)

View File

@ -96,6 +96,7 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
font-size: 20px; font-size: 20px;
line-height: 1.25; line-height: 1.25;
color: rgba(255, 255, 255, 0.75); color: rgba(255, 255, 255, 0.75);
@ -107,6 +108,7 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
font-size: 15px; font-size: 15px;
line-height: 1.5; line-height: 1.5;
@ -119,11 +121,12 @@
justify-content: start; justify-content: start;
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
background: rgba(255,255,255,0.05); background: rgba(255, 255, 255, 0.05);
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
line-clamp: 1;
} }
.cMMFootReactions { .cMMFootReactions {
@ -143,3 +146,12 @@
align-items: center; align-items: center;
color: rgba(255, 255, 255, 0.25); color: rgba(255, 255, 255, 0.25);
} }
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW.IBMSMSMBSSTagsTagNSFWCard {
position: absolute;
bottom: 10px;
right: 10px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: rgba(35, 35, 35, 0.85);
}

View File

@ -1,3 +1,9 @@
import { NDKUserProfile } from '@nostr-dev-kit/ndk' import { NDKUserProfile } from '@nostr-dev-kit/ndk'
export type UserProfile = NDKUserProfile | null export type UserProfile = NDKUserProfile | null
export enum UserRelaysType {
Read = 'readRelayUrls',
Write = 'writeRelayUrls',
Both = 'bothRelayUrls'
}

View File

@ -9,9 +9,8 @@ import {
UnsignedEvent UnsignedEvent
} from 'nostr-tools' } from 'nostr-tools'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { RelayController } from '../controllers'
import { log, LogType } from './utils' 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). * 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. * @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 * @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. // Sign the event. This returns a signed event or null if signing fails.
const signedEvent = await window.nostr const signedEvent = await window.nostr
?.signEvent(unsignedEvent) ?.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 the event couldn't be signed, exit the function and return null.
if (!signedEvent) return false 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. // This returns an array of relay URLs where the event was successfully published.
const publishedOnRelays = await RelayController.getInstance().publish( const ndkEvent = new NDKEvent(ndk, signedEvent)
signedEvent as Event const publishedOnRelays = await publish(ndkEvent)
)
// Handle cases where publishing to the relays failed // Handle cases where publishing to the relays failed
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
@ -170,7 +172,9 @@ export const signAndPublish = async (unsignedEvent: UnsignedEvent) => {
*/ */
export const sendDMUsingRandomKey = async ( export const sendDMUsingRandomKey = async (
message: string, message: string,
receiver: string receiver: string,
ndk: NDK,
publish: (event: NDKEvent) => Promise<string[]>
) => { ) => {
// Generate a random secret key for encrypting the message // Generate a random secret key for encrypting the message
const secretKey = generateSecretKey() const secretKey = generateSecretKey()
@ -201,11 +205,8 @@ export const sendDMUsingRandomKey = async (
// Finalize and sign the event using the generated secret key // Finalize and sign the event using the generated secret key
const signedEvent = finalizeEvent(unsignedEvent, secretKey) const signedEvent = finalizeEvent(unsignedEvent, secretKey)
// Publish the signed event (the encrypted DM) to the relays const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishedOnRelays = await RelayController.getInstance().publishDM( const publishedOnRelays = await publish(ndkEvent)
signedEvent,
receiver
)
// Handle cases where publishing to the relays failed // Handle cases where publishing to the relays failed
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {

View File

@ -135,3 +135,14 @@ export const handleModImageError = (
) => { ) => {
e.currentTarget.src = import.meta.env.VITE_FALLBACK_MOD_IMAGE e.currentTarget.src = import.meta.env.VITE_FALLBACK_MOD_IMAGE
} }
export const scrollIntoView = (el: HTMLElement | null) => {
if (el) {
setTimeout(() => {
el.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}, 100)
}
}