Merge pull request 'Game name now visible on game mod cards and sliders. Mod post reactions now functional. Zap refactored.' (#33) from staging into master
All checks were successful
Release to Staging / build_and_release (push) Successful in 45s

Reviewed-on: #33
This commit is contained in:
freakoverse 2024-09-05 10:39:45 +00:00
commit 42a8eef755
12 changed files with 740 additions and 769 deletions

View File

@ -3,6 +3,7 @@ import { handleModImageError } from '../utils'
type ModCardProps = {
title: string
gameName: string
summary: string
imageUrl: string
link: string
@ -11,6 +12,7 @@ type ModCardProps = {
export const ModCard = ({
title,
gameName,
summary,
imageUrl,
link,
@ -36,6 +38,9 @@ export const ModCard = ({
<div className='cMMBody'>
<h3 className='cMMBodyTitle'>{title}</h3>
<p className='cMMBodyText'>{summary}</p>
<div className='cMMBodyGame'>
<p>{gameName}</p>
</div>
</div>
<div className='cMMFoot'>
<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 { useCallback, useState } from 'react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import {
MetadataController,
RelayController,
UserRelaysType,
ZapController
UserRelaysType
} from '../controllers'
import { useAppSelector, useDidMount } from '../hooks'
import { getProfilePageRoute } from '../routes'
import '../styles/author.css'
import '../styles/innerPage.css'
import '../styles/socialPosts.css'
import { PaymentRequest, UserProfile } from '../types'
import {
copyTextToClipboard,
formatNumber,
log,
LogType,
unformatNumber,
now
} from '../utils'
import { UserProfile } from '../types'
import { copyTextToClipboard, log, LogType, now } from '../utils'
import { LoadingSpinner } from './LoadingSpinner'
import { ZapButtons, ZapPresets, ZapQR } from './Zap'
import { getProfilePageRoute } from '../routes'
import { useNavigate } from 'react-router-dom'
import { ZapPopUp } from './Zap'
type Props = {
pubkey: string
@ -259,7 +251,7 @@ const QRButtonWithPopUp = ({ pubkey }: QRButtonWithPopUpProps) => {
</div>
{isOpen && (
<div id='PopUpMainQR' className='popUpMain'>
<div className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
@ -307,122 +299,6 @@ type ZapButtonWithPopUpProps = {
const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
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 (
<>
@ -442,77 +318,12 @@ const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
</svg>
</div>
{isOpen && (
<div id='PopUpMainZap' 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}>
<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}
<ZapPopUp
title='Tip/Zap'
receiver={pubkey}
handleClose={() => setIsOpen(false)}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
)}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</>
)
}

View File

@ -1,12 +1,20 @@
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 { toast } from 'react-toastify'
import { ZapController } from '../controllers'
import { useDidMount } from '../hooks'
import { MetadataController, ZapController } from '../controllers'
import { useAppSelector, useDidMount } from '../hooks'
import '../styles/popup.css'
import { PaymentRequest } from '../types'
import { copyTextToClipboard } from '../utils'
import { copyTextToClipboard, formatNumber, unformatNumber } from '../utils'
import { LoadingSpinner } from './LoadingSpinner'
type PresetAmountProps = {
label: string
@ -194,3 +202,223 @@ const Timer = React.memo(({ onTimerExpired }: TimerProps) => {
</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.
*
* This method connects to the application relay and a set of write 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.
* 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.
* @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.
* @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): Promise<string[]> => {
// Connect to the application relay specified by environment variable
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: 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()
// Retrieve the list of write relays for the event's public key
// Use a timeout to handle cases where retrieving write relays takes too long
const writeRelaysPromise = metadataController.findUserRelays(
event.pubkey,
UserRelaysType.Write
// Retrieve the list of relays for the specified user's public key
// A timeout is used to prevent long waits if the relay retrieval is delayed
const relaysPromise = metadataController.findUserRelays(
userHexKey || event.pubkey,
userRelaysType || UserRelaysType.Write
)
log(this.debug, LogType.Info, ` Finding user's write relays`)
// Use Promise.race to either get the write relay URLs or timeout
const writeRelayUrls = await Promise.race([
writeRelaysPromise,
timeout() // This is a custom timeout function that rejects the promise after a specified time
// Use Promise.race to either get the relay URLs or handle the timeout
const relayUrls = await Promise.race([
relaysPromise,
timeout() // Custom timeout function that rejects after a specified time
]).catch((err) => {
log(this.debug, LogType.Error, err)
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) => {
writeRelayUrls.push(url)
relayUrls.push(url)
})
// Connect to all write relays obtained from MetadataController
const relayPromises = writeRelayUrls.map((relayUrl) =>
// Attempt to connect to all write relays obtained from MetadataController
const relayPromises = relayUrls.map((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])
// 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) {
log(this.debug, LogType.Error, 'No relay is connected!')
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) => {
log(
this.debug,
@ -128,7 +143,7 @@ export class RelayController {
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
timeout(30000) // Set a timeout to handle slow publishing operations
])
.then((res) => {
log(
@ -137,7 +152,7 @@ export class RelayController {
`⬆️ nostr (${relay.url}): Publish result:`,
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) => {
log(
@ -153,16 +168,15 @@ export class RelayController {
await Promise.allSettled(publishPromises)
if (publishedOnRelays.length > 0) {
// if the event was successfully published to relays then check if it contains the `aTag`
// if so, then cache the event
// 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 published
// Return the list of relay URLs where the event was successfully published
return publishedOnRelays
}
@ -378,28 +392,19 @@ export class RelayController {
}
/**
* 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.
* 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 events 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.
* @returns A promise that resolves with an array of events.
*/
fetchEventFromUserRelays = async (
fetchEventsFromUserRelays = async (
filter: Filter,
hexKey: string,
userRelaysType: UserRelaysType
) => {
// 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
}
): Promise<Event[]> => {
// Get an instance of the MetadataController, which manages user metadata and relays
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
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 (

View File

@ -2,21 +2,18 @@ import {
init as initNostrLogin,
launch as launchNostrLoginDialog
} from 'nostr-login'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { Banner } from '../components/Banner'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap'
import { MetadataController, ZapController } from '../controllers'
import { useAppDispatch, useAppSelector } from '../hooks'
import { ZapPopUp } from '../components/Zap'
import { MetadataController } from '../controllers'
import { useAppDispatch, useAppSelector, useDidMount } from '../hooks'
import { appRoutes } from '../routes'
import { setAuth, setUser } from '../store/reducers/user'
import mainStyles from '../styles//main.module.scss'
import navStyles from '../styles/nav.module.scss'
import '../styles/popup.css'
import { PaymentRequest } from '../types'
import { formatNumber, npubToHex, unformatNumber } from '../utils'
import { npubToHex } from '../utils'
export const Header = () => {
const dispatch = useAppDispatch()
@ -173,7 +170,9 @@ export const Header = () => {
<div className={navStyles.NavMainBottom}>
<div className={mainStyles.ContainerMain}>
<div className={navStyles.NavMainBottomInside}>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}></div>
<div
className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}
></div>
<div className={navStyles.NavMainBottomInsideLinks}>
<Link
to={appRoutes.games}
@ -200,11 +199,24 @@ export const Header = () => {
Blog
</Link>
</div>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x" target="_blank">
<img src="https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif" width="15px" />
<div
className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}
>
<a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x'
target='_blank'
>
<img
src='https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif'
width='15px'
/>
</a>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://x.com/DEGMods" target="_blank">
<a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://x.com/DEGMods'
target='_blank'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
@ -215,7 +227,11 @@ export const Header = () => {
<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>
</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
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
@ -235,124 +251,13 @@ export const Header = () => {
}
const TipButtonWithDialog = React.memo(() => {
const [adminNpub, setAdminNpub] = useState<string | null>(null)
const [isOpen, setIsOpen] = useState(false)
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 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')
useDidMount(async () => {
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
setAdminNpub(metadataController.adminNpubs[0])
})
.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 (
<>
@ -371,91 +276,38 @@ const TipButtonWithDialog = React.memo(() => {
</svg>
Tip
</a>
{isOpen && (
<div id='PopUpMainZap' className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>Tip/Zap DEG Mods</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'>
<p
className='labelDescriptionMain'
style={{ textAlign: 'center' }}
>
If you don't want the development and maintenance of DEG
Mods to stop, then a tip helps!
{isOpen && adminNpub && (
<ZapPopUp
title='Tip/Zap DEG Mods'
receiver={adminNpub}
handleClose={() => setIsOpen(false)}
labelDescriptionMain={
<p className='labelDescriptionMain' 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}
/>
)}
}
lastNode={
<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>
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>
}
/>
)}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</>
)
})

View File

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

View File

@ -2,21 +2,15 @@ import Link from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { formatDate } from 'date-fns'
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import FsLightbox from 'fslightbox-react'
import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { BlogCard } from '../components/BlogCard'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { ProfileSection } from '../components/ProfileSection'
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap'
import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from '../components/Zap'
import {
MetadataController,
RelayController,
@ -48,10 +42,10 @@ import {
now,
npubToHex,
sendDMUsingRandomKey,
unformatNumber,
signAndPublish
signAndPublish,
unformatNumber
} from '../utils'
import FsLightbox from 'fslightbox-react'
import { REACTIONS } from '../constants'
export const ModPage = () => {
const { naddr } = useParams()
@ -1049,48 +1043,7 @@ const Interactions = ({ modDetails }: InteractionsProps) => {
</div>
</a>
<Zap modDetails={modDetails} />
<div
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>
<Reactions modDetails={modDetails} />
</div>
</div>
)
@ -1638,221 +1591,17 @@ const Zap = ({ modDetails }: ZapProps) => {
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
{isOpen && <ZapModal modDetails={modDetails} handleClose={setIsOpen} />}
</>
)
}
type ZapModalProps = {
modDetails: ModDetails
handleClose: Dispatch<SetStateAction<boolean>>
}
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}
{isOpen && (
<ZapPopUp
title='Tip/Zap'
receiver={modDetails.author}
eventId={modDetails.id}
aTag={modDetails.aTag}
handleClose={() => setIsOpen(false)}
lastNode={<ZapSite />}
notCloseAfterZap
/>
)}
{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
key={mod.id}
title={mod.title}
gameName={mod.game}
summary={mod.summary}
imageUrl={mod.featuredImageUrl}
link={`#${route}`}

View File

@ -14,6 +14,8 @@ import '../styles/settings.css'
import '../styles/styles.css'
import '../styles/write.css'
import { copyTextToClipboard } from '../utils'
import { MetadataController } from '../controllers'
import { useEffect, useState } from 'react'
export const SettingsPage = () => {
const location = useLocation()
@ -47,8 +49,21 @@ export const SettingsPage = () => {
const SettingTabs = () => {
const location = useLocation()
const [isAdmin, setIsAdmin] = useState(false)
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 = () => {
logout()
}
@ -120,6 +135,7 @@ const SettingTabs = () => {
</svg>
Preference
</Link>
{isAdmin && (
<Link
className={`btn btnMain btnMainAltText btnMainClear ${
location.pathname === appRoutes.settingsAdmin
@ -140,6 +156,7 @@ const SettingTabs = () => {
</svg>
Admin
</Link>
)}
</div>
{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 {
display: none;
bottom: -10px !important;

View File

@ -112,6 +112,20 @@
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 {
display: flex;
flex-direction: row;