feat: implemented comment feature and refactored mod page
All checks were successful
Release to Staging / build_and_release (push) Successful in 47s
All checks were successful
Release to Staging / build_and_release (push) Successful in 47s
This commit is contained in:
parent
458ad744e4
commit
a3a022c436
@ -13,7 +13,12 @@ import { MetadataController, ZapController } from '../controllers'
|
||||
import { useAppSelector, useDidMount } from '../hooks'
|
||||
import '../styles/popup.css'
|
||||
import { PaymentRequest } from '../types'
|
||||
import { copyTextToClipboard, formatNumber, unformatNumber } from '../utils'
|
||||
import {
|
||||
copyTextToClipboard,
|
||||
formatNumber,
|
||||
getZapAmount,
|
||||
unformatNumber
|
||||
} from '../utils'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
|
||||
type PresetAmountProps = {
|
||||
@ -114,15 +119,25 @@ type ZapQRProps = {
|
||||
paymentRequest: PaymentRequest
|
||||
handleClose: () => void
|
||||
handleQRExpiry: () => void
|
||||
setTotalZapAmount?: Dispatch<SetStateAction<number>>
|
||||
}
|
||||
|
||||
export const ZapQR = React.memo(
|
||||
({ paymentRequest, handleClose, handleQRExpiry }: ZapQRProps) => {
|
||||
({
|
||||
paymentRequest,
|
||||
handleClose,
|
||||
handleQRExpiry,
|
||||
setTotalZapAmount
|
||||
}: ZapQRProps) => {
|
||||
useDidMount(() => {
|
||||
ZapController.getInstance()
|
||||
.pollZapReceipt(paymentRequest)
|
||||
.then(() => {
|
||||
.then((zapReceipt) => {
|
||||
toast.success(`Successfully sent sats!`)
|
||||
if (setTotalZapAmount) {
|
||||
const amount = getZapAmount(zapReceipt)
|
||||
setTotalZapAmount((prev) => prev + amount)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
@ -211,6 +226,7 @@ type ZapPopUpProps = {
|
||||
aTag?: string
|
||||
notCloseAfterZap?: boolean
|
||||
lastNode?: ReactNode
|
||||
setTotalZapAmount?: Dispatch<SetStateAction<number>>
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
@ -222,6 +238,7 @@ export const ZapPopUp = ({
|
||||
aTag,
|
||||
lastNode,
|
||||
notCloseAfterZap,
|
||||
setTotalZapAmount,
|
||||
handleClose
|
||||
}: ZapPopUpProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@ -318,6 +335,10 @@ export const ZapPopUp = ({
|
||||
.sendPayment(pr.pr)
|
||||
.then(() => {
|
||||
toast.success(`Successfully sent ${amount} sats!`)
|
||||
if (setTotalZapAmount) {
|
||||
setTotalZapAmount((prev) => prev + amount)
|
||||
}
|
||||
|
||||
if (!notCloseAfterZap) {
|
||||
handleClose()
|
||||
}
|
||||
@ -331,7 +352,13 @@ export const ZapPopUp = ({
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}, [amount, notCloseAfterZap, handleClose, generatePaymentRequest])
|
||||
}, [
|
||||
amount,
|
||||
notCloseAfterZap,
|
||||
handleClose,
|
||||
generatePaymentRequest,
|
||||
setTotalZapAmount
|
||||
])
|
||||
|
||||
const handleQRExpiry = useCallback(() => {
|
||||
setPaymentRequest(undefined)
|
||||
@ -410,6 +437,7 @@ export const ZapPopUp = ({
|
||||
paymentRequest={paymentRequest}
|
||||
handleClose={handleQRClose}
|
||||
handleQRExpiry={handleQRExpiry}
|
||||
setTotalZapAmount={setTotalZapAmount}
|
||||
/>
|
||||
)}
|
||||
{lastNode}
|
||||
|
@ -2,7 +2,7 @@ import NDK, { getRelayListForUser, NDKList, NDKUser } from '@nostr-dev-kit/ndk'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { MuteLists } from '../types'
|
||||
import { UserProfile } from '../types/user'
|
||||
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||
import { hexToNpub, log, LogType, npubToHex, timeout } from '../utils'
|
||||
|
||||
export enum UserRelaysType {
|
||||
Read = 'readRelayUrls',
|
||||
@ -122,13 +122,22 @@ export class MetadataController {
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType = UserRelaysType.Both
|
||||
) => {
|
||||
const ndkRelayList = await getRelayListForUser(hexKey, this.ndk)
|
||||
log(true, LogType.Info, `ℹ Finding user's relays`, hexKey, userRelaysType)
|
||||
|
||||
if (!ndkRelayList) {
|
||||
throw new Error(`Couldn't found user's relay list`)
|
||||
}
|
||||
const ndkRelayListPromise = await getRelayListForUser(hexKey, this.ndk)
|
||||
|
||||
// Use Promise.race to either get the NDKRelayList instance or handle the timeout
|
||||
return await Promise.race([
|
||||
ndkRelayListPromise,
|
||||
timeout() // Custom timeout function that rejects after a specified time
|
||||
])
|
||||
.then((ndkRelayList) => {
|
||||
return ndkRelayList[userRelaysType]
|
||||
})
|
||||
.catch((err) => {
|
||||
log(true, LogType.Error, err)
|
||||
return [] as string[] // Return an empty array if an error occurs
|
||||
})
|
||||
}
|
||||
|
||||
public getMuteLists = async (
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Event, Filter, kinds, Relay } from 'nostr-tools'
|
||||
import { ModDetails } from '../types'
|
||||
import {
|
||||
extractZapAmount,
|
||||
log,
|
||||
@ -94,23 +93,11 @@ export class RelayController {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
// 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(
|
||||
const relayUrls = await 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 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
|
||||
})
|
||||
|
||||
// Add admin relay URLs from the metadata controller to the list of relay URLs
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
relayUrls.push(url)
|
||||
@ -200,23 +187,11 @@ export class RelayController {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
// Retrieve the list of read relays for the receiver
|
||||
// Use a timeout to handle cases where retrieving read relays takes too long
|
||||
const readRelaysPromise = metadataController.findUserRelays(
|
||||
const readRelayUrls = await metadataController.findUserRelays(
|
||||
receiver,
|
||||
UserRelaysType.Read
|
||||
)
|
||||
|
||||
log(this.debug, LogType.Info, `ℹ Finding receiver's read relays`)
|
||||
|
||||
// Use Promise.race to either get the read relay URLs or timeout
|
||||
const readRelayUrls = await Promise.race([
|
||||
readRelaysPromise,
|
||||
timeout() // This is a custom timeout function that rejects the promise 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 readRelayUrls list
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
readRelayUrls.push(url)
|
||||
@ -277,6 +252,112 @@ export class RelayController {
|
||||
return publishedOnRelays
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes an event to multiple relays.
|
||||
*
|
||||
* This method establishes a connection to the application relay specified by
|
||||
* an environment variable and a set of relays provided as argument.
|
||||
* It attempts to publish the event to all connected relays
|
||||
* and returns a list of URLs of relays where the event was successfully published.
|
||||
*
|
||||
* If the process of publishing the event takes too long,
|
||||
* it handles the timeout to prevent blocking the operation.
|
||||
*
|
||||
* @param event - The event to be published.
|
||||
* @param relayUrls - The array of relayUrl where event should be published
|
||||
* @returns A promise that resolves to an array of URLs of relays where the event
|
||||
* was published, or an empty array if no relays were connected or the
|
||||
* event could not be published.
|
||||
*/
|
||||
publishOnRelays = async (
|
||||
event: Event,
|
||||
relayUrls: string[]
|
||||
): Promise<string[]> => {
|
||||
const appRelay = import.meta.env.VITE_APP_RELAY
|
||||
|
||||
if (!relayUrls.includes(appRelay)) {
|
||||
/**
|
||||
* NOTE: To avoid side-effects on external relayUrls array passed as argument
|
||||
* re-assigned relayUrls with added sigit relay instead of just appending to same array
|
||||
*/
|
||||
relayUrls = [...relayUrls, appRelay] // Add app relay to relays array if not exists already
|
||||
}
|
||||
|
||||
// connect to all specified relays
|
||||
const relayPromises = relayUrls.map((relayUrl) =>
|
||||
this.connectRelay(relayUrl)
|
||||
)
|
||||
|
||||
// Use Promise.allSettled to wait for all promises to settle
|
||||
const results = await Promise.allSettled(relayPromises)
|
||||
|
||||
// Extract non-null values from fulfilled promises in a single pass
|
||||
const relays = results.reduce<Relay[]>((acc, result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const value = result.value
|
||||
if (value) {
|
||||
acc.push(value)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Check if any relays are connected
|
||||
if (relays.length === 0) {
|
||||
log(this.debug, LogType.Error, 'No relay is connected!')
|
||||
return []
|
||||
}
|
||||
|
||||
const publishedOnRelays: string[] = [] // Track relays where the event was successfully published
|
||||
|
||||
// Create promises to publish the event to each connected relay
|
||||
const publishPromises = this.connectedRelays.map((relay) => {
|
||||
log(
|
||||
this.debug,
|
||||
LogType.Info,
|
||||
`⬆️ nostr (${relay.url}): Sending event:`,
|
||||
event
|
||||
)
|
||||
|
||||
return Promise.race([
|
||||
relay.publish(event), // Publish the event to the relay
|
||||
timeout(30000) // Set a timeout to handle slow publishing operations
|
||||
])
|
||||
.then((res) => {
|
||||
log(
|
||||
this.debug,
|
||||
LogType.Info,
|
||||
`⬆️ nostr (${relay.url}): Publish result:`,
|
||||
res
|
||||
)
|
||||
publishedOnRelays.push(relay.url) // Add successful relay URL to the list
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
this.debug,
|
||||
LogType.Error,
|
||||
`❌ nostr (${relay.url}): Publish error!`,
|
||||
err
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Wait for all publish operations to complete (either fulfilled or rejected)
|
||||
await Promise.allSettled(publishPromises)
|
||||
|
||||
if (publishedOnRelays.length > 0) {
|
||||
// If the event was successfully published to any relays, check if it contains an `aTag`
|
||||
// If the `aTag` is present, cache the event locally
|
||||
const aTag = event.tags.find((item) => item[0] === 'a')
|
||||
if (aTag && aTag[1]) {
|
||||
this.events.set(aTag[1], event)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the list of relay URLs where the event was successfully published
|
||||
return publishedOnRelays
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
||||
* If no relays are specified, it defaults to using connected relays.
|
||||
@ -408,25 +489,12 @@ export class RelayController {
|
||||
// Get an instance of the MetadataController, which manages user metadata and relays
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
// Find the user's relays using the MetadataController. This is an asynchronous operation.
|
||||
const usersRelaysPromise = metadataController.findUserRelays(
|
||||
// Find the user's relays using the MetadataController.
|
||||
const relayUrls = await metadataController.findUserRelays(
|
||||
hexKey,
|
||||
userRelaysType
|
||||
)
|
||||
|
||||
log(true, LogType.Info, `ℹ Finding user's relays`)
|
||||
|
||||
// Use Promise.race to attempt to resolve the user's relays, or timeout if it takes too long
|
||||
const relayUrls = await Promise.race([
|
||||
usersRelaysPromise,
|
||||
timeout() // This is a custom timeout function that rejects the promise after a specified time
|
||||
]).catch((err) => {
|
||||
// Log an error if the relay fetching operation fails
|
||||
log(true, LogType.Error, err)
|
||||
// Return an empty array to indicate failure in retrieving relay URLs
|
||||
return [] as string[]
|
||||
})
|
||||
|
||||
// Fetch the event from the user's relays using the provided filter and relay URLs
|
||||
return this.fetchEvents(filter, relayUrls)
|
||||
}
|
||||
@ -479,28 +547,97 @@ export class RelayController {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to events from multiple relays.
|
||||
*
|
||||
* This method connects to the specified relay URLs and subscribes to events
|
||||
* using the provided filter. It handles incoming events through the given
|
||||
* `eventHandler` callback and manages the subscription lifecycle.
|
||||
*
|
||||
* @param filter - The filter criteria to apply when subscribing to events.
|
||||
* @param relayUrls - An optional array of relay URLs to connect to. The default relay URL (`APP_RELAY`) is added automatically.
|
||||
* @param eventHandler - A callback function to handle incoming events. It receives an `Event` object.
|
||||
*
|
||||
*/
|
||||
subscribeForEvents = async (
|
||||
filter: Filter,
|
||||
relayUrls: string[] = [],
|
||||
eventHandler: (event: Event) => void
|
||||
) => {
|
||||
const appRelay = import.meta.env.VITE_APP_RELAY
|
||||
if (!relayUrls.includes(appRelay)) {
|
||||
/**
|
||||
* NOTE: To avoid side-effects on external relayUrls array passed as argument
|
||||
* re-assigned relayUrls with added sigit relay instead of just appending to same array
|
||||
*/
|
||||
relayUrls = [...relayUrls, appRelay] // Add app relay to relays array if not exists already
|
||||
}
|
||||
|
||||
// connect to all specified relays
|
||||
const relayPromises = relayUrls.map((relayUrl) =>
|
||||
this.connectRelay(relayUrl)
|
||||
)
|
||||
|
||||
// Use Promise.allSettled to wait for all promises to settle
|
||||
const results = await Promise.allSettled(relayPromises)
|
||||
|
||||
// Extract non-null values from fulfilled promises in a single pass
|
||||
const relays = results.reduce<Relay[]>((acc, result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const value = result.value
|
||||
if (value) {
|
||||
acc.push(value)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Check if any relays are connected
|
||||
if (relays.length === 0) {
|
||||
throw new Error('No relay is connected to fetch events!')
|
||||
}
|
||||
|
||||
const processedEvents: string[] = [] // To keep track of processed events
|
||||
|
||||
// Create a promise for each relay subscription
|
||||
const subPromises = relays.map((relay) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
// Subscribe to the relay with the specified filter
|
||||
const sub = relay.subscribe([filter], {
|
||||
// Handle incoming events
|
||||
onevent: (e) => {
|
||||
// Process event only if it hasn't been processed before
|
||||
if (!processedEvents.includes(e.id)) {
|
||||
processedEvents.push(e.id)
|
||||
eventHandler(e) // Call the event handler with the event
|
||||
}
|
||||
},
|
||||
// Handle the End-Of-Stream (EOSE) message
|
||||
oneose: () => {
|
||||
sub.close() // Close the subscription
|
||||
resolve() // Resolve the promise when EOSE is received
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Wait for all subscriptions to complete
|
||||
await Promise.allSettled(subPromises)
|
||||
}
|
||||
|
||||
getTotalZapAmount = async (
|
||||
modDetails: ModDetails,
|
||||
user: string,
|
||||
eTag: string,
|
||||
aTag?: string,
|
||||
currentLoggedInUser?: string
|
||||
) => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
const authorReadRelaysPromise = metadataController.findUserRelays(
|
||||
modDetails.author,
|
||||
const relayUrls = await metadataController.findUserRelays(
|
||||
user,
|
||||
UserRelaysType.Read
|
||||
)
|
||||
|
||||
log(this.debug, LogType.Info, `ℹ Finding user's read relays`)
|
||||
|
||||
// Use Promise.race to either get the write relay URLs or timeout
|
||||
const relayUrls = await Promise.race([
|
||||
authorReadRelaysPromise,
|
||||
timeout() // This is a custom timeout function that rejects the promise after a specified time
|
||||
]).catch((err) => {
|
||||
log(this.debug, LogType.Error, err)
|
||||
return [] as string[] // Return an empty array if an error occurs
|
||||
})
|
||||
|
||||
// add app relay to relays array
|
||||
relayUrls.push(import.meta.env.VITE_APP_RELAY)
|
||||
|
||||
@ -520,23 +657,25 @@ export class RelayController {
|
||||
|
||||
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
||||
|
||||
const filter: Filter = {
|
||||
kinds: [kinds.Zap]
|
||||
}
|
||||
|
||||
if (aTag) {
|
||||
filter['#a'] = [aTag]
|
||||
} else {
|
||||
filter['#e'] = [eTag]
|
||||
}
|
||||
|
||||
// Create a promise for each relay subscription
|
||||
const subPromises = this.connectedRelays.map((relay) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
// Subscribe to the relay with the specified filter
|
||||
const sub = relay.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [kinds.Zap],
|
||||
'#a': [modDetails.aTag]
|
||||
}
|
||||
],
|
||||
{
|
||||
const sub = relay.subscribe([filter], {
|
||||
// Handle incoming events
|
||||
onevent: (e) => {
|
||||
// Add the event to the array if it's not a duplicate
|
||||
if (!eventIds.has(e.id)) {
|
||||
console.log('e :>> ', e)
|
||||
eventIds.add(e.id) // Record the event ID
|
||||
const amount = extractZapAmount(e)
|
||||
accumulatedZapAmount += amount
|
||||
@ -554,8 +693,7 @@ export class RelayController {
|
||||
sub.close() // Close the subscription
|
||||
resolve() // Resolve the promise when EOSE is received
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './redux'
|
||||
export * from './useDidMount'
|
||||
export * from './useReactions'
|
||||
|
174
src/hooks/useReactions.ts
Normal file
174
src/hooks/useReactions.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { REACTIONS } from 'constants.ts'
|
||||
import { RelayController, UserRelaysType } from 'controllers'
|
||||
import { useAppSelector, useDidMount } from 'hooks'
|
||||
import { Event, Filter, UnsignedEvent, kinds } from 'nostr-tools'
|
||||
import { abbreviateNumber, log, LogType, now } from 'utils'
|
||||
|
||||
type UseReactionsParams = {
|
||||
pubkey: string
|
||||
eTag: string
|
||||
aTag?: string
|
||||
}
|
||||
|
||||
export const useReactions = (params: UseReactionsParams) => {
|
||||
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]
|
||||
}
|
||||
|
||||
if (params.aTag) {
|
||||
filter['#a'] = [params.aTag]
|
||||
} else {
|
||||
filter['#e'] = [params.eTag]
|
||||
}
|
||||
|
||||
RelayController.getInstance()
|
||||
.fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read)
|
||||
.then((events) => {
|
||||
setReactionEvents(events)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDataLoaded(true)
|
||||
})
|
||||
})
|
||||
|
||||
const hasReactedPositively = useMemo(() => {
|
||||
return (
|
||||
!!userState.auth &&
|
||||
reactionEvents.some(
|
||||
(event) =>
|
||||
event.pubkey === userState.user?.pubkey &&
|
||||
(REACTIONS.positive.emojis.includes(event.content) ||
|
||||
REACTIONS.positive.shortCodes.includes(event.content))
|
||||
)
|
||||
)
|
||||
}, [reactionEvents, userState])
|
||||
|
||||
const hasReactedNegatively = useMemo(() => {
|
||||
return (
|
||||
!!userState.auth &&
|
||||
reactionEvents.some(
|
||||
(event) =>
|
||||
event.pubkey === userState.user?.pubkey &&
|
||||
(REACTIONS.negative.emojis.includes(event.content) ||
|
||||
REACTIONS.negative.shortCodes.includes(event.content))
|
||||
)
|
||||
)
|
||||
}, [reactionEvents, userState])
|
||||
|
||||
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 || hasReactedPositively || hasReactedNegatively) return
|
||||
|
||||
if (isReactionInProgress) return
|
||||
|
||||
setIsReactionInProgress(true)
|
||||
|
||||
try {
|
||||
const pubkey = await getPubkey()
|
||||
if (!pubkey) return
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: kinds.Reaction,
|
||||
created_at: now(),
|
||||
content: isPositive ? '+' : '-',
|
||||
pubkey,
|
||||
tags: [
|
||||
['e', params.eTag],
|
||||
['p', params.pubkey]
|
||||
]
|
||||
}
|
||||
|
||||
if (params.aTag) {
|
||||
unsignedEvent.tags.push(['a', params.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,
|
||||
params.pubkey,
|
||||
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])
|
||||
|
||||
return {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively,
|
||||
handleReaction
|
||||
}
|
||||
}
|
@ -3,54 +3,53 @@ import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { formatDate } from 'date-fns'
|
||||
import FsLightbox from 'fslightbox-react'
|
||||
import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { useEffect, 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, ZapPopUp, ZapPresets, ZapQR } from '../components/Zap'
|
||||
import { BlogCard } from '../../components/BlogCard'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { ProfileSection } from '../../components/ProfileSection'
|
||||
import {
|
||||
MetadataController,
|
||||
RelayController,
|
||||
UserRelaysType,
|
||||
ZapController
|
||||
} from '../controllers'
|
||||
import { useAppSelector, useDidMount } from '../hooks'
|
||||
import { getModsEditPageRoute } from '../routes'
|
||||
import '../styles/comments.css'
|
||||
import '../styles/downloads.css'
|
||||
import '../styles/innerPage.css'
|
||||
import '../styles/popup.css'
|
||||
import '../styles/post.css'
|
||||
import '../styles/reactions.css'
|
||||
import '../styles/styles.css'
|
||||
import '../styles/tabs.css'
|
||||
import '../styles/tags.css'
|
||||
import '../styles/write.css'
|
||||
import { DownloadUrl, ModDetails, PaymentRequest } from '../types'
|
||||
UserRelaysType
|
||||
} from '../../controllers'
|
||||
import { useAppSelector, useDidMount } from '../../hooks'
|
||||
import { getModsEditPageRoute } from '../../routes'
|
||||
import '../../styles/comments.css'
|
||||
import '../../styles/downloads.css'
|
||||
import '../../styles/innerPage.css'
|
||||
import '../../styles/popup.css'
|
||||
import '../../styles/post.css'
|
||||
import '../../styles/reactions.css'
|
||||
import '../../styles/styles.css'
|
||||
import '../../styles/tabs.css'
|
||||
import '../../styles/tags.css'
|
||||
import '../../styles/write.css'
|
||||
import { DownloadUrl, ModDetails } from '../../types'
|
||||
import {
|
||||
abbreviateNumber,
|
||||
copyTextToClipboard,
|
||||
downloadFile,
|
||||
extractModData,
|
||||
formatNumber,
|
||||
getFilenameFromUrl,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
npubToHex,
|
||||
sendDMUsingRandomKey,
|
||||
signAndPublish,
|
||||
unformatNumber
|
||||
} from '../utils'
|
||||
import { REACTIONS } from '../constants'
|
||||
signAndPublish
|
||||
} from '../../utils'
|
||||
import { Reactions } from './internal/reactions'
|
||||
import { Zap } from './internal/zap'
|
||||
import { Comments } from './internal/comment'
|
||||
|
||||
export const ModPage = () => {
|
||||
const { naddr } = useParams()
|
||||
const [modData, setModData] = useState<ModDetails>()
|
||||
const [isFetching, setIsFetching] = useState(true)
|
||||
const [commentCount, setCommentCount] = useState(0)
|
||||
|
||||
useDidMount(async () => {
|
||||
if (naddr) {
|
||||
@ -131,7 +130,10 @@ export const ModPage = () => {
|
||||
tags={modData.tags}
|
||||
nsfw={modData.nsfw}
|
||||
/>
|
||||
<Interactions modDetails={modData} />
|
||||
<Interactions
|
||||
modDetails={modData}
|
||||
commentCount={commentCount}
|
||||
/>
|
||||
<PublishDetails
|
||||
published_at={modData.published_at}
|
||||
edited_at={modData.edited_at}
|
||||
@ -190,7 +192,10 @@ export const ModPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<Comments />
|
||||
<Comments
|
||||
modDetails={modData}
|
||||
setCommentCount={setCommentCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSection pubkey={modData.author} />
|
||||
@ -1016,16 +1021,14 @@ const Body = ({
|
||||
|
||||
type InteractionsProps = {
|
||||
modDetails: ModDetails
|
||||
commentCount: number
|
||||
}
|
||||
|
||||
const Interactions = ({ modDetails }: InteractionsProps) => {
|
||||
const Interactions = ({ modDetails, commentCount }: InteractionsProps) => {
|
||||
return (
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<div className='IBMSMSMBSS_Details'>
|
||||
<a
|
||||
href='#commentsArea'
|
||||
style={{ textDecoration: 'unset', color: 'unset' }}
|
||||
>
|
||||
<a style={{ textDecoration: 'unset', color: 'unset' }}>
|
||||
<div className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CComments'>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
@ -1039,7 +1042,9 @@ const Interactions = ({ modDetails }: InteractionsProps) => {
|
||||
<path d='M256 31.1c-141.4 0-255.1 93.12-255.1 208c0 49.62 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734c1.249 3 4.021 4.766 7.271 4.766c66.25 0 115.1-31.76 140.6-51.39c32.63 12.25 69.02 19.39 107.4 19.39c141.4 0 255.1-93.13 255.1-207.1S397.4 31.1 256 31.1zM127.1 271.1c-17.75 0-32-14.25-32-31.1s14.25-32 32-32s32 14.25 32 32S145.7 271.1 127.1 271.1zM256 271.1c-17.75 0-31.1-14.25-31.1-31.1s14.25-32 31.1-32s31.1 14.25 31.1 32S273.8 271.1 256 271.1zM383.1 271.1c-17.75 0-32-14.25-32-31.1s14.25-32 32-32s32 14.25 32 32S401.7 271.1 383.1 271.1z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>420</p>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>
|
||||
{abbreviateNumber(commentCount)}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<Zap modDetails={modDetails} />
|
||||
@ -1382,610 +1387,3 @@ const Download = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Comments = () => {
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsWrapper'>
|
||||
<h4 className='IBMSMSMBSSTitle'>Comments (WIP)</h4>
|
||||
<div id='ArticleComments-1' className='IBMSMSMBSSComments'>
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
id='commentBox-1'
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
></textarea>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<a className='IBMSMSMBSSCC_BottomButton'>
|
||||
Comment
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className='CommentsToggle'>
|
||||
<button
|
||||
className='btn btnMain CommentsToggleBtn CommentsToggleActive'
|
||||
type='button'
|
||||
>
|
||||
All Comments
|
||||
</button>
|
||||
<button className='btn btnMain CommentsToggleBtn' type='button'>
|
||||
Creator Comments
|
||||
</button>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCommentsList'>
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<a
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
href='profile.html'
|
||||
style={{
|
||||
background: `url('/assets/img/DEG%20Mods%20Default%20PP.png') center / cover no-repeat`
|
||||
}}
|
||||
></a>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<a className='IBMSMSMBSSCL_CTD_Name' href='profile.html'>
|
||||
User name
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CTD_Address' href='profile.html'>
|
||||
npub1address
|
||||
</a>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime' href='feed-note.html'>
|
||||
8:45 PM
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate' href='feed-note.html'>
|
||||
02/05/2024
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
<p className='IBMSMSMBSSCL_CBText'>Example user comment</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp IBMSMSMBSSCL_CAEUpActive'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<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>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>52</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<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>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>4</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost IBMSMSMBSSCL_CAERepostActive'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -64 640 640'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>6</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt IBMSMSMBSSCL_CAEBoltActive'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>500K</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>12</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ZapProps = {
|
||||
modDetails: ModDetails
|
||||
}
|
||||
|
||||
const Zap = ({ modDetails }: ZapProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState('0')
|
||||
|
||||
useDidMount(() => {
|
||||
RelayController.getInstance()
|
||||
.getTotalZapAmount(modDetails, userState.user?.pubkey as string)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(abbreviateNumber(res.accumulatedZapAmount))
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id='reactBolt'
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt ${
|
||||
hasZapped ? 'IBMSMSMBSS_D_CBActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSS_Details_CardVisualIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>{totalZappedAmount}</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={modDetails.author}
|
||||
eventId={modDetails.id}
|
||||
aTag={modDetails.aTag}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
lastNode={<ZapSite />}
|
||||
notCloseAfterZap
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ZapSite = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [amount, setAmount] = useState(0)
|
||||
|
||||
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 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
|
||||
)
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [amount, 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 (
|
||||
<>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>
|
||||
Tip DEG Mods too (Optional)
|
||||
</label>
|
||||
<div className='ZapSplitUserBox'>
|
||||
<div className='ZapSplitUserBoxUser'>
|
||||
<div
|
||||
className='ZapSplitUserBoxUserPic'
|
||||
style={{
|
||||
background: `url('/assets/img/Logo%20with%20circle.png')
|
||||
center / cover no-repeat`
|
||||
}}
|
||||
></div>
|
||||
<div className='ZapSplitUserBoxUserDetails'>
|
||||
<p className='ZapSplitUserBoxUserDetailsName'>DEG Mods</p>
|
||||
<p className='ZapSplitUserBoxUserDetailsHandle'>
|
||||
degmods@degmods.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className='ZapSplitUserBoxText'>
|
||||
Help with the development, maintenance, management, and growth of
|
||||
DEG Mods.
|
||||
</p>
|
||||
<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>
|
||||
<ZapButtons
|
||||
disabled={!amount}
|
||||
handleGenerateQRCode={handleGenerateQRCode}
|
||||
handleSend={handleSend}
|
||||
/>
|
||||
{paymentRequest && (
|
||||
<ZapQR
|
||||
paymentRequest={paymentRequest}
|
||||
handleClose={handleClose}
|
||||
handleQRExpiry={handleQRExpiry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
528
src/pages/mod/internal/comment/index.tsx
Normal file
528
src/pages/mod/internal/comment/index.tsx
Normal file
@ -0,0 +1,528 @@
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import {
|
||||
MetadataController,
|
||||
RelayController,
|
||||
UserRelaysType
|
||||
} from 'controllers'
|
||||
import { formatDate } from 'date-fns'
|
||||
import { useAppSelector, useDidMount, useReactions } from 'hooks'
|
||||
import {
|
||||
Event,
|
||||
kinds,
|
||||
nip19,
|
||||
Filter as NostrEventFilter,
|
||||
UnsignedEvent
|
||||
} from 'nostr-tools'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Dispatch, SetStateAction, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getProfilePageRoute } from 'routes'
|
||||
import { ModDetails, UserProfile } from 'types'
|
||||
import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest'
|
||||
}
|
||||
|
||||
enum AuthorFilterEnum {
|
||||
All_Comments = 'All Comments',
|
||||
Creator_Comments = 'Creator Comments'
|
||||
}
|
||||
|
||||
type FilterOptions = {
|
||||
sort: SortByEnum
|
||||
author: AuthorFilterEnum
|
||||
}
|
||||
|
||||
type Props = {
|
||||
modDetails: ModDetails
|
||||
setCommentCount: Dispatch<SetStateAction<number>>
|
||||
}
|
||||
|
||||
export const Comments = ({ modDetails, setCommentCount }: Props) => {
|
||||
const [commentEvents, setCommentEvents] = useState<Event[]>([])
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
author: AuthorFilterEnum.All_Comments
|
||||
})
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
useDidMount(async () => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
const authorReadRelays = await metadataController.findUserRelays(
|
||||
modDetails.author,
|
||||
UserRelaysType.Read
|
||||
)
|
||||
|
||||
const filter: NostrEventFilter = {
|
||||
kinds: [kinds.ShortTextNote],
|
||||
'#a': [modDetails.aTag]
|
||||
}
|
||||
|
||||
RelayController.getInstance().subscribeForEvents(
|
||||
filter,
|
||||
authorReadRelays,
|
||||
(event) => {
|
||||
setCommentEvents((prev) => {
|
||||
if (prev.find((e) => e.id === event.id)) {
|
||||
return [...prev]
|
||||
}
|
||||
|
||||
return [event, ...prev]
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleSubmit = async (content: string): Promise<boolean> => {
|
||||
if (content === '') return false
|
||||
|
||||
let pubkey: string
|
||||
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
pubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
pubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
toast.error('Could not get user pubkey')
|
||||
return false
|
||||
}
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
content: content,
|
||||
pubkey: pubkey,
|
||||
kind: kinds.ShortTextNote,
|
||||
created_at: now(),
|
||||
tags: [
|
||||
['e', modDetails.id],
|
||||
['a', modDetails.aTag]
|
||||
]
|
||||
}
|
||||
|
||||
const signedEvent = await window.nostr
|
||||
?.signEvent(unsignedEvent)
|
||||
.then((event) => event as Event)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to sign the event!')
|
||||
log(true, LogType.Error, 'Failed to sign the event!', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return false
|
||||
|
||||
setCommentEvents((prev) => [signedEvent, ...prev])
|
||||
|
||||
const publish = async () => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
const modAuthorReadRelays = await metadataController.findUserRelays(
|
||||
modDetails.author,
|
||||
UserRelaysType.Read
|
||||
)
|
||||
const commentatorWriteRelays = await metadataController.findUserRelays(
|
||||
pubkey,
|
||||
UserRelaysType.Write
|
||||
)
|
||||
|
||||
const combinedRelays = [
|
||||
...new Set(...modAuthorReadRelays, ...commentatorWriteRelays)
|
||||
]
|
||||
|
||||
RelayController.getInstance().publishOnRelays(signedEvent, combinedRelays)
|
||||
}
|
||||
|
||||
publish()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
setCommentCount(commentEvents.length)
|
||||
|
||||
const comments = useMemo(() => {
|
||||
let filteredComments = commentEvents
|
||||
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
||||
filteredComments = filteredComments.filter(
|
||||
(comment) => comment.pubkey === modDetails.author
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filteredComments.sort((a, b) => b.created_at - a.created_at)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filteredComments.sort((a, b) => a.created_at - b.created_at)
|
||||
}
|
||||
|
||||
return filteredComments
|
||||
}, [commentEvents, filterOptions, modDetails.author])
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsWrapper'>
|
||||
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
|
||||
<div className='IBMSMSMBSSComments'>
|
||||
<CommentForm handleSubmit={handleSubmit} />
|
||||
<Filter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
<div className='IBMSMSMBSSCommentsList'>
|
||||
{comments.map((event) => (
|
||||
<Comment key={event.id} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type CommentFormProps = {
|
||||
handleSubmit: (content: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
const CommentForm = ({ handleSubmit }: CommentFormProps) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(commentText)
|
||||
if (submitted) setCommentText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<button
|
||||
className='IBMSMSMBSSCC_BottomButton'
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Comment'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FilterProps) => {
|
||||
return (
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.author}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(AuthorFilterEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
author: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const Comment = (props: Event) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(async () => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
metadataController.findMetadata(props.pubkey).then((res) => {
|
||||
setProfile(res)
|
||||
})
|
||||
})
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: props.pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<a
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
href={`#${profileRoute}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
navigate(profileRoute)
|
||||
}}
|
||||
style={{
|
||||
background: `url('${
|
||||
profile?.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
></a>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<a className='IBMSMSMBSSCL_CTD_Name' href='profile.html'>
|
||||
{profile?.displayName || profile?.name || ''}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CTD_Address' href='profile.html'>
|
||||
{hexToNpub(props.pubkey)}
|
||||
</a>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(props.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(props.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
<p className='IBMSMSMBSSCL_CBText'>{props.content}</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -64 640 640'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<Zap {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||
</div>
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Reactions = (props: Event) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: props.pubkey,
|
||||
eTag: props.id
|
||||
})
|
||||
|
||||
if (!isDataLoaded) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<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>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>{likesCount}</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction()}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<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>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>{disLikesCount}</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Zap = (props: Event) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
|
||||
useDidMount(() => {
|
||||
RelayController.getInstance()
|
||||
.getTotalZapAmount(
|
||||
props.pubkey,
|
||||
props.id,
|
||||
undefined,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt ${
|
||||
hasZapped ? 'IBMSMSMBSSCL_CAEBoltActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={props.pubkey}
|
||||
eventId={props.id}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
74
src/pages/mod/internal/reactions/index.tsx
Normal file
74
src/pages/mod/internal/reactions/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { useReactions } from 'hooks'
|
||||
import { ModDetails } from 'types'
|
||||
|
||||
type ReactionsProps = {
|
||||
modDetails: ModDetails
|
||||
}
|
||||
|
||||
export const Reactions = ({ modDetails }: ReactionsProps) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: modDetails.author,
|
||||
eTag: modDetails.id,
|
||||
aTag: modDetails.aTag
|
||||
})
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
255
src/pages/mod/internal/zap/index.tsx
Normal file
255
src/pages/mod/internal/zap/index.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from 'components/Zap'
|
||||
import { MetadataController, RelayController, ZapController } from 'controllers'
|
||||
import { useAppSelector, useDidMount } from 'hooks'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ModDetails, PaymentRequest } from 'types'
|
||||
import { abbreviateNumber, formatNumber, unformatNumber } from 'utils'
|
||||
|
||||
type ZapProps = {
|
||||
modDetails: ModDetails
|
||||
}
|
||||
|
||||
export const Zap = ({ modDetails }: ZapProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
|
||||
useDidMount(() => {
|
||||
RelayController.getInstance()
|
||||
.getTotalZapAmount(
|
||||
modDetails.author,
|
||||
modDetails.id,
|
||||
modDetails.aTag,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id='reactBolt'
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt ${
|
||||
hasZapped ? 'IBMSMSMBSS_D_CBActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSS_Details_CardVisualIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={modDetails.author}
|
||||
eventId={modDetails.id}
|
||||
aTag={modDetails.aTag}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
lastNode={<ZapSite />}
|
||||
notCloseAfterZap
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ZapSite = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [amount, setAmount] = useState(0)
|
||||
|
||||
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 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
|
||||
)
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [amount, 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 (
|
||||
<>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>
|
||||
Tip DEG Mods too (Optional)
|
||||
</label>
|
||||
<div className='ZapSplitUserBox'>
|
||||
<div className='ZapSplitUserBoxUser'>
|
||||
<div
|
||||
className='ZapSplitUserBoxUserPic'
|
||||
style={{
|
||||
background: `url('/assets/img/Logo%20with%20circle.png')
|
||||
center / cover no-repeat`
|
||||
}}
|
||||
></div>
|
||||
<div className='ZapSplitUserBoxUserDetails'>
|
||||
<p className='ZapSplitUserBoxUserDetailsName'>DEG Mods</p>
|
||||
<p className='ZapSplitUserBoxUserDetailsHandle'>
|
||||
degmods@degmods.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className='ZapSplitUserBoxText'>
|
||||
Help with the development, maintenance, management, and growth of
|
||||
DEG Mods.
|
||||
</p>
|
||||
<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>
|
||||
<ZapButtons
|
||||
disabled={!amount}
|
||||
handleGenerateQRCode={handleGenerateQRCode}
|
||||
handleSend={handleSend}
|
||||
/>
|
||||
{paymentRequest && (
|
||||
<ZapQR
|
||||
paymentRequest={paymentRequest}
|
||||
handleClose={handleClose}
|
||||
handleQRExpiry={handleQRExpiry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
</>
|
||||
)
|
||||
}
|
@ -2,3 +2,4 @@ export * from './mod'
|
||||
export * from './nostr'
|
||||
export * from './url'
|
||||
export * from './utils'
|
||||
export * from './zap'
|
||||
|
38
src/utils/zap.ts
Normal file
38
src/utils/zap.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { ZapReceipt, ZapRequest } from 'types'
|
||||
|
||||
/**
|
||||
* Gets value of description tag.
|
||||
* @param receipt - zap receipt.
|
||||
* @returns value of description tag.
|
||||
*/
|
||||
export const getDescription = (receipt: ZapReceipt) => {
|
||||
return receipt.tags.filter((tag) => tag[0] === 'description')[0][1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value of amount tag.
|
||||
* @param request - zap receipt.
|
||||
* @returns value of amount tag.
|
||||
*/
|
||||
export const getAmount = (request: ZapRequest) => {
|
||||
return request.tags.filter((tag) => tag[0] === 'amount')[0][1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets zap amount.
|
||||
* @param receipt - zap receipt.
|
||||
* @returns zap amount
|
||||
*/
|
||||
export const getZapAmount = (receipt: ZapReceipt) => {
|
||||
const description = getDescription(receipt)
|
||||
let request: ZapRequest
|
||||
|
||||
try {
|
||||
request = JSON.parse(description)
|
||||
} catch (err) {
|
||||
throw 'An error occurred in parsing description tag from zapReceipt'
|
||||
}
|
||||
|
||||
// Zap amount is stored in mili sats, to get the zap amount we'll divide it by 1000
|
||||
return parseInt(getAmount(request)) / 1000
|
||||
}
|
Loading…
Reference in New Issue
Block a user