import Link from '@tiptap/extension-link' 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 } from 'nostr-tools' import { useCallback, 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 { 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' import { abbreviateNumber, copyTextToClipboard, downloadFile, extractModData, formatNumber, getFilenameFromUrl, log, LogType, now, npubToHex, sendDMUsingRandomKey, signAndPublish, unformatNumber } from '../utils' export const ModPage = () => { const { naddr } = useParams() const [modData, setModData] = useState() const [isFetching, setIsFetching] = useState(true) useDidMount(async () => { if (naddr) { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) const { identifier, kind, pubkey, relays = [] } = decoded.data const filter: Filter = { '#a': [identifier], authors: [pubkey], kinds: [kind] } RelayController.getInstance() .fetchEvent(filter, relays) .then((event) => { if (event) { const extracted = extractModData(event) setModData(extracted) } }) .catch((err) => { log( true, LogType.Error, 'An error occurred in fetching mod details from relays', err ) toast.error('An error occurred in fetching mod details from relays') }) .finally(() => { setIsFetching(false) }) } }) const oldDownloadListRef = useRef(null) const handleViewOldLinks = () => { if (oldDownloadListRef.current) { // Toggle styles if (oldDownloadListRef.current.style.height === '0px') { // Enable styles oldDownloadListRef.current.style.padding = '' oldDownloadListRef.current.style.height = '' oldDownloadListRef.current.style.border = '' } else { // Disable styles oldDownloadListRef.current.style.padding = '0' oldDownloadListRef.current.style.height = '0' oldDownloadListRef.current.style.border = 'unset' } } } if (isFetching) return if (!modData) return null return (

Mod Download

{modData.downloadUrls.length > 0 && (
)} {modData.downloadUrls.length > 1 && ( <>
{modData.downloadUrls .slice(1) .map((download, index) => ( ))}
)}

Creator's Blog Posts (WIP)

) } type GameProps = { naddr: string game: string author: string aTag: string } const Game = ({ naddr, game, author, aTag }: GameProps) => { const navigate = useNavigate() const userState = useAppSelector((state) => state.user) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [showReportPopUp, setShowReportPopUp] = useState(false) const [isBlocked, setIsBlocked] = useState(false) const [isAddedToNSFW, setIsAddedToNSFW] = useState(false) useEffect(() => { if (userState.auth && userState.user?.pubkey) { const pubkey = userState.user.pubkey as string const muteListFilter: Filter = { kinds: [kinds.Mutelist], authors: [pubkey] } RelayController.getInstance() .fetchEventFromUserRelays(muteListFilter, pubkey, UserRelaysType.Write) .then((event) => { if (event) { // get a list of tags const tags = event.tags const blocked = tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 setIsBlocked(blocked) } }) if ( userState.user.npub && userState.user.npub === import.meta.env.VITE_REPORTING_NPUB ) { const nsfwListFilter: Filter = { kinds: [kinds.Curationsets], authors: [pubkey], '#d': ['nsfw'] } RelayController.getInstance() .fetchEventFromUserRelays( nsfwListFilter, pubkey, UserRelaysType.Write ) .then((event) => { if (event) { // get a list of tags const tags = event.tags const existsInNSFWList = tags.findIndex( (item) => item[0] === 'a' && item[1] === aTag ) !== -1 setIsAddedToNSFW(existsInNSFWList) } }) } } }, [userState, aTag]) const handleBlock = async () => { let hexPubkey: string setIsLoading(true) setLoadingSpinnerDesc('Getting user pubkey') 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 for updating mute list') setIsLoading(false) return } setLoadingSpinnerDesc(`Finding user's mute list`) // Define the event filter to search for the user's mute list events. // We look for events of a specific kind (Mutelist) authored by the given hexPubkey. const filter: Filter = { kinds: [kinds.Mutelist], authors: [hexPubkey] } // Fetch the mute list event from the relays. This returns the event containing the user's mute list. const muteListEvent = await RelayController.getInstance().fetchEventFromUserRelays( filter, hexPubkey, UserRelaysType.Write ) let unsignedEvent: UnsignedEvent if (muteListEvent) { // get a list of tags const tags = muteListEvent.tags const alreadyExists = tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 if (alreadyExists) { setIsLoading(false) setIsBlocked(true) return toast.warn(`Mod reference is already in user's mute list`) } tags.push(['a', aTag]) unsignedEvent = { pubkey: muteListEvent.pubkey, kind: muteListEvent.kind, content: muteListEvent.content, created_at: now(), tags: [...tags] } } else { unsignedEvent = { pubkey: hexPubkey, kind: kinds.Mutelist, content: '', created_at: now(), tags: [['a', aTag]] } } setLoadingSpinnerDesc('Updating mute list event') const isUpdated = await signAndPublish(unsignedEvent) if (isUpdated) { setIsBlocked(true) } setIsLoading(false) } const handleUnblock = async () => { const pubkey = userState.user?.pubkey as string const filter: Filter = { kinds: [kinds.Mutelist], authors: [pubkey] } setIsLoading(true) setLoadingSpinnerDesc(`Finding user's mute list`) // Fetch the mute list event from the relays. This returns the event containing the user's mute list. const muteListEvent = await RelayController.getInstance().fetchEventFromUserRelays( filter, pubkey, UserRelaysType.Write ) if (!muteListEvent) { toast.error(`Couldn't get user's mute list event from relays`) return } const tags = muteListEvent.tags const unsignedEvent: UnsignedEvent = { pubkey: muteListEvent.pubkey, kind: muteListEvent.kind, content: muteListEvent.content, created_at: now(), tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag) } setLoadingSpinnerDesc('Updating mute list event') const isUpdated = await signAndPublish(unsignedEvent) if (isUpdated) { setIsBlocked(false) } setIsLoading(false) } const handleBlockNSFW = async () => { const pubkey = userState.user?.pubkey as string | undefined if (!pubkey) return const filter: Filter = { kinds: [kinds.Curationsets], authors: [pubkey], '#d': ['nsfw'] } setIsLoading(true) setLoadingSpinnerDesc('Finding NSFW list') const nsfwListEvent = await RelayController.getInstance().fetchEventFromUserRelays( filter, pubkey, UserRelaysType.Write ) let unsignedEvent: UnsignedEvent if (nsfwListEvent) { // get a list of tags const tags = nsfwListEvent.tags const alreadyExists = tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 if (alreadyExists) { setIsLoading(false) setIsAddedToNSFW(true) return toast.warn(`Mod reference is already in user's nsfw list`) } tags.push(['a', aTag]) unsignedEvent = { pubkey: nsfwListEvent.pubkey, kind: nsfwListEvent.kind, content: nsfwListEvent.content, created_at: now(), tags: [...tags] } } else { unsignedEvent = { pubkey: pubkey, kind: kinds.Curationsets, content: '', created_at: now(), tags: [ ['a', aTag], ['d', 'nsfw'] ] } } setLoadingSpinnerDesc('Updating nsfw list event') const isUpdated = await signAndPublish(unsignedEvent) if (isUpdated) { setIsAddedToNSFW(true) } setIsLoading(false) } const handleUnblockNSFW = async () => { const pubkey = userState.user?.pubkey as string const filter: Filter = { kinds: [kinds.Curationsets], authors: [pubkey], '#d': ['nsfw'] } setIsLoading(true) setLoadingSpinnerDesc('Finding NSFW list') const nsfwListEvent = await RelayController.getInstance().fetchEventFromUserRelays( filter, pubkey, UserRelaysType.Write ) if (!nsfwListEvent) { toast.error(`Couldn't get nsfw list event from relays`) return } const tags = nsfwListEvent.tags const unsignedEvent: UnsignedEvent = { pubkey: nsfwListEvent.pubkey, kind: nsfwListEvent.kind, content: nsfwListEvent.content, created_at: now(), tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag) } setLoadingSpinnerDesc('Updating nsfw list event') const isUpdated = await signAndPublish(unsignedEvent) if (isUpdated) { setIsAddedToNSFW(false) } setIsLoading(false) } const isAdmin = userState.user?.npub && userState.user.npub === import.meta.env.VITE_REPORTING_NPUB return ( <> {isLoading && }

Mod for:  {game}

{showReportPopUp && ( setShowReportPopUp(false)} /> )} ) } type ReportPopupProps = { aTag: string handleClose: () => void } const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { const userState = useAppSelector((state) => state.user) const [selectedOptions, setSelectedOptions] = useState({ actuallyCP: false, spam: false, scam: false, notAGameMod: false, stolenGameMod: false, wasntTaggedNSFW: false, otherReason: false }) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const handleCheckboxChange = (option: keyof typeof selectedOptions) => { setSelectedOptions((prevState) => ({ ...prevState, [option]: !prevState[option] })) } const handleSubmit = async () => { const selectedOptionsCount = Object.values(selectedOptions).filter( (isSelected) => isSelected ).length if (selectedOptionsCount === 0) { toast.error('At least one option should be checked!') return } let hexPubkey: string setIsLoading(true) setLoadingSpinnerDesc('Getting user pubkey') 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 for reporting mod!') setIsLoading(false) return } const metadataController = await MetadataController.getInstance() const reportingPubkey = npubToHex(metadataController.reportingNpub) if (reportingPubkey === hexPubkey) { setLoadingSpinnerDesc(`Finding user's mute list`) // Define the event filter to search for the user's mute list events. // We look for events of a specific kind (Mutelist) authored by the given hexPubkey. const filter: Filter = { kinds: [kinds.Mutelist], authors: [hexPubkey] } // Fetch the mute list event from the relays. This returns the event containing the user's mute list. const muteListEvent = await RelayController.getInstance().fetchEventFromUserRelays( filter, hexPubkey, UserRelaysType.Write ) let unsignedEvent: UnsignedEvent if (muteListEvent) { // get a list of tags const tags = muteListEvent.tags const alreadyExists = tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 if (alreadyExists) { setIsLoading(false) return toast.warn(`Mod reference is already in user's mute list`) } tags.push(['a', aTag]) unsignedEvent = { pubkey: muteListEvent.pubkey, kind: muteListEvent.kind, content: muteListEvent.content, created_at: now(), tags: [...tags] } } else { unsignedEvent = { pubkey: hexPubkey, kind: kinds.Mutelist, content: '', created_at: now(), tags: [['a', aTag]] } } setLoadingSpinnerDesc('Updating mute list event') const isUpdated = await signAndPublish(unsignedEvent) if (isUpdated) handleClose() } else { const href = window.location.href let message = `I'd like to report ${href} due to following reasons:\n` Object.entries(selectedOptions).forEach(([key, value]) => { if (value) { message += `* ${key}\n` } }) setLoadingSpinnerDesc('Sending report') const isSent = await sendDMUsingRandomKey(message, reportingPubkey!) if (isSent) handleClose() } setIsLoading(false) } return ( <> {isLoading && }

Report Post

handleCheckboxChange('actuallyCP')} />
handleCheckboxChange('spam')} />
handleCheckboxChange('scam')} />
handleCheckboxChange('notAGameMod')} />
handleCheckboxChange('stolenGameMod')} />
handleCheckboxChange('wasntTaggedNSFW')} />
handleCheckboxChange('otherReason')} />
) } type BodyProps = { featuredImageUrl: string title: string body: string screenshotsUrls: string[] tags: string[] nsfw: boolean } const Body = ({ featuredImageUrl, title, body, screenshotsUrls, tags, nsfw }: BodyProps) => { const postBodyRef = useRef(null) const viewFullPostBtnRef = useRef(null) const [lightBoxController, setLightBoxController] = useState({ toggler: false, slide: 1 }) const openLightBoxOnSlide = (slide: number) => { setLightBoxController((prev) => ({ toggler: !prev.toggler, slide })) } const viewFullPost = () => { if (postBodyRef.current && viewFullPostBtnRef.current) { postBodyRef.current.style.maxHeight = 'unset' postBodyRef.current.style.padding = 'unset' viewFullPostBtnRef.current.style.display = 'none' } } const editor = useEditor({ content: body, extensions: [StarterKit, Link], editable: false }) return ( <>

{title}

View

{screenshotsUrls.map((url, index) => ( {`ScreenShot-${index}`} openLightBoxOnSlide(index + 1)} /> ))}
{nsfw && (

NSFW

)} {tags.map((tag, index) => ( {tag} ))}
) } type InteractionsProps = { modDetails: ModDetails } const Interactions = ({ modDetails }: InteractionsProps) => { return (

420

4.2k

69

) } type PublishDetailsProps = { published_at: number edited_at: number site: string } const PublishDetails = ({ published_at, edited_at, site }: PublishDetailsProps) => { return (

{formatDate( (published_at !== -1 ? published_at : edited_at) * 1000, 'dd/MM/yyyy hh:mm:ss aa' )}

{formatDate(edited_at * 1000, 'dd/MM/yyyy hh:mm:ss aa')}

{site}

) } const Download = ({ url, hash, signatureKey, malwareScanLink, modVersion, customNote }: DownloadUrl) => { const [showAuthDetails, setShowAuthDetails] = useState(false) const handleDownload = () => { // Get the filename from the URL const filename = getFilenameFromUrl(url) downloadFile(url, filename) } return (

Ratings (WIP):

420

420

420

4,200

4,200

4,200

setShowAuthDetails((prev) => !prev)} > Authentication Details

{showAuthDetails && (

SHA-256 hash

{hash}

Signature from

{signatureKey}

Scan

{malwareScanLink}

Mod Version

{modVersion}

Note

{customNote}

)}
) } const Comments = () => { return (

Comments (WIP)

Example user comment

52

4

6

500K

12

Replies

Reply

) } 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 ( <>
setIsOpen(true)} >

{totalZappedAmount}

{isOpen && ( setIsOpen(false)} lastNode={} notCloseAfterZap /> )} ) } const ZapSite = () => { const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [amount, setAmount] = useState(0) const [paymentRequest, setPaymentRequest] = useState() const userState = useAppSelector((state) => state.user) const handleAmountChange = (event: React.ChangeEvent) => { 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 => { 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 ( <>

DEG Mods

degmods@degmods.com

Help with the development, maintenance, management, and growth of DEG Mods.

{paymentRequest && ( )}
{isLoading && } ) }