import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { Tooltip } from 'react-tooltip'; import { Virtuoso } from 'react-virtuoso'; import { useAccount, useAccountComments, useFeed, usePublishComment, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { flattenCommentsPages } from '@plebbit/plebbit-react-hooks/dist/lib/utils' import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; import { Container, NavBar, Header, Break, PostFormLink, PostFormTable, PostForm, TopBar, BoardForm, PostMenu} from '../styled/Board.styled'; import { Footer } from '../styled/Thread.styled'; import ImageBanner from '../ImageBanner'; import ModerationModal from '../ModerationModal'; import OfflineIndicator from '../OfflineIndicator'; import Post from '../Post'; import PostLoader from '../PostLoader'; import ReplyModal from '../ReplyModal'; import SettingsModal from '../SettingsModal'; import findShortParentCid from '../../utils/findShortParentCid'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; import getDate from '../../utils/getDate'; import handleAddressClick from '../../utils/handleAddressClick'; import handleImageClick from '../../utils/handleImageClick'; import handleQuoteClick from '../../utils/handleQuoteClick'; import handleStyleChange from '../../utils/handleStyleChange'; import useClickForm from '../../hooks/useClickForm'; import useError from '../../hooks/useError'; import useStateString from '../../hooks/useStateString'; import useSuccess from '../../hooks/useSuccess'; import packageJson from '../../../package.json' const {version} = packageJson const Board = () => { const { setCaptchaResponse, setChallengesArray, defaultSubplebbits, setIsCaptchaOpen, isModerationOpen, setIsModerationOpen, isSettingsOpen, setIsSettingsOpen, setModeratingCommentCid, setPendingComment, setPendingCommentIndex, setResolveCaptchaPromise, selectedAddress, setSelectedAddress, setSelectedParentCid, setSelectedShortCid, selectedStyle, setSelectedThread, selectedTitle, setSelectedTitle, showPostForm, showPostFormLink, } = useGeneralStore(state => state); const account = useAccount(); const navigate = useNavigate(); const { subplebbitAddress } = useParams(); const nameRef = useRef(); const subjectRef = useRef(); const commentRef = useRef(); const linkRef = useRef(); const threadMenuRefs = useRef({}); const replyMenuRefs = useRef({}); const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'}); const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); const { subscribed, subscribe, unsubscribe } = useSubscribe({subplebbitAddress: selectedAddress}); const stateString = useStateString(subplebbit); const [isReplyOpen, setIsReplyOpen] = useState(false); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [selectedFeed, setSelectedFeed] = useState(feed); const [deletePost, setDeletePost] = useState(false); const [rotatedStates, setRotatedStates] = useState({}); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isModerator, setIsModerator] = useState(false); const [isModToolsOpen, setIsModToolsOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null); useError(errorMessage, [errorMessage]); useSuccess(successMessage, [successMessage]); useEffect(() => { setSelectedAddress(subplebbitAddress); }, [subplebbitAddress, setSelectedAddress]); useEffect(() => { if (subplebbit.roles !== undefined) { const role = subplebbit.roles[account.author.address]?.role; if (role === 'moderator' || role === 'admin' || role === 'owner') { setIsModerator(true); } else { setIsModerator(false); } } }, [account?.author.address, subplebbit.roles]); const handleOptionClick = (threadCid) => { setRotatedStates(prevState => ({ ...prevState, [threadCid]: false })); }; 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) { setErrorMessage(errorString); } }, [errorString]); const flattenedRepliesByThread = useMemo(() => { return selectedFeed.reduce((acc, thread) => { const replies = flattenCommentsPages(thread.replies); acc[thread.cid] = replies; return acc; }, {}); }, [selectedFeed]); const allParentCids = useMemo(() => { const allRepliesCids = Object.values(flattenedRepliesByThread).flatMap(replies => replies.map(reply => reply.cid)); const allThreadCids = selectedFeed.map(thread => thread.cid); return [...allThreadCids, ...allRepliesCids]; }, [flattenedRepliesByThread, selectedFeed]); const filter = useMemo(() => ({ parentCids: allParentCids }), [allParentCids]); const { accountComments } = useAccountComments({ filter }); const filteredRepliesByThread = useMemo(() => { const maxRepliesPerThread = 5; const accountRepliesNotYetInCommentReplies = selectedFeed.reduce((acc, thread) => { const replyCids = new Set(flattenedRepliesByThread[thread.cid].map(reply => reply.cid)); acc[thread.cid] = accountComments.filter(accountReply => !replyCids.has(accountReply.cid) && accountReply.parentCid === thread.cid); return acc; }, {}); return selectedFeed.reduce((acc, thread) => { const combinedReplies = [...flattenedRepliesByThread[thread.cid], ...accountRepliesNotYetInCommentReplies[thread.cid]].sort((a, b) => a.timestamp - b.timestamp); acc[thread.cid] = { displayedReplies: combinedReplies.slice(0, maxRepliesPerThread), omittedCount: Math.max(combinedReplies.length - maxRepliesPerThread, 0), }; return acc; }, {}); }, [flattenedRepliesByThread, accountComments, selectedFeed]); const pendingReplyCounts = useMemo(() => { return selectedFeed.reduce((acc, thread) => { const replyCids = new Set(flattenedRepliesByThread[thread.cid].map(reply => reply.cid)); acc[thread.cid] = accountComments.filter(accountReply => !replyCids.has(accountReply.cid) && accountReply.parentCid === thread.cid).length; return acc; }, {}); }, [flattenedRepliesByThread, accountComments, selectedFeed]); // temporary title from JSON, gets subplebbitAddress from URL useEffect(() => { setSelectedAddress(subplebbitAddress); const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress); if (selectedSubplebbit) { setSelectedTitle(selectedSubplebbit.title); } }, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits]); // sets useFeed to address from URL useEffect(() => { setSelectedFeed(feed); }, [feed]); // 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 tryLoadMore = async () => { try { await loadMore(); } catch (e) { await new Promise(resolve => setTimeout(resolve, 1000)); } }; const onChallengeVerification = (challengeVerification) => { if (challengeVerification.challengeSuccess === true) { if (challengeVerification.publication?.cid !== undefined) { navigate(`/p/${selectedAddress}/c/${challengeVerification.publication?.cid}`); console.log('challenge success'); } else { setSuccessMessage('Challenge Success'); } } else if (challengeVerification.challengeSuccess === false) { setErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); } }; const onChallenge = async (challenges, comment) => { setPendingComment(comment); let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges) } catch (error) { setErrorMessage(error); } if (challengeAnswers) { await comment.publishChallengeAnswers(challengeAnswers) } }; useEffect(() => { setPublishCommentOptions((prevPublishCommentOptions) => ({ ...prevPublishCommentOptions, subplebbitAddress: selectedAddress, })); }, [selectedAddress]); const [publishCommentOptions, setPublishCommentOptions] = useState({ subplebbitAddress: selectedAddress, onChallenge, onChallengeVerification, onError: (error) => { setErrorMessage(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(); if (subjectRef.current.value === "") { setErrorMessage('Subject field is mandatory'); return; } setPublishCommentOptions((prevPublishCommentOptions) => ({ ...prevPublishCommentOptions, author: { displayName: nameRef.current.value || undefined, }, title: subjectRef.current.value || undefined, content: commentRef.current.value || undefined, link: linkRef.current.value || undefined, })); setTriggerPublishComment(true); }; useEffect(() => { if (publishCommentOptions && triggerPublishComment) { (async () => { await publishComment(); resetFields(); })(); } }, [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 = useGeneralStore.getState().captchaResponse; resolve(currentCaptchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); event.preventDefault(); } }; setCaptchaResponse(''); document.addEventListener('keydown', handleKeyDown); setResolveCaptchaPromise(resolve); }; challengeImg.onerror = () => { reject(setErrorMessage('Could not load challenges')); }; }); }; // desktop navbar board select functionality const handleClickTitle = (title, address) => { setSelectedTitle(title); setSelectedAddress(address); setSelectedFeed(feed.filter(feed => feed.title === title)); if (subplebbitAddress === address) { window.location.reload(); } }; // 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) { setErrorMessage(error); } }; return ( <> {((selectedTitle ? selectedTitle : selectedAddress) + " - plebchan")} setIsReplyOpen(false)} /> setIsSettingsOpen(false)} /> setIsModerationOpen(false)} deletePost={deletePost} /> <> [ All  /  Subscriptions ] [ {defaultSubplebbits.map((subplebbit, index) => ( {index === 0 ? null : "\u00a0"} handleClickTitle(subplebbit.title, subplebbit.address)} >{subplebbit.title ? subplebbit.title : subplebbit.address} {index !== defaultSubplebbits.length - 1 ? " /" : null} ))} ] [ ] [ setIsSettingsOpen(true)}>Settings ] [ handleStyleChange({target: {value: "Yotsuba"}} )}>Home ]
Board    
setIsSettingsOpen(true)}>Settings   {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home
 
 
<>
{selectedTitle}
p/{selectedAddress}
[ Start a New Thread ]
Start a New Thread
Name Subject Comment