import React, { useCallback, useLayoutEffect, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; import { Virtuoso } from 'react-virtuoso'; import { useAccount, useFeed, usePublishComment, usePublishCommentEdit, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable, PostMenu, BoardForm } from '../styled/views/Board.styled'; import { Threads, PostPreview, PostMenuCatalog } from '../styled/views/Catalog.styled'; import { TopBar, Footer } from '../styled/views/Thread.styled'; import { AlertModal } from '../styled/modals/AlertModal.styled'; import EditModal from '../modals/EditModal'; import CreateBoardModal from '../modals/CreateBoardModal'; import ModerationModal from '../modals/ModerationModal'; import SettingsModal from '../modals/SettingsModal'; import BoardSettings from '../BoardSettings'; import BoardStats from '../BoardStats'; import CatalogLoader from '../CatalogLoader'; import DisplayName from '../DisplayName'; import ImageBanner from '../ImageBanner'; import OfflineIndicator from '../OfflineIndicator'; import VerifiedAuthor from '../VerifiedAuthor'; import countLinks from '../../utils/countLinks'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; import getFormattedTime from '../../utils/getFormattedTime'; import handleStyleChange from '../../utils/handleStyleChange'; import useAnonModeRef from '../../hooks/useAnonModeRef'; import useClickForm from '../../hooks/useClickForm'; import useError from '../../hooks/useError'; import useStateString from '../../hooks/useStateString'; import useSuccess from '../../hooks/useSuccess'; import useAnonModeStore from '../../hooks/stores/useAnonModeStore'; import useFeedRows from '../../hooks/useFeedRows'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; import useWindowWidth from '../../hooks/useWindowWidth'; import packageJson from '../../../package.json'; const { version } = packageJson; let lastVirtuosoStates = {}; const commitRef = process?.env?.REACT_APP_COMMIT_REF ? ` ${process.env.REACT_APP_COMMIT_REF.slice(0, 7)}` : ''; const CatalogPost = ({ post }) => { const { editedComment, setDeletePost, setIsAuthorDelete, captchaResponse, setCaptchaResponse, selectedAddress, setIsModerationOpen, setChallengesArray, setIsAuthorEdit, setIsCaptchaOpen, setIsEditModalOpen, canModerate, setModeratingCommentCid, setOriginalCommentContent, setPendingComment, setResolveCaptchaPromise, selectedStyle, setSelectedThread, } = useGeneralStore((state) => state); const thread = post; const commentMediaInfo = getCommentMediaInfo(thread); const linkCount = countLinks(thread); const fallbackImgUrl = 'assets/filedeleted-res.gif'; const [isHoveringOnThread, setIsHoveringOnThread] = useState(false); const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 }); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); const [openMenuCid, setOpenMenuCid] = useState(null); const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isClientRedirectMenuOpen, setIsClientRedirectMenuOpen] = useState(false); const [commentCid, setCommentCid] = useState(null); const threadMenuRefs = useRef({}); const threadRefs = useRef({}); const postMenuRef = useRef(null); const popupRef = useRef(null); const imageRef = useRef(null); const postMenuCatalogRef = useRef(null); const textRef = useRef(null); const { subplebbitAddress } = useParams(); const [, setNewErrorMessage] = useError(); const [, setNewSuccessMessage] = useSuccess(); const subplebbit = useSubplebbit({ subplebbitAddress: selectedAddress }); const account = useAccount(); const [spaceOnRight, setSpaceOnRight] = useState(null); const [isCalculationDone, setIsCalculationDone] = useState(false); const [hoverTimeoutId, setHoverTimeoutId] = useState(null); const [isHoveringForMenu, setIsHoveringForMenu] = useState(false); const textRect = textRef.current?.getBoundingClientRect(); const imageRect = imageRef.current?.getBoundingClientRect(); const popupRect = popupRef.current?.getBoundingClientRect(); const isMediaShowed = thread.link && commentMediaInfo && (commentMediaInfo.type === 'image' || commentMediaInfo.type === 'video' || (commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) || (commentMediaInfo.type === 'iframe' && commentMediaInfo.thumbnail)) ? true : false; const { left: textLeft, right: textRight, width: textWidth } = textRef.current?.getBoundingClientRect() || {}; const { left: imageLeft, right: imageRight, width: imageWidth } = imageRef.current?.getBoundingClientRect() || {}; useLayoutEffect(() => { const executeLayoutEffectLogic = () => { let ref; if (isMediaShowed) { ref = imageRef.current; } else { ref = textRef.current; } if (ref && popupRef.current) { const rect = ref.getBoundingClientRect(); const viewportWidth = document.documentElement.clientWidth; const spaceRight = viewportWidth - (rect.left + rect.width); const spaceLeft = rect.left; if (spaceRight < 200 && spaceLeft > spaceRight && spaceLeft < 500) { popupRef.current.style.maxWidth = `calc( 100vw - ${isMediaShowed ? imageWidth : textWidth}px - ${spaceRight + 40}px )`; } else if (spaceRight < 200 && spaceLeft < spaceRight) { popupRef.current.style.maxWidth = `calc(100vw - ${isMediaShowed ? imageWidth : textWidth}px)`; } else { popupRef.current.style.maxWidth = `500px`; } setSpaceOnRight(spaceRight); setIsCalculationDone(true); } }; if (isHoveringOnThread) { const timeoutId = setTimeout(executeLayoutEffectLogic, 250); return () => { clearTimeout(timeoutId); setIsCalculationDone(false); }; } }, [isHoveringOnThread, isMediaShowed, textLeft, textRight, imageLeft, imageRight, textWidth, imageWidth]); const handleMouseOnLeaveThread = () => { if (hoverTimeoutId) { clearTimeout(hoverTimeoutId); setHoverTimeoutId(null); setIsHoveringOnThread(''); } else if (isHoveringOnThread !== '') { setIsHoveringOnThread(''); setIsCalculationDone(false); } }; const handleOutsideClick = useCallback( (e) => { if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { setOpenMenuCid(null); } }, [openMenuCid], ); const handleOptionClick = () => { setOpenMenuCid(null); }; useEffect(() => { if (openMenuCid !== null) { document.addEventListener('click', handleOutsideClick); } else { document.removeEventListener('click', handleOutsideClick); } return () => { document.removeEventListener('click', handleOutsideClick); }; }, [openMenuCid, handleOutsideClick]); const handleAuthorDeleteClick = (comment) => { handleOptionClick(comment.cid); confirmAlert({ customUI: ({ onClose }) => { return (

Are you sure you want to delete this post?

); }, }); }; const handleAuthorEditClick = (comment) => { handleOptionClick(comment.cid); setIsAuthorEdit(true); setIsAuthorDelete(false); setCommentCid(comment.cid); setOriginalCommentContent(comment.content); setIsEditModalOpen(true); }; const onChallengeVerification = (challengeVerification) => { if (challengeVerification.challengeSuccess === true) { setNewSuccessMessage('Challenge Success'); console.log('challenge success', challengeVerification); } else if (challengeVerification.challengeSuccess === false) { setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`); console.log('challenge failed', challengeVerification); } }; const onChallenge = async (challenges, comment) => { setPendingComment(comment); let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges); } catch (error) { setNewErrorMessage(error.message); console.log(error); } if (challengeAnswers) { await comment.publishChallengeAnswers(challengeAnswers); } }; const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({ commentCid: commentCid, content: editedComment || undefined, subplebbitAddress: selectedAddress || subplebbitAddress, onChallenge, onChallengeVerification, onError: (error) => { setNewErrorMessage(error.message); console.log(error); }, }); const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); useEffect(() => { setPublishCommentEditOptions((prevOptions) => ({ ...prevOptions, commentCid: commentCid, content: editedComment || undefined, })); }, [commentCid, editedComment]); useEffect(() => { if (editedComment !== '') { setTriggerPublishCommentEdit(true); } }, [editedComment]); useEffect(() => { if (publishCommentEditOptions && triggerPublishCommentEdit) { (async () => { await publishCommentEdit(); setTriggerPublishCommentEdit(false); })(); } }, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]); const getChallengeAnswersFromUser = async (challenges) => { setChallengesArray(challenges); return new Promise((resolve, reject) => { const imageString = challenges?.challenges[0].challenge; const imageSource = `data:image/png;base64,${imageString}`; const challengeImg = new Image(); challengeImg.src = imageSource; challengeImg.onload = () => { setIsCaptchaOpen(true); const handleKeyDown = async (event) => { if (event.key === 'Enter') { const currentCaptchaResponse = captchaResponse; resolve(currentCaptchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); event.preventDefault(); } }; setCaptchaResponse(''); document.addEventListener('keydown', handleKeyDown); setResolveCaptchaPromise(resolve); }; challengeImg.onerror = () => { reject(setNewErrorMessage('Could not load challenges')); }; }); }; let displayWidth, displayHeight; if (thread.linkWidth && thread.linkHeight) { let scale = Math.min(1, 150 / Math.max(thread.linkWidth, thread.linkHeight)); displayWidth = `${thread.linkWidth * scale}px`; displayHeight = `${thread.linkHeight * scale}px`; } else if (thread.link) { displayWidth = '150px'; displayHeight = '150px'; } else { displayWidth = '0px'; displayHeight = '0px'; } if (post.type === 'rules') { return ( <> {isHoveringOnThread === 'rules' ? createPortal(
handleMouseOnLeaveThread()} style={{ visibility: isCalculationDone ? 'visible' : 'hidden', left: isCalculationDone && spaceOnRight > 250 ? textRect.right + 5 : isCalculationDone && spaceOnRight < 250 ? textRect.left - popupRect.width - 5 : 'auto', top: popupPosition.top + 5, }} > Rules  by  ## Board Mods {thread.timestamp ? getFormattedTime(thread.timestamp) : null}
, document.body, ) : null}
{ threadRefs.current['rules'] = el; }} onMouseLeave={() => { handleMouseOnLeaveThread(); }} > { setIsHoveringForMenu('rules'); handleMouseOnLeaveThread(); }} onMouseLeave={() => setIsHoveringForMenu(false)} >
R: 0 / L: 0
{ threadMenuRefs.current['rules'] = el; postMenuRef.current = el; }} className='post-menu-button' id='post-menu-button-catalog' rotated={openMenuCid === 'rules'} onClick={(event) => { event.stopPropagation(); const rect = threadMenuRefs.current['rules'].getBoundingClientRect(); setMenuPosition({ top: rect.top + window.scrollY, left: rect.left }); setOpenMenuCid((prevCid) => (prevCid === 'rules' ? null : 'rules')); }} > ▶
{createPortal( { postMenuCatalogRef.current = el; }} onClick={(event) => event.stopPropagation()} style={{ position: 'absolute', top: menuPosition.top + 7, left: menuPosition.left }} >
    {/* {canModerate ? ( <> change rules ) : null} */}
  • { setIsClientRedirectMenuOpen(true); }} onMouseLeave={() => { setIsClientRedirectMenuOpen(false); }} > View on »
, document.body, )}
{ setIsHoveringOnThread('rules'); setIsHoveringForMenu('rules'); const rect = threadRefs.current['rules'].getBoundingClientRect(); setPopupPosition({ top: rect.top + window.scrollY, left: rect.left }); }} onMouseLeave={() => setIsHoveringForMenu(false)} > Rules {': ' + subplebbit.rules?.map((rule, index) => `${index + 1}. ${rule}`).join(' ')}
); } else if (post.type === 'description') { return ( <> {isHoveringOnThread === 'description' ? createPortal(
handleMouseOnLeaveThread()} style={{ visibility: isCalculationDone ? 'visible' : 'hidden', left: isCalculationDone && spaceOnRight > 250 ? subplebbit.suggestedAvatarUrl ? imageRect.right + 5 : textRect.right + 5 : isCalculationDone && spaceOnRight < 250 ? subplebbit.suggestedAvatarUrl ? imageRect.left - popupRect.width - 5 : textRect.left - popupRect.width - 5 : 'auto', top: popupPosition.top + 5, }} > Welcome to {subplebbit.title || subplebbit.address} by  ## Board Mods {thread.timestamp ? getFormattedTime(thread.timestamp) : null}
, document.body, ) : null}
{ threadRefs.current['description'] = el; }} onMouseLeave={() => { handleMouseOnLeaveThread(); }} > {subplebbit.suggested?.avatarUrl ? ( { setIsHoveringOnThread('description'); setIsHoveringForMenu('description'); const rect = threadRefs.current['description'].getBoundingClientRect(); setPopupPosition({ top: rect.top + window.scrollY, left: rect.left }); }} onMouseLeave={() => setIsHoveringForMenu(false)} src={subplebbit.suggested?.avatarUrl} alt='board avatar' /> ) : null} {subplebbit.suggested?.avatarUrl ? (
) : null} { setIsHoveringForMenu('description'); handleMouseOnLeaveThread(); }} onMouseLeave={() => setIsHoveringForMenu(false)} >
R: 0 / L: 0 {subplebbit.suggested?.avatarUrl ? null : (
)} { threadMenuRefs.current['description'] = el; postMenuRef.current = el; }} className='post-menu-button' id='post-menu-button-catalog' rotated={openMenuCid === 'description'} onClick={(event) => { event.stopPropagation(); const rect = threadMenuRefs.current['description'].getBoundingClientRect(); setMenuPosition({ top: rect.top + window.scrollY, left: rect.left }); setOpenMenuCid((prevCid) => (prevCid === 'description' ? null : 'description')); }} > ▶
{createPortal( { postMenuCatalogRef.current = el; }} onClick={(event) => event.stopPropagation()} style={{ position: 'absolute', top: menuPosition.top + 7, left: menuPosition.left }} >
    {/* {canModerate ? ( <> change description ) : null} */} {subplebbit.suggested?.avatarUrl ? (
  • { setIsImageSearchOpen(true); }} onMouseLeave={() => { setIsImageSearchOpen(false); }} > Image search »
  • ) : null}
  • { setIsClientRedirectMenuOpen(true); }} onMouseLeave={() => { setIsClientRedirectMenuOpen(false); }} > View on »
    • handleOptionClick('description')}> Plebbit
    • {/*
    • handleOptionClick("description")}> Seedit
    • */}
    • handleOptionClick('description')}> Plebones
, document.body, )}
setSelectedThread('description')}>
{ setIsHoveringOnThread('description'); setIsHoveringForMenu('description'); const rect = threadRefs.current['description'].getBoundingClientRect(); setPopupPosition({ top: rect.top + window.scrollY, left: rect.left }); }} onMouseLeave={() => setIsHoveringForMenu(false)} > Welcome to {subplebbit.title || subplebbit.address}! {': ' + subplebbit.description}
); } else { return ( <> {isHoveringOnThread === thread.cid ? createPortal(
handleMouseOnLeaveThread()} style={{ visibility: isCalculationDone ? 'visible' : 'hidden', left: isCalculationDone && spaceOnRight > 250 ? isMediaShowed ? imageRect.right + 5 : textRect.right + 5 : isCalculationDone && spaceOnRight < 250 ? isMediaShowed ? imageRect.left - popupRect.width - 5 : textRect.left - popupRect.width - 5 : 'auto', top: popupPosition.top + 5, }} > {thread.title ? `${thread.title} ` : 'Posted '} by {thread.author.displayName ? ` ${thread.author.displayName} ` : ' Anonymous '} {thread.timestamp ? getFormattedTime(thread.timestamp) : null} {thread.replyCount > 0 ? (
Last reply by {' '} {' '} {getFormattedTime(thread.lastReplyTimestamp)}
) : null}
, document.body, ) : null}
{ threadRefs.current[thread.cid] = el; }} onMouseLeave={() => { handleMouseOnLeaveThread(); }} > {commentMediaInfo?.url ? ( setSelectedThread(thread.cid)} onMouseOver={() => { setIsHoveringOnThread(thread.cid); setIsHoveringForMenu(thread.cid); const rect = threadRefs.current[thread.cid].getBoundingClientRect(); setPopupPosition({ top: rect.top + window.scrollY, left: rect.left }); }} onMouseLeave={() => setIsHoveringForMenu(false)} > {commentMediaInfo?.type === 'webpage' ? ( thread.thumbnailUrl ? ( {commentMediaInfo.type} { e.target.src = fallbackImgUrl; e.target.onerror = null; }} /> ) : null ) : null} {commentMediaInfo?.type === 'iframe' && (thread.thumbnailUrl || commentMediaInfo.thumbnail) ? ( {commentMediaInfo.type} { e.target.src = fallbackImgUrl; }} /> ) : null} {commentMediaInfo?.type === 'image' ? ( {commentMediaInfo.type} { e.target.src = fallbackImgUrl; e.target.onerror = null; }} /> ) : null} {commentMediaInfo?.type === 'video' ? ( ) : null} {commentMediaInfo?.type === 'audio' ? (
); } }; const CatalogRow = ({ row }) => { const posts = []; for (const post of row) { posts.push(); } return
{posts}
; }; const Catalog = () => { const { captchaResponse, setCaptchaResponse, setChallengesArray, defaultSubplebbits, setIsCaptchaOpen, isModerationOpen, setIsModerationOpen, setCanModerate, isSettingsOpen, setIsSettingsOpen, originalCommentContent, setPendingComment, setPendingCommentIndex, setResolveCaptchaPromise, selectedAddress, setSelectedAddress, selectedStyle, selectedTitle, setSelectedTitle, showPostForm, showPostFormLink, } = useGeneralStore((state) => state); const { anonymousMode } = useAnonModeStore(); const nameRef = useRef(); const subjectRef = useRef(); const commentRef = useRef(); const linkRef = useRef(); const selectedThreadCidRef = useRef(null); const virtuosoRef = useRef(); const navigate = useNavigate(); const [, setNewErrorMessage] = useError(); const [, setNewSuccessMessage] = useSuccess(); const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [deletePost, setDeletePost] = useState(false); const [executeAnonMode, setExecuteAnonMode] = useState(false); const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); useAnonModeRef(selectedThreadCidRef, anonymousMode && executeAnonMode); const account = useAccount(); const { feed, hasMore, loadMore } = useFeed({ subplebbitAddresses: [`${selectedAddress}`], sortType: 'active' }); const { subplebbitAddress } = useParams(); const subplebbit = useSubplebbit({ subplebbitAddress: selectedAddress }); const stateString = useStateString(subplebbit); let feedWithDescriptionAndRules = [...feed]; if (subplebbit.rules) { feedWithDescriptionAndRules.unshift({ type: 'rules', content: subplebbit.rules }); } if (subplebbit.description) { feedWithDescriptionAndRules.unshift({ type: 'description', content: subplebbit.description }); } const columnWidth = 180; const windowWidth = useWindowWidth(); const columnCount = Math.floor(windowWidth / columnWidth); const rows = useFeedRows(feedWithDescriptionAndRules, columnCount); useEffect(() => { if (subplebbit.roles !== undefined) { const role = subplebbit.roles[account?.author.address]?.role; if (role === 'moderator' || role === 'admin' || role === 'owner') { setCanModerate(true); } else { setCanModerate(false); } } }, [account?.author.address, subplebbit.roles, setCanModerate]); useEffect(() => { setSelectedAddress(subplebbitAddress); }, [subplebbitAddress, setSelectedAddress]); const errorString = useMemo(() => { if (subplebbit?.state === 'failed') { let errorString = 'Failed fetching board "' + selectedAddress + '".'; if (subplebbit.error) { errorString += `: ${subplebbit.error.toString().slice(0, 300)}`; } return errorString; } }, [subplebbit?.state, subplebbit?.error, selectedAddress]); useEffect(() => { if (errorString) { setNewErrorMessage(errorString); } }, [errorString, setNewErrorMessage]); const { subscribed, subscribe, unsubscribe } = useSubscribe({ subplebbitAddress: selectedAddress }); useEffect(() => { const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress); if (subplebbitAddress) { setSelectedAddress(subplebbitAddress); } else if (subplebbit?.address) { setSelectedAddress(subplebbit.address); } if (selectedSubplebbit) { setSelectedTitle(selectedSubplebbit.title); } else if (subplebbit?.title) { setSelectedTitle(subplebbit.title); } }, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits, subplebbit?.address, subplebbit?.title]); // mobile navbar scroll effect useEffect(() => { const debouncedHandleScroll = debounce(() => { const currentScrollPos = window.pageYOffset; setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10); setPrevScrollPos(currentScrollPos); }, 50); window.addEventListener('scroll', debouncedHandleScroll); return () => window.removeEventListener('scroll', debouncedHandleScroll); }, [prevScrollPos, visible]); const onChallengeVerification = (challengeVerification) => { if (challengeVerification.challengeSuccess === true) { if (challengeVerification.publication?.cid !== undefined) { navigate(`/p/${subplebbitAddress}/c/${challengeVerification.publication?.cid}`); console.log('challenge success'); } else { setNewSuccessMessage('Challenge Success'); console.log('challenge success', challengeVerification); } } else if (challengeVerification.challengeSuccess === false) { setNewErrorMessage(`Challenge Failed, reason: ${challengeVerification.reason}. Errors: ${challengeVerification.errors}`); console.log('challenge failed', challengeVerification); } }; const onChallenge = async (challenges, comment) => { setPendingComment(comment); let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges); } catch (error) { setNewErrorMessage(error.message); console.log(error); } if (challengeAnswers) { await comment.publishChallengeAnswers(challengeAnswers); } }; useEffect(() => { setPublishCommentOptions((prevPublishCommentOptions) => ({ ...prevPublishCommentOptions, subplebbitAddress: selectedAddress, })); }, [selectedAddress]); const [publishCommentOptions, setPublishCommentOptions] = useState({ subplebbitAddress: selectedAddress, onChallenge, onChallengeVerification, onError: (error) => { setNewErrorMessage(error.message); console.log(error); }, }); const { publishComment, index } = usePublishComment(publishCommentOptions); useEffect(() => { if (index !== undefined) { setPendingCommentIndex(index); navigate(`/profile/c/${index}`); } }, [index, navigate, setPendingCommentIndex]); const resetFields = useCallback(() => { if (nameRef.current) { nameRef.current.value = ''; } if (subjectRef.current) { subjectRef.current.value = ''; } if (commentRef.current) { commentRef.current.value = ''; } if (linkRef.current) { linkRef.current.value = ''; } }, []); const handleSubmit = async (event) => { event.preventDefault(); setPublishCommentOptions((prevPublishCommentOptions) => ({ ...prevPublishCommentOptions, author: { displayName: nameRef.current.value || undefined, ...(anonymousMode ? {} : { address: account?.author.address }), }, title: subjectRef.current.value || undefined, content: commentRef.current.value || undefined, link: linkRef.current.value || undefined, })); setTriggerPublishComment(true); }; const updateSigner = useCallback(async () => { if (anonymousMode) { setExecuteAnonMode(true); let storedSigners = JSON.parse(localStorage.getItem('storedSigners')) || {}; let signer; if (!storedSigners[selectedThreadCidRef]) { signer = await account?.plebbit.createSigner(); storedSigners[selectedThreadCidRef] = { privateKey: signer?.privateKey, address: signer?.address }; localStorage.setItem('storedSigners', JSON.stringify(storedSigners)); } else { const signerPrivateKey = storedSigners[selectedThreadCidRef].privateKey; try { signer = await account?.plebbit.createSigner({ type: 'ed25519', privateKey: signerPrivateKey }); } catch (error) { console.log(error); } } setPublishCommentOptions((prevPublishCommentOptions) => { const newPublishCommentOptions = { ...prevPublishCommentOptions, signer, author: { ...prevPublishCommentOptions.author, address: signer?.address, }, }; if (JSON.stringify(prevPublishCommentOptions) !== JSON.stringify(newPublishCommentOptions)) { return newPublishCommentOptions; } return prevPublishCommentOptions; }); } }, [selectedThreadCidRef, anonymousMode, account]); useEffect(() => { updateSigner(); }, [updateSigner]); useEffect(() => { if (publishCommentOptions && triggerPublishComment) { (async () => { await publishComment(); resetFields(); })(); setTriggerPublishComment(false); setExecuteAnonMode(false); } }, [publishCommentOptions, triggerPublishComment, publishComment, resetFields]); const getChallengeAnswersFromUser = async (challenges) => { setChallengesArray(challenges); return new Promise((resolve, reject) => { const imageString = challenges?.challenges[0].challenge; const imageSource = `data:image/png;base64,${imageString}`; const challengeImg = new Image(); challengeImg.src = imageSource; challengeImg.onload = () => { setIsCaptchaOpen(true); const handleKeyDown = async (event) => { if (event.key === 'Enter') { const currentCaptchaResponse = captchaResponse; resolve(currentCaptchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); event.preventDefault(); } }; setCaptchaResponse(''); document.addEventListener('keydown', handleKeyDown); setResolveCaptchaPromise(resolve); }; challengeImg.onerror = () => { reject(setNewErrorMessage('Could not load challenges')); }; }); }; // mobile navbar board select functionality const handleSelectChange = (event) => { const selected = event.target.value; if (selected === 'subscriptions') { navigate(`/p/subscriptions`); return; } else if (selected === 'all') { navigate(`/p/all`); return; } const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title; setSelectedTitle(selectedTitle); setSelectedAddress(selected); navigate(`/p/${selected}`); }; const handleSubscribe = async () => { try { if (subscribed === false) { await subscribe(selectedAddress); } else if (subscribed === true) { await unsubscribe(selectedAddress); } } catch (error) { setNewErrorMessage(error.message); console.log(error); } }; const location = useLocation(); useEffect(() => { if (location.state?.scrollToTop) { lastVirtuosoStates[`${selectedAddress}-catalog`] = null; } const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot) => { if (snapshot?.scrollTop === 0 || snapshot?.ranges?.length) { lastVirtuosoStates[`${selectedAddress}-catalog`] = snapshot; } }); }; window.addEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState); }, [selectedAddress, location.state?.scrollToTop]); const lastVirtuosoState = lastVirtuosoStates[`${selectedAddress}-catalog`]; return ( <> {(selectedTitle ? selectedTitle : selectedAddress) + ' - Catalog - plebchan'} setIsCreateBoardOpen(false)} /> setIsSettingsOpen(false)} /> { setIsModerationOpen(false); setDeletePost(false); }} deletePost={deletePost} /> setIsEditModalOpen(false)} originalCommentContent={originalCommentContent} /> <> [All  /  Subscriptions ] [ {defaultSubplebbits.map((subplebbit, index) => ( {index === 0 ? null : '\u00a0'} { setSelectedTitle(subplebbit.title); setSelectedAddress(subplebbit.address); }} > {subplebbit.title ? subplebbit.title : subplebbit.address} {index !== defaultSubplebbits.length - 1 ? ' /' : null} ))} ] [ { window.electron && window.electron.isElectron ? setIsCreateBoardOpen(true) : alert( 'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.', ); }} > Create Board ] [ setIsSettingsOpen(true)}> Settings ] [Home]
Board     alert( 'HOW TO CREATE A BOARD (currently via CLI only):\n\n(1) have a computer you can use as server, because users will connect to your board peer-to-peer;\n\n(2) go to https://github.com/plebbit/plebbit-cli#install and follow the instructions to run your daemon, create a subplebbit (board) and edit its settings;\n\n(3) users can always connect to your board via its address, but it can be added below by submitting a pull request here: https://github.com/plebbit/temporary-default-subplebbits. This will be automated with a plebbit DAO using the plebbit token.', ) } > Create Board
setIsSettingsOpen(true)}> Settings   { handleStyleChange({ target: { value: 'Yotsuba' } }); }} > Home
 
 
<>
<>
{subplebbit.title ?? null}
p/{subplebbit.address}
[ (event.target.style.cursor = 'pointer')}> Start a New Thread ]
(event.target.style.cursor = 'pointer')}> Start a New Thread
Name {account?.author.displayName ? ( ) : ( )} Subject Comment