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
All checks were successful
Release to Staging / build_and_release (push) Successful in 45s
Reviewed-on: #33
This commit is contained in:
commit
42a8eef755
@ -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'>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={pubkey}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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:']
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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,31 +199,48 @@ 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'
|
||||
width='1em'
|
||||
height='1em'
|
||||
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>
|
||||
</svg>
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
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>
|
||||
</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'
|
||||
width='1em'
|
||||
height='1em'
|
||||
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>
|
||||
</svg>
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
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>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -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')
|
||||
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)
|
||||
}
|
||||
useDidMount(async () => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
setAdminNpub(metadataController.adminNpubs[0])
|
||||
})
|
||||
|
||||
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!
|
||||
</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>
|
||||
{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>
|
||||
}
|
||||
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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -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}`}
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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}`}
|
||||
|
@ -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,26 +135,28 @@ const SettingTabs = () => {
|
||||
</svg>
|
||||
Preference
|
||||
</Link>
|
||||
<Link
|
||||
className={`btn btnMain btnMainAltText btnMainClear ${
|
||||
location.pathname === appRoutes.settingsAdmin
|
||||
? 'btnMainClearActive'
|
||||
: ''
|
||||
}`}
|
||||
role='button'
|
||||
to={appRoutes.settingsAdmin}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -32 576 576'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
{isAdmin && (
|
||||
<Link
|
||||
className={`btn btnMain btnMainAltText btnMainClear ${
|
||||
location.pathname === appRoutes.settingsAdmin
|
||||
? 'btnMainClearActive'
|
||||
: ''
|
||||
}`}
|
||||
role='button'
|
||||
to={appRoutes.settingsAdmin}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userState.auth &&
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user