Game name now visible on game mod cards and sliders. Mod post reactions now functional. Zap refactored. #33

Merged
freakoverse merged 13 commits from staging into master 2024-09-05 10:39:45 +00:00
12 changed files with 740 additions and 769 deletions

View File

@ -3,6 +3,7 @@ import { handleModImageError } from '../utils'
type ModCardProps = { type ModCardProps = {
title: string title: string
gameName: string
summary: string summary: string
imageUrl: string imageUrl: string
link: string link: string
@ -11,6 +12,7 @@ type ModCardProps = {
export const ModCard = ({ export const ModCard = ({
title, title,
gameName,
summary, summary,
imageUrl, imageUrl,
link, link,
@ -36,6 +38,9 @@ export const ModCard = ({
<div className='cMMBody'> <div className='cMMBody'>
<h3 className='cMMBodyTitle'>{title}</h3> <h3 className='cMMBodyTitle'>{title}</h3>
<p className='cMMBodyText'>{summary}</p> <p className='cMMBodyText'>{summary}</p>
<div className='cMMBodyGame'>
<p>{gameName}</p>
</div>
</div> </div>
<div className='cMMFoot'> <div className='cMMFoot'>
<div className='cMMFootReactions'> <div className='cMMFootReactions'>

View File

@ -1,30 +1,22 @@
import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { import {
MetadataController, MetadataController,
RelayController, RelayController,
UserRelaysType, UserRelaysType
ZapController
} from '../controllers' } from '../controllers'
import { useAppSelector, useDidMount } from '../hooks' import { useAppSelector, useDidMount } from '../hooks'
import { 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 { PaymentRequest, UserProfile } from '../types' import { UserProfile } from '../types'
import { import { copyTextToClipboard, log, LogType, now } from '../utils'
copyTextToClipboard,
formatNumber,
log,
LogType,
unformatNumber,
now
} from '../utils'
import { LoadingSpinner } from './LoadingSpinner' import { LoadingSpinner } from './LoadingSpinner'
import { ZapButtons, ZapPresets, ZapQR } from './Zap' import { ZapPopUp } from './Zap'
import { getProfilePageRoute } from '../routes'
import { useNavigate } from 'react-router-dom'
type Props = { type Props = {
pubkey: string pubkey: string
@ -259,7 +251,7 @@ const QRButtonWithPopUp = ({ pubkey }: QRButtonWithPopUpProps) => {
</div> </div>
{isOpen && ( {isOpen && (
<div id='PopUpMainQR' className='popUpMain'> <div className='popUpMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='popUpMainCardWrapper'> <div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'> <div className='popUpMainCard popUpMainCardQR'>
@ -307,122 +299,6 @@ type ZapButtonWithPopUpProps = {
const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [amount, setAmount] = useState<number>(0)
const [message, setMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
setIsOpen(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | null> => {
let userHexKey: string
setIsLoading(true)
setLoadingSpinnerDesc('Getting user pubkey')
if (userState.auth && userState.user?.pubkey) {
userHexKey = userState.user.pubkey as string
} else {
userHexKey = (await window.nostr?.getPublicKey()) as string
}
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return null
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const authorMetadata = await metadataController.findMetadata(pubkey)
if (!authorMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in admin metadata!')
return null
}
if (!authorMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in admin metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
authorMetadata.lud16,
amount,
authorMetadata.pubkey as string,
userHexKey,
message
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState, pubkey])
const handleSend = useCallback(async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setIsLoading(true)
setLoadingSpinnerDesc('Sending payment!')
const zapController = ZapController.getInstance()
if (await zapController.isWeblnProviderExists()) {
await zapController
.sendPayment(pr.pr)
.then(() => {
toast.success(`Successfully sent ${amount} sats!`)
handleClose()
})
.catch((err) => {
toast.error(err.message || err)
})
} else {
toast.warn('Webln is not present. Use QR code to send zap.')
setPaymentRequest(pr)
}
setIsLoading(false)
}, [amount, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return ( return (
<> <>
@ -442,77 +318,12 @@ const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
</svg> </svg>
</div> </div>
{isOpen && ( {isOpen && (
<div id='PopUpMainZap' className='popUpMain'> <ZapPopUp
<div className='ContainerMain'> title='Tip/Zap'
<div className='popUpMainCardWrapper'> receiver={pubkey}
<div className='popUpMainCard popUpMainCardQR'> handleClose={() => setIsOpen(false)}
<div className='popUpMainCardTop'> />
<div className='popUpMainCardTopInfo'>
<h3>Tip/Zap</h3>
</div>
<div className='popUpMainCardTopClose' onClick={handleClose}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' />
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
} }

View File

@ -1,12 +1,20 @@
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import React, { Dispatch, SetStateAction, useMemo } from 'react' import React, {
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useMemo,
useState
} 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 { ZapController } from '../controllers' import { MetadataController, ZapController } from '../controllers'
import { useDidMount } from '../hooks' import { useAppSelector, useDidMount } from '../hooks'
import '../styles/popup.css' import '../styles/popup.css'
import { PaymentRequest } from '../types' import { PaymentRequest } from '../types'
import { copyTextToClipboard } from '../utils' import { copyTextToClipboard, formatNumber, unformatNumber } from '../utils'
import { LoadingSpinner } from './LoadingSpinner'
type PresetAmountProps = { type PresetAmountProps = {
label: string label: string
@ -194,3 +202,223 @@ const Timer = React.memo(({ onTimerExpired }: TimerProps) => {
</div> </div>
) )
}) })
type ZapPopUpProps = {
title: string
labelDescriptionMain?: ReactNode
receiver: string
eventId?: string
aTag?: string
notCloseAfterZap?: boolean
lastNode?: ReactNode
handleClose: () => void
}
export const ZapPopUp = ({
title,
labelDescriptionMain,
receiver,
eventId,
aTag,
lastNode,
notCloseAfterZap,
handleClose
}: ZapPopUpProps) => {
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState<number>(0)
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | null> => {
let userHexKey: string
setIsLoading(true)
setLoadingSpinnerDesc('Getting user pubkey')
if (userState.auth && userState.user?.pubkey) {
userHexKey = userState.user.pubkey as string
} else {
userHexKey = (await window.nostr?.getPublicKey()) as string
}
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return null
}
setLoadingSpinnerDesc('finding receiver metadata')
const metadataController = await MetadataController.getInstance()
const receiverMetadata = await metadataController.findMetadata(receiver)
if (!receiverMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in receiver metadata!')
return null
}
if (!receiverMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in receiver metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
receiverMetadata.lud16,
amount,
receiverMetadata.pubkey as string,
userHexKey,
message,
eventId,
aTag
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState, receiver, eventId, aTag])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
const handleSend = useCallback(async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setIsLoading(true)
setLoadingSpinnerDesc('Sending payment!')
const zapController = ZapController.getInstance()
if (await zapController.isWeblnProviderExists()) {
await zapController
.sendPayment(pr.pr)
.then(() => {
toast.success(`Successfully sent ${amount} sats!`)
if (!notCloseAfterZap) {
handleClose()
}
})
.catch((err) => {
toast.error(err.message || err)
})
} else {
toast.warn('Webln is not present. Use QR code to send zap.')
setPaymentRequest(pr)
}
setIsLoading(false)
}, [amount, notCloseAfterZap, handleClose, generatePaymentRequest])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleQRClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
if (!notCloseAfterZap) {
handleClose()
}
}, [notCloseAfterZap, handleClose])
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<div className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>{title}</h3>
</div>
<div className='popUpMainCardTopClose' onClick={handleClose}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' />
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
{labelDescriptionMain}
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleQRClose}
handleQRExpiry={handleQRExpiry}
/>
)}
{lastNode}
</div>
</div>
</div>
</div>
</div>
</div>
</>
)
}

View File

@ -36,3 +36,21 @@ export const LANDING_PAGE_DATA = {
} }
] ]
} }
// we use this object to check if a user has reacted positively or negatively to a post
// reactions are kind 7 events and their content is either emoji icon or emoji shortcode
// Extend the following object as per need to include more emojis and shortcodes
// NOTE: In following object emojis and shortcode array are not interlinked.
// Both of these arrays can have separate items
export const REACTIONS = {
positive: {
emojis: ['+', '❤️', '💙', '💖', '💚','⭐', '🚀', '🫂', '🎉', '🥳', '🎊', '👍', '💪', '😎'],
shortCodes: [':red_heart:', ':blue_heart:', ':sparkling_heart:', ':green_heart:', ':star:', ':rocket:', ':people_hugging:', ':party_popper:',
':tada:', ':partying_face:', ':confetti_ball:', ':thumbs_up:', ':+1:', ':thumbsup:', ':thumbup:', ':flexed_biceps:', ':muscle:']
},
negative: {
emojis: ['-', '💩', '💔', '👎', '😠', '😞', '🤬', '🤢', '🤮', '🖕', '😡', '💢', '😠', '💀'],
shortCodes: [':poop:', ':shit:', ':poo:', ':hankey:', ':pile_of_poo:', ':broken_heart:', ':thumbsdown:', ':thumbdown:', ':nauseated_face:', ':sick:',
':face_vomiting:', ':vomiting_face:', ':face_with_open_mouth_vomiting:', ':middle_finger:', ':rage:', ':anger:', ':anger_symbol:', ':angry_face:', ':angry:',
':smiling_face_with_sunglasses:', ':sunglasses:', ':skull:', ':skeleton:']
}
}

View File

@ -62,62 +62,77 @@ export class RelayController {
/** /**
* Publishes an event to multiple relays. * Publishes an event to multiple relays.
* *
* This method connects to the application relay and a set of write relays * This method establishes a connection to the application relay specified by
* obtained from the `MetadataController`. It then publishes the event to * an environment variable and a set of relays obtained from the
* all connected relays and returns a list of relays where the event was successfully published. * `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 event - The event to be published.
* @returns A promise that resolves to an array of URLs of relays where the event was published, * @param userHexKey - The user's hexadecimal public key, used to retrieve their relays.
* or an empty array if no relays were connected or the event could not be published. * 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): Promise<string[]> => { publish = async (
// Connect to the application relay specified by environment variable 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) 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 // 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() const metadataController = await MetadataController.getInstance()
// Retrieve the list of write relays for the event's public key // Retrieve the list of relays for the specified user's public key
// Use a timeout to handle cases where retrieving write relays takes too long // A timeout is used to prevent long waits if the relay retrieval is delayed
const writeRelaysPromise = metadataController.findUserRelays( const relaysPromise = metadataController.findUserRelays(
event.pubkey, userHexKey || event.pubkey,
UserRelaysType.Write userRelaysType || UserRelaysType.Write
) )
log(this.debug, LogType.Info, ` Finding user's write relays`) log(this.debug, LogType.Info, ` Finding user's write relays`)
// Use Promise.race to either get the write relay URLs or timeout // Use Promise.race to either get the relay URLs or handle the timeout
const writeRelayUrls = await Promise.race([ const relayUrls = await Promise.race([
writeRelaysPromise, relaysPromise,
timeout() // This is a custom timeout function that rejects the promise after a specified time timeout() // Custom timeout function that rejects after a specified time
]).catch((err) => { ]).catch((err) => {
log(this.debug, LogType.Error, err) log(this.debug, LogType.Error, err)
return [] as string[] // Return an empty array if an error occurs return [] as string[] // Return an empty array if an error occurs
}) })
// push admin relay urls obtained from metadata controller to writeRelayUrls list // Add admin relay URLs from the metadata controller to the list of relay URLs
metadataController.adminRelays.forEach((url) => { metadataController.adminRelays.forEach((url) => {
writeRelayUrls.push(url) relayUrls.push(url)
}) })
// Connect to all write relays obtained from MetadataController // Attempt to connect to all write relays obtained from MetadataController
const relayPromises = writeRelayUrls.map((relayUrl) => const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl) this.connectRelay(relayUrl)
) )
// Wait for all relay connections to settle (either fulfilled or rejected) // Wait for all relay connection attempts to settle (either fulfilled or rejected)
await Promise.allSettled([appRelayPromise, ...relayPromises]) await Promise.allSettled([appRelayPromise, ...relayPromises])
// Check if any relays are connected; if not, log an error and return null // If no relays are connected, log an error and return an empty array
if (this.connectedRelays.length === 0) { if (this.connectedRelays.length === 0) {
log(this.debug, LogType.Error, 'No relay is connected!') log(this.debug, LogType.Error, 'No relay is connected!')
return [] return []
} }
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event const publishedOnRelays: string[] = [] // Track relays where the event was successfully published
// Create a promise for publishing the event to each connected relay // Create promises to publish the event to each connected relay
const publishPromises = this.connectedRelays.map((relay) => { const publishPromises = this.connectedRelays.map((relay) => {
log( log(
this.debug, this.debug,
@ -128,7 +143,7 @@ export class RelayController {
return Promise.race([ return Promise.race([
relay.publish(event), // Publish the event to the relay relay.publish(event), // Publish the event to the relay
timeout(30000) // Set a timeout to handle cases where publishing takes too long timeout(30000) // Set a timeout to handle slow publishing operations
]) ])
.then((res) => { .then((res) => {
log( log(
@ -137,7 +152,7 @@ export class RelayController {
`⬆️ nostr (${relay.url}): Publish result:`, `⬆️ nostr (${relay.url}): Publish result:`,
res res
) )
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays publishedOnRelays.push(relay.url) // Add successful relay URL to the list
}) })
.catch((err) => { .catch((err) => {
log( log(
@ -153,16 +168,15 @@ export class RelayController {
await Promise.allSettled(publishPromises) await Promise.allSettled(publishPromises)
if (publishedOnRelays.length > 0) { if (publishedOnRelays.length > 0) {
// if the event was successfully published to relays then check if it contains the `aTag` // If the event was successfully published to any relays, check if it contains an `aTag`
// if so, then cache the event // If the `aTag` is present, cache the event locally
const aTag = event.tags.find((item) => item[0] === 'a') const aTag = event.tags.find((item) => item[0] === 'a')
if (aTag && aTag[1]) { if (aTag && aTag[1]) {
this.events.set(aTag[1], event) this.events.set(aTag[1], event)
} }
} }
// Return the list of relay URLs where the event was published // Return the list of relay URLs where the event was successfully published
return publishedOnRelays return publishedOnRelays
} }
@ -378,28 +392,19 @@ export class RelayController {
} }
/** /**
* Fetches an event from the user's relays based on a specified filter. * Asynchronously retrieves multiple events from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the event using the provided filter. * The function first retrieves the user's relays, and then fetches the events using the provided filter.
* *
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key. * @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read). * @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves to the fetched event or null if the operation fails. * @returns A promise that resolves with an array of events.
*/ */
fetchEventFromUserRelays = async ( fetchEventsFromUserRelays = async (
filter: Filter, filter: Filter,
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => { ): Promise<Event[]> => {
// first check if event is present in cached map then return that
// otherwise query relays
if (filter['#a']) {
const aTag = filter['#a'][0]
const cachedEvent = this.events.get(aTag)
if (cachedEvent) return cachedEvent
}
// Get an instance of the MetadataController, which manages user metadata and relays // Get an instance of the MetadataController, which manages user metadata and relays
const metadataController = await MetadataController.getInstance() const metadataController = await MetadataController.getInstance()
@ -423,7 +428,55 @@ export class RelayController {
}) })
// Fetch the event from the user's relays using the provided filter and relay URLs // Fetch the event from the user's relays using the provided filter and relay URLs
return this.fetchEvent(filter, relayUrls) return this.fetchEvents(filter, relayUrls)
}
/**
* Fetches an event from the user's relays based on a specified filter.
* The function first retrieves the user's relays, and then fetches the event using the provided filter.
*
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
* @param hexKey - The hexadecimal representation of the user's public key.
* @param userRelaysType - The type of relays to search (e.g., write, read).
* @returns A promise that resolves to the fetched event or null if the operation fails.
*/
fetchEventFromUserRelays = async (
filter: Filter,
hexKey: string,
userRelaysType: UserRelaysType
): Promise<Event | null> => {
// first check if event is present in cached map then return that
// otherwise query relays
if (filter['#a']) {
const aTag = filter['#a'][0]
const cachedEvent = this.events.get(aTag)
if (cachedEvent) return cachedEvent
}
const events = await this.fetchEventsFromUserRelays(
filter,
hexKey,
userRelaysType
)
// Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at)
if (events.length > 0) {
const event = events[0]
// if the aTag was specified in filter then cache the fetched event before returning
if (filter['#a']) {
const aTag = filter['#a'][0]
this.events.set(aTag, event)
}
// return the event
return event
}
// return null if event array is empty
return null
} }
getTotalZapAmount = async ( getTotalZapAmount = async (

View File

@ -2,21 +2,18 @@ import {
init as initNostrLogin, init as initNostrLogin,
launch as launchNostrLoginDialog launch as launchNostrLoginDialog
} from 'nostr-login' } from 'nostr-login'
import React, { useCallback, useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { Banner } from '../components/Banner' import { Banner } from '../components/Banner'
import { LoadingSpinner } from '../components/LoadingSpinner' import { ZapPopUp } from '../components/Zap'
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' import { MetadataController } from '../controllers'
import { MetadataController, ZapController } from '../controllers' import { useAppDispatch, useAppSelector, useDidMount } from '../hooks'
import { useAppDispatch, useAppSelector } from '../hooks'
import { appRoutes } from '../routes' import { appRoutes } from '../routes'
import { setAuth, setUser } from '../store/reducers/user' import { setAuth, setUser } from '../store/reducers/user'
import mainStyles from '../styles//main.module.scss' import mainStyles from '../styles//main.module.scss'
import navStyles from '../styles/nav.module.scss' import navStyles from '../styles/nav.module.scss'
import '../styles/popup.css' import '../styles/popup.css'
import { PaymentRequest } from '../types' import { npubToHex } from '../utils'
import { formatNumber, npubToHex, unformatNumber } from '../utils'
export const Header = () => { export const Header = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -173,7 +170,9 @@ export const Header = () => {
<div className={navStyles.NavMainBottom}> <div className={navStyles.NavMainBottom}>
<div className={mainStyles.ContainerMain}> <div className={mainStyles.ContainerMain}>
<div className={navStyles.NavMainBottomInside}> <div className={navStyles.NavMainBottomInside}>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}></div> <div
className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}
></div>
<div className={navStyles.NavMainBottomInsideLinks}> <div className={navStyles.NavMainBottomInsideLinks}>
<Link <Link
to={appRoutes.games} to={appRoutes.games}
@ -200,31 +199,48 @@ export const Header = () => {
Blog Blog
</Link> </Link>
</div> </div>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}> <div
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x" target="_blank"> className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}
<img src="https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif" width="15px" /> >
<a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x'
target='_blank'
>
<img
src='https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif'
width='15px'
/>
</a> </a>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://x.com/DEGMods" target="_blank"> <a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://x.com/DEGMods'
target='_blank'
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512' viewBox='0 0 512 512'
width='1em' width='1em'
height='1em' height='1em'
fill='currentColor' fill='currentColor'
> >
<path d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'></path> <path d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'></path>
</svg> </svg>
</a> </a>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://www.youtube.com/@DEGModsDotCom" target="_blank"> <a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://www.youtube.com/@DEGModsDotCom'
target='_blank'
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512' viewBox='0 0 512 512'
width='1em' width='1em'
height='1em' height='1em'
fill='currentColor' fill='currentColor'
> >
<path d='M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z'></path> <path d='M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z'></path>
</svg> </svg>
</a> </a>
</div> </div>
</div> </div>
@ -235,124 +251,13 @@ export const Header = () => {
} }
const TipButtonWithDialog = React.memo(() => { const TipButtonWithDialog = React.memo(() => {
const [adminNpub, setAdminNpub] = useState<string | null>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false) useDidMount(async () => {
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const metadataController = await MetadataController.getInstance()
setAdminNpub(metadataController.adminNpubs[0])
const [amount, setAmount] = useState<number>(0) })
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
setIsOpen(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | null> => {
let userHexKey: string
setIsLoading(true)
setLoadingSpinnerDesc('Getting user pubkey')
if (userState.auth && userState.user?.pubkey) {
userHexKey = userState.user.pubkey as string
} else {
userHexKey = (await window.nostr?.getPublicKey()) as string
}
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return null
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const adminMetadata = await metadataController.findAdminMetadata()
if (!adminMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in admin metadata!')
return null
}
if (!adminMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in admin metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
adminMetadata.lud16,
amount,
adminMetadata.pubkey as string,
userHexKey,
message
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState])
const handleSend = useCallback(async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setIsLoading(true)
setLoadingSpinnerDesc('Sending payment!')
const zapController = ZapController.getInstance()
if (await zapController.isWeblnProviderExists()) {
await zapController
.sendPayment(pr.pr)
.then(() => {
toast.success(`Successfully sent ${amount} sats!`)
handleClose()
})
.catch((err) => {
toast.error(err.message || err)
})
} else {
toast.warn('Webln is not present. Use QR code to send zap.')
setPaymentRequest(pr)
}
setIsLoading(false)
}, [amount, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return ( return (
<> <>
@ -371,91 +276,38 @@ const TipButtonWithDialog = React.memo(() => {
</svg> </svg>
Tip Tip
</a> </a>
{isOpen && ( {isOpen && adminNpub && (
<div id='PopUpMainZap' className='popUpMain'> <ZapPopUp
<div className='ContainerMain'> title='Tip/Zap DEG Mods'
<div className='popUpMainCardWrapper'> receiver={adminNpub}
<div className='popUpMainCard popUpMainCardQR'> handleClose={() => setIsOpen(false)}
<div className='popUpMainCardTop'> labelDescriptionMain={
<div className='popUpMainCardTopInfo'> <p className='labelDescriptionMain' style={{ textAlign: 'center' }}>
<h3>Tip/Zap DEG Mods</h3> If you don't want the development and maintenance of DEG Mods to
</div> stop, then a tip helps!
<div className='popUpMainCardTopClose' onClick={handleClose}> </p>
<svg }
xmlns='http://www.w3.org/2000/svg' lastNode={
viewBox='-96 0 512 512' <div className='BTCAddressPopZap'>
width='1em' <p>
height='1em' DEG Mod's Silent Payment Bitcoin Address (Be careful.{' '}
fill='currentColor' <a
style={{ zIndex: 1 }} href='https://youtu.be/payDPlHzp58?t=215'
> className='linkMain'
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' /> target='_blank'
</svg> >
</div> Learn more
</div> </a>
<div className='pUMCB_Zaps'> ):
<div className='pUMCB_ZapsInside'> <br />
<div className='pUMCB_ZapsInsideAmount'> <span className='BTCAddressPopZapTextSpan'>
<div className='inputLabelWrapperMain'> sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd
<p </span>
className='labelDescriptionMain' </p>
style={{ textAlign: 'center' }}
>
If you don't want the development and maintenance of DEG
Mods to stop, then a tip helps!
</p>
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/>
)}
<div className='BTCAddressPopZap'>
<p>
DEG Mod's Silent Payment Bitcoin Address (Be careful. <a href='https://youtu.be/payDPlHzp58?t=215' className='linkMain' target='_blank'>Learn more</a>):<br />
<span className='BTCAddressPopZapTextSpan'>sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd</span>
</p>
</div>
</div>
</div>
</div>
</div> </div>
</div> }
</div> />
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
}) })

View File

@ -178,6 +178,10 @@ const SlideContent = ({ naddr }: SlideContentProps) => {
{mod.summary} {mod.summary}
<br /> <br />
</p> </p>
<p className='IBMSMSCWSInfoText IBMSMSCWSInfoText2'>
{mod.game}
<br />
</p>
<div className='IBMSMSliderContainerWrapperSliderAction'> <div className='IBMSMSliderContainerWrapperSliderAction'>
<a <a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn' className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
@ -235,6 +239,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
return ( return (
<ModCard <ModCard
title={mod.title} title={mod.title}
gameName={mod.game}
summary={mod.summary} summary={mod.summary}
imageUrl={mod.featuredImageUrl} imageUrl={mod.featuredImageUrl}
link={`#${route}`} link={`#${route}`}
@ -283,6 +288,7 @@ const DisplayLatestMods = () => {
<ModCard <ModCard
key={mod.id} key={mod.id}
title={mod.title} title={mod.title}
gameName={mod.game}
summary={mod.summary} summary={mod.summary}
imageUrl={mod.featuredImageUrl} imageUrl={mod.featuredImageUrl}
link={`#${route}`} link={`#${route}`}

View File

@ -2,21 +2,15 @@ import Link from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react' import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { formatDate } from 'date-fns' import { formatDate } from 'date-fns'
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import FsLightbox from 'fslightbox-react'
import { import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools'
Dispatch, import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
SetStateAction,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' 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 { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from '../components/Zap'
import { import {
MetadataController, MetadataController,
RelayController, RelayController,
@ -48,10 +42,10 @@ import {
now, now,
npubToHex, npubToHex,
sendDMUsingRandomKey, sendDMUsingRandomKey,
unformatNumber, signAndPublish,
signAndPublish unformatNumber
} from '../utils' } from '../utils'
import FsLightbox from 'fslightbox-react' import { REACTIONS } from '../constants'
export const ModPage = () => { export const ModPage = () => {
const { naddr } = useParams() const { naddr } = useParams()
@ -1049,48 +1043,7 @@ const Interactions = ({ modDetails }: InteractionsProps) => {
</div> </div>
</a> </a>
<Zap modDetails={modDetails} /> <Zap modDetails={modDetails} />
<div <Reactions modDetails={modDetails} />
id='reactUp'
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp IBMSMSMBSS_D_CRUActive'
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>4.2k</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div
id='reactDown'
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown'
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>69</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
</div> </div>
</div> </div>
) )
@ -1638,221 +1591,17 @@ const Zap = ({ modDetails }: ZapProps) => {
<div className='IBMSMSMBSSCL_CAElementLoad'></div> <div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div> </div>
</div> </div>
{isOpen && <ZapModal modDetails={modDetails} handleClose={setIsOpen} />} {isOpen && (
</> <ZapPopUp
) title='Tip/Zap'
} receiver={modDetails.author}
eventId={modDetails.id}
type ZapModalProps = { aTag={modDetails.aTag}
modDetails: ModDetails handleClose={() => setIsOpen(false)}
handleClose: Dispatch<SetStateAction<boolean>> lastNode={<ZapSite />}
} notCloseAfterZap
const ZapModal = ({ modDetails, handleClose }: ZapModalProps) => {
return (
<div id='PopUpMainZapSplitAlt' className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>Tip/Zap</h3>
</div>
<div
className='popUpMainCardTopClose'
onClick={() => handleClose(false)}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<ZapMod modDetails={modDetails} />
<ZapSite />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
type ZapModProps = {
modDetails: ModDetails
}
const ZapMod = ({ modDetails }: ZapModProps) => {
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState(0)
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | null> => {
let userHexKey: string
setIsLoading(true)
setLoadingSpinnerDesc('Getting user pubkey')
if (userState.auth && userState.user?.pubkey) {
userHexKey = userState.user.pubkey as string
} else {
userHexKey = (await window.nostr?.getPublicKey()) as string
}
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return null
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const authorMetadata = await metadataController.findMetadata(
modDetails.author
)
if (!authorMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in author metadata!')
return null
}
if (!authorMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in author metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
authorMetadata.lud16,
amount,
authorMetadata.pubkey as string,
userHexKey,
message,
modDetails.id,
modDetails.aTag
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState, modDetails])
const handleSend = useCallback(async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setIsLoading(true)
setLoadingSpinnerDesc('Sending payment!')
const zapController = ZapController.getInstance()
if (await zapController.isWeblnProviderExists()) {
await zapController
.sendPayment(pr.pr)
.then(() => {
toast.success(`Successfully sent ${amount} sats!`)
handleClose()
})
.catch((err) => {
toast.error(err.message || err)
})
} else {
toast.warn('Webln is not present. Use QR code to send zap.')
setPaymentRequest(pr)
}
setIsLoading(false)
}, [amount, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return (
<>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Amount (Satoshis)</label>
<input
type='text'
className='inputMain'
inputMode='numeric'
placeholder='69 or 420? or 69,420?'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Message (optional)</label>
<input
type='text'
className='inputMain'
placeholder='This is awesome!'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/> />
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
} }
@ -2030,3 +1779,213 @@ const ZapSite = () => {
</> </>
) )
} }
type ReactionsProps = {
modDetails: ModDetails
}
const Reactions = ({ modDetails }: ReactionsProps) => {
const [isReactionInProgress, setIsReactionInProgress] = useState(false)
const [isDataLoaded, setIsDataLoaded] = useState(false)
const [reactionEvents, setReactionEvents] = useState<Event[]>([])
const userState = useAppSelector((state) => state.user)
useDidMount(() => {
const filter: Filter = {
kinds: [kinds.Reaction],
'#a': [modDetails.aTag]
}
RelayController.getInstance()
.fetchEventsFromUserRelays(filter, modDetails.author, UserRelaysType.Read)
.then((events) => {
setReactionEvents(events)
})
.finally(() => {
setIsDataLoaded(true)
})
})
const checkHasPositiveReaction = () => {
return (
!!userState.auth &&
reactionEvents.some(
(event) =>
event.pubkey === userState.user?.pubkey &&
(REACTIONS.positive.emojis.includes(event.content) ||
REACTIONS.positive.shortCodes.includes(event.content))
)
)
}
const checkHasNegativeReaction = () => {
return (
!!userState.auth &&
reactionEvents.some(
(event) =>
event.pubkey === userState.user?.pubkey &&
(REACTIONS.negative.emojis.includes(event.content) ||
REACTIONS.negative.shortCodes.includes(event.content))
)
)
}
const getPubkey = async () => {
let hexPubkey: string
if (userState.auth && userState.user?.pubkey) {
hexPubkey = userState.user.pubkey as string
} else {
hexPubkey = (await window.nostr?.getPublicKey()) as string
}
if (!hexPubkey) {
toast.error('Could not get pubkey')
return null
}
return hexPubkey
}
const handleReaction = async (isPositive?: boolean) => {
if (
!isDataLoaded ||
checkHasPositiveReaction() ||
checkHasNegativeReaction()
)
return
// Check if the voting process is already in progress
if (isReactionInProgress) return
// Set the flag to indicate that the voting process has started
setIsReactionInProgress(true)
try {
const pubkey = await getPubkey()
if (!pubkey) return
const unsignedEvent: UnsignedEvent = {
kind: kinds.Reaction,
created_at: now(),
content: isPositive ? '+' : '-',
pubkey,
tags: [
['e', modDetails.id],
['p', modDetails.author],
['a', modDetails.aTag]
]
}
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the reaction event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
return null
})
if (!signedEvent) return
setReactionEvents((prev) => [...prev, signedEvent])
const publishedOnRelays = await RelayController.getInstance().publish(
signedEvent as Event,
modDetails.author,
UserRelaysType.Read
)
if (publishedOnRelays.length === 0) {
log(
true,
LogType.Error,
'Failed to publish reaction event on any relay'
)
return
}
} finally {
setIsReactionInProgress(false)
}
}
const { likesCount, disLikesCount } = useMemo(() => {
let positiveCount = 0
let negativeCount = 0
reactionEvents.forEach((event) => {
if (
REACTIONS.positive.emojis.includes(event.content) ||
REACTIONS.positive.shortCodes.includes(event.content)
) {
positiveCount++
} else if (
REACTIONS.negative.emojis.includes(event.content) ||
REACTIONS.negative.shortCodes.includes(event.content)
) {
negativeCount++
}
})
return {
likesCount: abbreviateNumber(positiveCount),
disLikesCount: abbreviateNumber(negativeCount)
}
}, [reactionEvents])
const hasReactedPositively = checkHasPositiveReaction()
const hasReactedNegatively = checkHasNegativeReaction()
if (!isDataLoaded) return null
return (
<>
<div
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp ${
hasReactedPositively ? 'IBMSMSMBSS_D_CRUActive' : ''
}`}
onClick={() => handleReaction(true)}
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>{likesCount}</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${
hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : ''
}`}
onClick={() => handleReaction()}
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>{disLikesCount}</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
</>
)
}

View File

@ -220,6 +220,7 @@ export const ModsPage = () => {
<ModCard <ModCard
key={mod.id} key={mod.id}
title={mod.title} title={mod.title}
gameName={mod.game}
summary={mod.summary} summary={mod.summary}
imageUrl={mod.featuredImageUrl} imageUrl={mod.featuredImageUrl}
link={`#${route}`} link={`#${route}`}

View File

@ -14,6 +14,8 @@ import '../styles/settings.css'
import '../styles/styles.css' import '../styles/styles.css'
import '../styles/write.css' import '../styles/write.css'
import { copyTextToClipboard } from '../utils' import { copyTextToClipboard } from '../utils'
import { MetadataController } from '../controllers'
import { useEffect, useState } from 'react'
export const SettingsPage = () => { export const SettingsPage = () => {
const location = useLocation() const location = useLocation()
@ -47,8 +49,21 @@ export const SettingsPage = () => {
const SettingTabs = () => { const SettingTabs = () => {
const location = useLocation() const location = useLocation()
const [isAdmin, setIsAdmin] = useState(false)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
useEffect(() => {
MetadataController.getInstance().then((controller) => {
if (userState.auth && userState.user?.npub) {
setIsAdmin(
controller.adminNpubs.includes(userState.user.npub as string)
)
} else {
setIsAdmin(false)
}
})
}, [userState])
const handleSignOut = () => { const handleSignOut = () => {
logout() logout()
} }
@ -120,26 +135,28 @@ const SettingTabs = () => {
</svg> </svg>
Preference Preference
</Link> </Link>
<Link {isAdmin && (
className={`btn btnMain btnMainAltText btnMainClear ${ <Link
location.pathname === appRoutes.settingsAdmin className={`btn btnMain btnMainAltText btnMainClear ${
? 'btnMainClearActive' location.pathname === appRoutes.settingsAdmin
: '' ? 'btnMainClearActive'
}`} : ''
role='button' }`}
to={appRoutes.settingsAdmin} role='button'
> to={appRoutes.settingsAdmin}
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -32 576 576'
width='1em'
height='1em'
fill='currentColor'
> >
<path d='M560 448H512V113.5c0-27.25-21.5-49.5-48-49.5L352 64.01V128h96V512h112c8.875 0 16-7.125 16-15.1v-31.1C576 455.1 568.9 448 560 448zM280.3 1.007l-192 49.75C73.1 54.51 64 67.76 64 82.88V448H16c-8.875 0-16 7.125-16 15.1v31.1C0 504.9 7.125 512 16 512H320V33.13C320 11.63 300.5-4.243 280.3 1.007zM232 288c-13.25 0-24-14.37-24-31.1c0-17.62 10.75-31.1 24-31.1S256 238.4 256 256C256 273.6 245.3 288 232 288z'></path> <svg
</svg> xmlns='http://www.w3.org/2000/svg'
Admin viewBox='0 -32 576 576'
</Link> width='1em'
height='1em'
fill='currentColor'
>
<path d='M560 448H512V113.5c0-27.25-21.5-49.5-48-49.5L352 64.01V128h96V512h112c8.875 0 16-7.125 16-15.1v-31.1C576 455.1 568.9 448 560 448zM280.3 1.007l-192 49.75C73.1 54.51 64 67.76 64 82.88V448H16c-8.875 0-16 7.125-16 15.1v31.1C0 504.9 7.125 512 16 512H320V33.13C320 11.63 300.5-4.243 280.3 1.007zM232 288c-13.25 0-24-14.37-24-31.1c0-17.62 10.75-31.1 24-31.1S256 238.4 256 256C256 273.6 245.3 288 232 288z'></path>
</svg>
Admin
</Link>
)}
</div> </div>
{userState.auth && {userState.auth &&

View File

@ -274,6 +274,13 @@
} }
} }
.IBMSMSCWSInfoText.IBMSMSCWSInfoText2 {
-webkit-line-clamp: 1;
border-top: solid 1px rgba(255,255,255,0.1);
padding: 10px 0 0 5px;
flex-grow: 0;
}
.swiper-pagination { .swiper-pagination {
display: none; display: none;
bottom: -10px !important; bottom: -10px !important;

View File

@ -112,6 +112,20 @@
line-height: 1.5; line-height: 1.5;
} }
.cMMBodyGame {
border-radius: 5px;
padding: 5px 10px;
flex-direction: row;
justify-content: start;
align-items: center;
font-size: 14px;
background: rgba(255,255,255,0.05);
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 1;
}
.cMMFootReactions { .cMMFootReactions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;