import React, { useState, useEffect } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import useAppStore from '../../useAppStore'; import { Container, NavBar, Header, Break, PostForm, PostFormTable, BoardForm } from './styles/Board.styled'; import { ReplyFormLink, TopBar, BottomBar } from './styles/Thread.styled'; import { useComment, useAccountsActions } from '@plebbit/plebbit-react-hooks'; import { Tooltip } from 'react-tooltip'; import CaptchaModal from '../CaptchaModal'; import ImageBanner from '../ImageBanner'; import PostLoader from '../PostLoader'; import ReplyModal from '../ReplyModal'; import SettingsModal from '../SettingsModal'; import handleStyleChange from '../../utils/handleStyleChange'; import onError from '../../utils/onError'; import onSuccess from '../../utils/onSuccess'; import getDate from '../../utils/getDate'; import renderThreadComments from '../../utils/renderThreadComments'; import useClickForm from '../../hooks/useClickForm'; const Thread = () => { const { captchaResponse, setCaptchaResponse, defaultSubplebbits, isSettingsOpen, setIsSettingsOpen, selectedAddress, setSelectedAddress, selectedStyle, selectedThread, setSelectedThread, selectedTitle, setSelectedTitle, showPostForm, showPostFormLink } = useAppStore(state => state); const [name, setName] = useState(''); const [subject, setSubject] = useState(''); const [commentContent, setCommentContent] = useState(''); const { publishComment } = useAccountsActions(); const [isCaptchaOpen, setIsCaptchaOpen] = useState(false); const [isReplyOpen, setIsReplyOpen] = useState(false); const [captchaImage, setCaptchaImage] = useState(''); const navigate = useNavigate(); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); const comment = useComment(selectedThread); const { subplebbitAddress, threadCid } = useParams(); const handleClickForm = useClickForm(); // temporary title from JSON, gets subplebbitAddress and threadCid from URL useEffect(() => { setSelectedAddress(subplebbitAddress); setSelectedThread(threadCid); const selectedSubplebbit = defaultSubplebbits.find((subplebbit) => subplebbit.address === subplebbitAddress); if (selectedSubplebbit) { setSelectedTitle(selectedSubplebbit.title); } }, [subplebbitAddress, setSelectedAddress, setSelectedTitle, defaultSubplebbits]); // mobile navbar scroll effect useEffect(() => { const handleScroll = () => { const currentScrollPos = window.pageYOffset; setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10); setPrevScrollPos(currentScrollPos); }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [prevScrollPos, visible]); const onChallengeVerification = (challengeVerification) => { if (challengeVerification.challengeSuccess === true) { onSuccess('challenge success', {publishedCid: challengeVerification.publication.cid}) } else if (challengeVerification.challengeSuccess === false) { onError('challenge failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); onError("Error: You seem to have mistyped the CAPTCHA. Please try again."); } } const onChallenge = async (challenges, comment) => { let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges) } catch (error) { onError(error); } if (challengeAnswers) { await comment.publishChallengeAnswers(challengeAnswers) } } const getChallengeAnswersFromUser = async (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); setCaptchaImage(imageSource); const handleKeyDown = (event) => { if (event.key === 'Enter') { setCaptchaImage(''); resolve(captchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); } }; setCaptchaResponse(''); document.addEventListener('keydown', handleKeyDown); }; challengeImg.onerror = () => { reject(onError('Could not load challenge image')); }; }); }; const handleVoidClick = () => {} // desktop navbar board select functionality const handleClickTitle = (title, address) => { setSelectedTitle(title); setSelectedAddress(address); }; // mobile navbar board select functionality const handleSelectChange = (event) => { const selected = event.target.value; const selectedTitle = defaultSubplebbits.find((subplebbit) => subplebbit.address === selected).title; setSelectedTitle(selectedTitle); setSelectedAddress(selected); navigate(`/${selected}`); } const handleClickHelp = () => { alert("- Embedding media is optional, posts can be text-only. \n- A CAPTCHA challenge will appear after posting. \n- The CAPTCHA is case-sensitive."); }; const handleClickTop = () => { window.scrollTo(0, 0); } const handleClickBottom = () => { window.scrollTo(0, document.body.scrollHeight); } const handlePublishComment = async () => { try { const pendingComment = await publishComment({ content: commentContent, title: subject, subplebbitAddress: selectedAddress, onChallengeVerification, onChallenge, onError: onError, }); console.log(`Comment pending with index: ${pendingComment.index}`); setName(''); setSubject(''); setCommentContent(''); } catch (error) { onError(error); } }; // scroll to post when quote is clicked function handleQuoteClick(reply, event) { event.preventDefault(); const cid = reply.cid.slice(0, 8); const targetElement = [...document.querySelectorAll('.post-reply')] .find(el => el.innerHTML.includes(cid)); if (targetElement) { targetElement.scrollIntoView({ behavior: "instant" }); } } const handleCaptchaClose = () => { setIsCaptchaOpen(false); }; const handleReplyClose = () => { setIsReplyOpen(false); }; const handleReplyOpen = () => { setIsReplyOpen(true); }; const handleSettingsClose = () => { setIsSettingsOpen(false); } const handleSettingsOpen = () => { setIsSettingsOpen(true); } return ( <> {defaultSubplebbits.map(subplebbit => ( [ handleClickTitle(subplebbit.title, subplebbit.address)} >{subplebbit.title} ]  ))} [ Settings ] [ handleStyleChange({target: {value: "Yotsuba"}} )}>Home ]
Board  
Settings   handleStyleChange({target: {value: "Yotsuba"}} )}>Home
 
 
<>
<>
{selectedTitle}
p/{selectedAddress}
Return
Catalog
[ event.target.style.cursor='pointer'}>Post a Reply ]
event.target.style.cursor='pointer'}>Post a Reply
Name setName(event.target.value)} /> Comment Embed File

Style:   [ Return ] [ Catalog ] [ event.target.style.cursor='pointer'} onTouchStart={handleClickBottom}>Bottom ] {comment ? ( comment.replyCount !== undefined ? ( comment.replyCount > 0 ? ( comment.replyCount === 1 ? ( {comment.replyCount} reply ) : ( {comment.replyCount} replies ) ) : ( No replies yet ) ) : ( Loading... ) ) : ( null )}
{comment ? ( <>
File:  filename.something (metadata)
filename.something
{comment.title ? ( comment.title.length > 75 ? <> {comment.title.slice(0, 75) + " (...)"} : {comment.title} ) : null}   {comment.author?.displayName ? comment.author?.displayName.length > 20 ? <> {comment.author?.displayName.slice(0, 20) + " (...)"} : {comment.author?.displayName} : Anonymous}     (u/ {comment.author?.address.length > 20 ? <> {comment.author?.address.slice(0, 20) + "..."} : {comment.author?.address} })   {getDate(comment.timestamp)}   c/   
{comment.content}
{comment.replyCount === undefined ? : null} {comment.replyCount > 0 && Object.keys(comment.replies.pages.topAll.comments).map(() => { const renderedComments = renderThreadComments(comment.replies.pages.topAll.comments); return renderedComments.map(reply => { return (
{'>>'}
  {reply.author.displayName ? reply.author.displayName.length > 20 ? <> {reply.author.displayName.slice(0, 20) + " (...)"} : {reply.author.displayName} : Anonymous}   (u/ {reply.author.address.length > 20 ? <> {reply.author.address.slice(0, 20) + "..."} : {reply.author.address} })   {getDate(reply.timestamp)}   c/  
handleQuoteClick(reply, event)}> {`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content}
) }) })}

{comment.author?.displayName ? comment.author?.displayName.length > 15 ? <> {comment.author?.displayName.slice(0, 15) + " (...)"} : {comment.author?.displayName} : Anonymous}   (u/ {comment.author?.address.length > 15 ? <> {comment.author?.address.slice(0, 15) + "..."} : {comment.author?.address} } ) 
{comment.title ? ( comment.title.length > 30 ? <> {comment.title.slice(0, 30) + " (...)"} : {comment.title} ) : null}
{getDate(comment.timestamp)}   {comment.cid?.slice(0, 8)}
58 KB JPG
{comment.content ? ( <> {comment.content} ) : null}
{comment.replyCount === undefined ? : null} {comment.replyCount > 0 && Object.keys(comment.replies.pages.topAll.comments).map(() => { const renderedComments = renderThreadComments(comment.replies.pages.topAll.comments); return renderedComments.map(reply => { return (
{reply.author.displayName ? reply.author.displayName.length > 12 ? <> {reply.author.displayName.slice(0, 12) + " (...)"} : {reply.author.displayName} : Anonymous}   (u/ {reply.author.address.length > 12 ? <> {reply.author.address.slice(0, 12) + "..."} : {reply.author.address} } )
{getDate(reply.timestamp)}  c/
handleQuoteClick(reply, event)}> {`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content}
) }) })}

[ Return ] [ Catalog ] [ event.target.style.cursor='pointer'} onTouchStart={handleClickTop}>Top ] [ event.target.style.cursor='pointer'}>Post a Reply ] {comment.replyCount > 0 ? ( {comment.replyCount} replies ) : ( No replies yet )}

Style:   {comment ? ( comment.replyCount !== undefined ? ( comment.replyCount > 0 ? ( comment.replyCount === 1 ? ( {comment.replyCount} reply ) : ( {comment.replyCount} replies ) ) : ( No replies yet ) ) : ( Loading... ) ) : ( null )}
event.target.style.cursor='pointer'}>Post a Reply
) : ( )}
); } export default Thread;