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
11 changed files with 154 additions and 507 deletions
Showing only changes of commit b69be4d755 - Show all commits

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'>
@ -827,8 +828,10 @@ const GameDropdown = ({
</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,7 +4,7 @@ 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 { UserRelaysType } from '../controllers'
import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import { useAppSelector, useDidMount, useNDKContext } from '../hooks'
import { appRoutes, getProfilePageRoute } from '../routes' import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
@ -22,6 +22,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
@ -368,7 +369,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 +442,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

@ -56,6 +56,7 @@ interface NDKContextType {
accumulatedZapAmount: number accumulatedZapAmount: number
hasZapped: boolean hasZapped: boolean
}> }>
publish: (event: NDKEvent) => Promise<string[]>
} }
// Create the context with an initial value of `null` // Create the context with an initial value of `null`
@ -352,6 +353,21 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
} }
} }
const publish = async (event: NDKEvent): Promise<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 []
})
}
return ( return (
<NDKContext.Provider <NDKContext.Provider
value={{ value={{
@ -362,7 +378,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
fetchEventsFromUserRelays, fetchEventsFromUserRelays,
fetchEventFromUserRelays, fetchEventFromUserRelays,
findMetadata, findMetadata,
getTotalZapAmount getTotalZapAmount,
publish
}} }}
> >
{children} {children}

View File

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

View File

@ -1,368 +0,0 @@
import { Event, Relay } from 'nostr-tools'
import { log, LogType, normalizeWebSocketURL, timeout } from '../utils'
import { MetadataController, UserRelaysType } from './metadata'
/**
* Singleton class to manage relay operations.
*/
export class RelayController {
private static instance: RelayController
private events = new Map<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
}
}

View File

@ -1,6 +1,6 @@
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 { 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'
@ -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

@ -53,7 +53,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 +61,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 +212,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)
@ -343,7 +343,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 +384,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 +450,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 +491,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 +661,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,
@ -760,7 +760,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 +773,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,9 +1,5 @@
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, useDidMount, useNDKContext, useReactions } from 'hooks'
import { useComments } from 'hooks/useComments' import { useComments } from 'hooks/useComments'
@ -47,6 +43,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 +79,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,27 +103,9 @@ 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,
UserRelaysType.Read
)
const commentatorWriteRelays = await metadataController.findUserRelays(
pubkey,
UserRelaysType.Write
)
const combinedRelays = [
...new Set(...modAuthorReadRelays, ...commentatorWriteRelays)
]
const publishedOnRelays =
await RelayController.getInstance().publishOnRelays(
signedEvent,
combinedRelays
)
if (publishedOnRelays.length === 0) { if (publishedOnRelays.length === 0) {
setCommentEvents((prev) => setCommentEvents((prev) =>
prev.map((event) => { prev.map((event) => {
@ -166,9 +146,22 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => {
}) })
) )
}, 15000) }, 15000)
})
.catch((err) => {
console.error('An error occurred in publishing comment', err)
setCommentEvents((prev) =>
prev.map((event) => {
if (event.id === signedEvent.id) {
return {
...event,
status: CommentEventStatus.Failed
}
} }
publish() return event
})
)
})
return true return true
} }

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,12 +1,8 @@
import { NDKRelayList } from '@nostr-dev-kit/ndk' import { NDKEvent, NDKRelayList, NDKRelayStatus } from '@nostr-dev-kit/ndk'
import { InputField } from 'components/Inputs' import { InputField } from 'components/Inputs'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { import { MetadataController, UserRelaysType } from 'controllers'
MetadataController, import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
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'
@ -16,6 +12,7 @@ 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)
@ -78,11 +75,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 +134,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 +205,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,18 +370,30 @@ 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)
}
})
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) setIsConnected(true)
} else { } else {
setIsConnected(false) setIsConnected(false)
} }
}) })
})
return ( return (
<div className='relayListItem'> <div className='relayListItem'>

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