import FsLightbox from 'fslightbox-react' import { nip19 } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { Outlet, Link as ReactRouterLink, useLoaderData, useNavigate, useNavigation, useParams, useSubmit } from 'react-router-dom' import { toast } from 'react-toastify' import { BlogCard } from '../../components/BlogCard' import { ProfileSection } from '../../components/ProfileSection' import { useAppSelector, useBodyScrollDisable, useDidMount, useLocalStorage } from '../../hooks' import { appRoutes, getGamePageRoute, 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, ModFormState, ModPageLoaderResult, ModPermissions, MODPERMISSIONS_CONF, MODPERMISSIONS_DESC } from '../../types' import { capitalizeEachWord, checkUrlForFile, copyTextToClipboard, downloadFile, getFilenameFromUrl, isValidUrl } from '../../utils' import { Comments } from '../../components/comment' import { PublishDetails } from 'components/Internal/PublishDetails' import { Interactions } from 'components/Internal/Interactions' import { ReportPopup } from 'components/ReportPopup' import { Spinner } from 'components/Spinner' import { RouterLoadingSpinner } from 'components/LoadingSpinner' import { OriginalAuthor } from 'components/OriginalAuthor' import { Viewer } from 'components/Markdown/Viewer' import { PostWarnings } from 'components/PostWarning' import { DownloadDetailsPopup } from 'components/DownloadDetailsPopup' import { NsfwAlertPopup } from 'components/NsfwAlertPopup' const MOD_REPORT_REASONS = [ { label: 'Actually CP', key: 'actuallyCP' }, { label: 'Spam', key: 'spam' }, { label: 'Scam', key: 'scam' }, { label: 'Not a game mod', key: 'notAGameMod' }, { label: 'Stolen game mod', key: 'stolenGameMod' }, { label: `Repost of a game mod`, key: 'repost' }, { label: `Wasn't tagged NSFW`, key: 'wasntTaggedNSFW' }, { label: 'Other reason', key: 'otherReason' } ] export const ModPage = () => { const { mod, postWarning } = useLoaderData() as ModPageLoaderResult // We can get author right away from naddr, no need to wait for mod data const { naddr, nevent } = useParams() let author = mod?.author if (naddr && !author) { try { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) author = decoded.data.pubkey } catch (error) { // Silently ignore - we will get author eventually from mods } } const [commentCount, setCommentCount] = useState(0) const navigate = useNavigate() const [confirmNsfw] = useLocalStorage('confirm-nsfw', false) const [showNsfwPopup, setShowNsfwPopup] = useState( (mod?.nsfw ?? false) && !confirmNsfw ) const handleConfirm = (confirm: boolean) => { if (!confirm) { navigate(appRoutes.home) } } return ( <>
{mod ? ( <>
{postWarning && }

Mod Download

{postWarning && } {mod.downloadUrls.length > 0 && (
)} {mod.downloadUrls.length > 1 && (
{mod.downloadUrls .slice(1) .map((download, index) => ( ))}
)}
) : ( )}
{typeof author !== 'undefined' && ( )} {showNsfwPopup && ( setShowNsfwPopup(false)} /> )}
) } const Game = () => { const { naddr } = useParams() const navigation = useNavigation() const { mod, isAddedToNSFW, isBlocked, isRepost } = useLoaderData() as ModPageLoaderResult const userState = useAppSelector((state) => state.user) const isLoggedIn = userState.auth && userState.user?.pubkey !== 'undefined' const [showReportPopUp, setShowReportPopUp] = useState() useBodyScrollDisable(!!showReportPopUp) const submit = useSubmit() const handleBlock = () => { if (navigation.state === 'idle') { submit( { intent: 'block', value: !isBlocked }, { method: 'post', encType: 'application/json' } ) } } const handleNSFW = () => { if (navigation.state === 'idle') { submit( { intent: 'nsfw', value: !isAddedToNSFW }, { method: 'post', encType: 'application/json' } ) } } const handleRepost = () => { if (navigation.state === 'idle') { submit( { intent: 'repost', value: !isRepost }, { method: 'post', encType: 'application/json' } ) } } const isAdmin = userState.user?.npub && userState.user.npub === import.meta.env.VITE_REPORTING_NPUB const game = mod?.game || '' const gameRoute = getGamePageRoute(game) const editRoute = getModsEditPageRoute(naddr ? naddr : '') return ( <>

Mod for:  {game}

{!!showReportPopUp && ( setShowReportPopUp(undefined)} /> )} ) } type BodyProps = Pick< ModDetails, | 'featuredImageUrl' | 'title' | 'body' | 'game' | 'screenshotsUrls' | 'tags' | 'LTags' | 'nsfw' | 'repost' | 'originalAuthor' | keyof ModPermissions | 'publisherNotes' | 'extraCredits' > const Body = ({ featuredImageUrl, game, title, body, screenshotsUrls, tags, LTags, nsfw, repost, originalAuthor, otherAssets, uploadPermission, modPermission, convPermission, assetUsePermission, assetUseComPermission, publisherNotes, extraCredits }: BodyProps) => { const COLLAPSED_MAX_SIZE = 250 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 })) } useEffect(() => { if (postBodyRef.current) { if (postBodyRef.current.scrollHeight <= COLLAPSED_MAX_SIZE) { viewFullPost() } } }, []) const viewFullPost = () => { if (postBodyRef.current && viewFullPostBtnRef.current) { postBodyRef.current.style.maxHeight = 'unset' postBodyRef.current.style.padding = 'unset' viewFullPostBtnRef.current.style.display = 'none' } } return ( <>

{title}

Read Full

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

NSFW

)} {repost && (

REPOST {originalAuthor && originalAuthor !== '' && ( <> . Original Author:{' '} )}

)} {tags.map((tag, index) => ( {tag} ))} {LTags.length > 0 && (
{LTags.map((hierarchy) => { const hierarchicalCategories = hierarchy.split(`:`) const categories = hierarchicalCategories .map((c, i) => { const partialHierarchy = hierarchicalCategories .slice(0, i + 1) .join(':') return (

{capitalizeEachWord(c)}

) }) .reduce((prev, curr, i) => [ prev,

>

, curr ]) return (
{categories}
) })}
)}
) } const Download = (props: DownloadUrl) => { const { url, title, malwareScanLink } = props const [showAuthDetails, setShowAuthDetails] = useState(false) const [showNotice, setShowNotice] = useState(false) const [showScanNotice, setShowCanNotice] = useState(false) useDidMount(async () => { const isFile = await checkUrlForFile(url) setShowNotice(!isFile) // Check the malware scan url // if it's valid URL // if it contains sha256 // if it differs from download link setShowCanNotice( !( malwareScanLink && isValidUrl(malwareScanLink) && /\b[a-fA-F0-9]{64}\b/.test(malwareScanLink) && malwareScanLink !== url ) ) }) const handleDownload = () => { // Get the filename from the URL const filename = getFilenameFromUrl(url) downloadFile(url, filename) } return (
{typeof title !== 'undefined' && title !== '' && ( {title} )}
{showNotice && (

Notice: The creator has provided a download link that doesn't download the files immediately, but rather redirects you to a different site.

)} {showScanNotice && (

The mod poster hasn't provided a malware scan report for these files. Be careful.

)} {/*temporarily commented out the WoT rating for download links within a mod post

Ratings (WIP):

420

420

420

4,200

4,200

4,200

*/}

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

{showAuthDetails && ( setShowAuthDetails(false)} /> )}
) } const DisplayModAuthorBlogs = () => { const { latest } = useLoaderData() as ModPageLoaderResult if (!latest?.length) return null return (

Creator's Blog Posts

{latest?.map((b) => ( ))}
) } type ExtraDetailsProps = ModPermissions & Pick const ExtraDetails = ({ publisherNotes, extraCredits, ...rest }: ExtraDetailsProps) => { const extraBoxRef = useRef(null) if ( typeof publisherNotes === 'undefined' && typeof extraCredits === 'undefined' && Object.values(rest).every((v) => typeof v === 'undefined') ) { return null } const handleClick = () => { if (extraBoxRef.current) { if (extraBoxRef.current.style.display === '') { extraBoxRef.current.style.display = 'none' } else { extraBoxRef.current.style.display = '' } } } return (
{Object.keys(MODPERMISSIONS_CONF).map((k) => { const permKey = k as keyof ModPermissions const confKey = k as keyof typeof MODPERMISSIONS_CONF const modPermission = MODPERMISSIONS_CONF[confKey] const value = rest[permKey] if (typeof value === 'undefined') return null const text = MODPERMISSIONS_DESC[`${permKey}_${value}`] return (

{modPermission.header}

{value ? (
) : (
)}

{text}

) })} {typeof publisherNotes !== 'undefined' && publisherNotes !== '' && (

Publisher Notes

{publisherNotes}

)} {typeof extraCredits !== 'undefined' && extraCredits !== '' && (

Extra Credits

{extraCredits}

)}
) }