Relay operations refactored with NDK for publishing events (+ more. Wrapped up refactoring), pagination scroll up on click, body scroll disable/enable when popups appear/disappear, nsfw tag shown on mod cards if mod post is nsfw #92

Merged
freakoverse merged 19 commits from staging into master 2024-10-21 14:17:02 +00:00
6 changed files with 87 additions and 176 deletions
Showing only changes of commit 82b87b3e32 - Show all commits

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

View File

@ -5,7 +5,8 @@ import NDK, {
NDKKind, NDKKind,
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'
@ -36,16 +37,25 @@ interface NDKContextType {
fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]> fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
fetchEvent: (filter: NDKFilter) => Promise<NDKEvent | null> fetchEvent: (filter: NDKFilter) => 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
}>
} }
// Create the context with an initial value of `null` // Create the context with an initial value of `null`
@ -218,10 +228,10 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
* @returns A promise that resolves with an array of events. * @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) => {
@ -266,7 +276,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
* @returns A promise that resolves to the fetched event or null if the operation fails. * @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
) => { ) => {
@ -296,6 +306,52 @@ 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
}
}
return ( return (
<NDKContext.Provider <NDKContext.Provider
value={{ value={{
@ -305,7 +361,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
fetchEvent, fetchEvent,
fetchEventsFromUserRelays, fetchEventsFromUserRelays,
fetchEventFromUserRelays, fetchEventFromUserRelays,
findMetadata findMetadata,
getTotalZapAmount
}} }}
> >
{children} {children}

View File

@ -1,11 +1,5 @@
import { Event, Filter, kinds, nip57, Relay } from 'nostr-tools' import { Event, Relay } from 'nostr-tools'
import { import { log, LogType, normalizeWebSocketURL, timeout } from '../utils'
extractZapAmount,
log,
LogType,
normalizeWebSocketURL,
timeout
} from '../utils'
import { MetadataController, UserRelaysType } from './metadata' import { MetadataController, UserRelaysType } from './metadata'
/** /**
@ -371,122 +365,4 @@ export class RelayController {
// Return the list of relay URLs where the event was successfully published // Return the list of relay URLs where the event was successfully published
return publishedOnRelays return publishedOnRelays
} }
getTotalZapAmount = async (
user: string,
eTag: string,
aTag?: string,
currentLoggedInUser?: string
) => {
const metadataController = await MetadataController.getInstance()
const relayUrls = await metadataController.findUserRelays(
user,
UserRelaysType.Read
)
const appRelay = import.meta.env.VITE_APP_RELAY
if (!relayUrls.includes(appRelay)) {
relayUrls.push(appRelay)
}
// Connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl)
)
// Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(relayPromises)
// Extract non-null values from fulfilled promises in a single pass
const relays = results.reduce<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

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

@ -496,15 +496,14 @@ 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)
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() getTotalZapAmount(
.getTotalZapAmount(
props.pubkey, props.pubkey,
props.id, props.id,
undefined, undefined,

View File

@ -1,6 +1,5 @@
import { ZapSplit } from 'components/Zap' import { ZapSplit } from 'components/Zap'
import { RelayController } from 'controllers' import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
import { useAppSelector, useDidMount } 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,15 +11,14 @@ 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)
useDidMount(() => { useDidMount(() => {
RelayController.getInstance() getTotalZapAmount(
.getTotalZapAmount(
modDetails.author, modDetails.author,
modDetails.id, modDetails.id,
modDetails.aTag, modDetails.aTag,