import React, { useState, useEffect, Fragment } from 'react'; import { Link, useNavigate, useParams, useLocation } from 'react-router-dom'; import useBoardStore from '../useBoardStore'; import { Container, NavBar, Header, Break, PostFormLink, PostFormTable, PostForm, TopBar, BoardForm } from './styles/Board.styled'; import ImageBanner from './ImageBanner'; import CaptchaModal from './CaptchaModal'; import { useFeed, useAccountsActions } from '@plebbit/plebbit-react-hooks'; import InfiniteScroll from 'react-infinite-scroller'; import { Tooltip } from 'react-tooltip'; import onError from '../utils/onError'; import onSuccess from '../utils/onSuccess'; import getDate from '../utils/getDate'; import renderComments from '../utils/renderComments'; import SettingsModal from './SettingsModal'; const Board = ({ setBodyStyle }) => { const { selectedTitle, setSelectedTitle, selectedAddress, setSelectedAddress, setSelectedThread, selectedStyle, setSelectedStyle, captchaResponse, setCaptchaResponse } = useBoardStore(state => state); const [defaultSubplebbits, setDefaultSubplebbits] = useState([]); const [showPostFormLink, setShowPostFormLink] = useState(true); const [showPostForm, setShowPostForm] = useState(false); const [name, setName] = useState(''); const [subject, setSubject] = useState(''); const [comment, setComment] = useState(''); const { publishComment } = useAccountsActions(); const [isCaptchaOpen, setIsCaptchaOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [captchaImage, setCaptchaImage] = useState(''); const navigate = useNavigate(); const location = useLocation(); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); const [endIndex, setEndIndex] = useState(2); const { feed, hasMore, loadMore } = useFeed([`${selectedAddress}`], 'new'); const [selectedFeed, setSelectedFeed] = useState(feed); const renderedFeed = selectedFeed.slice(0, endIndex); const { subplebbitAddress } = useParams(); // 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]); // fetches default subplebbits from JSON useEffect(() => { let didCancel = false; fetch( "https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/subplebbits.json", { cache: "no-cache" } ) .then((res) => res.json()) .then(res => { if (!didCancel) { setDefaultSubplebbits(res); } }); return () => { didCancel = true; }; }, []); // 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]); // reset endIndex whenever selectedAddress changes useEffect(() => { setEndIndex(2); }, [selectedAddress]); // nested routes handling useEffect(() => { const path = location.pathname; if (path.endsWith('/post')) { setShowPostFormLink(false); setShowPostForm(true); } else { setShowPostFormLink(true); setShowPostForm(false); } if (path.endsWith('/settings')) { setIsSettingsOpen(true); } else { setIsSettingsOpen(false); } }, [location.pathname]); // automatic dark mode without interefering with user's selected style useEffect(() => { const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const isDarkMode = darkModeMediaQuery.matches; if (isDarkMode) { setSelectedStyle('Tomorrow'); setBodyStyle({ background: '#1d1f21 none', color: '#c5c8c6', fontFamily: 'Arial, Helvetica, sans-serif' }); localStorage.setItem('selectedStyle', 'Tomorrow'); } const darkModeListener = (e) => { if (e.matches) { setSelectedStyle('Tomorrow'); setBodyStyle({ background: '#1d1f21 none', color: '#c5c8c6', fontFamily: 'Arial, Helvetica, sans-serif' }); localStorage.setItem('selectedStyle', 'Tomorrow'); } }; darkModeMediaQuery.addEventListener('change', darkModeListener); return () => { darkModeMediaQuery.removeEventListener('change', darkModeListener); }; }, []); const tryLoadMore = async () => { try { loadMore(); const newFeed = [...selectedFeed, ...feed]; setSelectedFeed(newFeed); setEndIndex(endIndex + 2); } catch (e) { await new Promise(resolve => setTimeout(resolve, 1000)); } }; 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 handleScroll = (event) => { const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; if (scrollTop + clientHeight >= scrollHeight) { setEndIndex(endIndex + 5); } }; const handleVoidClick = () => {}; // desktop navbar board select functionality const handleClickTitle = (title, address) => { setSelectedTitle(title); setSelectedAddress(address); setSelectedFeed(feed.filter(feed => feed.title === title)); }; // 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 handleClickForm = () => { setShowPostFormLink(false); setShowPostForm(true); navigate(`/${selectedAddress}/post`); }; const handleClickThread = (thread) => { setSelectedThread(thread); }; const handlePublishComment = async () => { try { const pendingComment = await publishComment({ content: comment, title: subject, subplebbitAddress: selectedAddress, onChallengeVerification, onChallenge, onError: onError, }); console.log(`Comment pending with index: ${pendingComment.index}`); setName(''); setSubject(''); setComment(''); } 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 handleSettingsClose = () => { setIsSettingsOpen(false); } const handleSettingsOpen = () => { setIsSettingsOpen(true); } const handleStyleChange = (event) => { switch (event.target.value) { case "Yotsuba": const yotsubaBodyStyle = { background: "#ffe url(/assets/fade.png) top repeat-x", color: "maroon", fontFamily: "Arial, Helvetica, sans-serif" }; setBodyStyle(yotsubaBodyStyle); setSelectedStyle("Yotsuba"); localStorage.setItem("selectedStyle", "Yotsuba"); localStorage.setItem("bodyStyle", JSON.stringify(yotsubaBodyStyle)); break; case "Yotsuba-B": const yotsubaBBodyStyle = { background: "#eef2ff url(/assets/fade-blue.png) top center repeat-x", color: "#000", fontFamily: "Arial, Helvetica, sans-serif" }; setBodyStyle(yotsubaBBodyStyle); setSelectedStyle("Yotsuba-B"); localStorage.setItem("selectedStyle", "Yotsuba-B"); localStorage.setItem("bodyStyle", JSON.stringify(yotsubaBBodyStyle)); break; case "Futaba": const futabaBodyStyle = { background: "#ffe", color: "maroon", fontFamily: "times new roman, serif" }; setBodyStyle(futabaBodyStyle); setSelectedStyle("Futaba"); localStorage.setItem("selectedStyle", "Futaba"); localStorage.setItem("bodyStyle", JSON.stringify(futabaBodyStyle)); break; case "Burichan": const burichanBodyStyle = { background: "#eef2ff", color: "#000", fontFamily: "times new roman, serif" }; setBodyStyle(burichanBodyStyle); setSelectedStyle("Burichan"); localStorage.setItem("selectedStyle", "Burichan"); localStorage.setItem("bodyStyle", JSON.stringify(burichanBodyStyle)); break; case "Tomorrow": const tomorrowBodyStyle = { background: "#1d1f21 none", color: "#c5c8c6", fontFamily: "Arial, Helvetica, sans-serif" }; setBodyStyle(tomorrowBodyStyle); setSelectedStyle("Tomorrow"); localStorage.setItem("selectedStyle", "Tomorrow"); localStorage.setItem("bodyStyle", JSON.stringify(tomorrowBodyStyle)); break; case "Photon": const photonBodyStyle = { background: "#eee none", color: "#333", fontFamily: "Arial, Helvetica, sans-serif" }; setBodyStyle(photonBodyStyle); setSelectedStyle("Photon"); localStorage.setItem("selectedStyle", "Photon"); localStorage.setItem("bodyStyle", JSON.stringify(photonBodyStyle)); break; default: const defaultBodyStyle = { background: "#ffe url(/assets/fade.png) top repeat-x", color: "maroon", fontFamily: "Arial, Helvetica, sans-serif" }; setBodyStyle(defaultBodyStyle); setSelectedStyle("Yotsuba"); localStorage.setItem("selectedStyle", "Yotsuba"); localStorage.setItem("bodyStyle", JSON.stringify(defaultBodyStyle)); } } 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}
[ event.target.style.cursor='pointer'}>Start a New Thread ]
event.target.style.cursor='pointer'}>Start a New Thread
Name setName(event.target.value)} /> Subject setSubject(event.target.value)} /> Comment Embed File

Style:  
[ Catalog ]
Catalog
Loading...
} > {renderedFeed.map(thread => { const { replies: { pages: { topAll: { comments } } } } = thread; const { renderedComments, omittedCount } = renderComments(comments); return (

File:  filename.something (metadata)
filename.something
{thread.title ? ( thread.title.length > 75 ? {thread.title.slice(0, 75) + " (...)"} : {thread.title} ) : null}  {thread.author.displayName ? thread.author.displayName.length > 20 ? {thread.author.displayName.slice(0, 20) + " (...)"} : {thread.author.displayName} : Anonymous}   (u/ {thread.author.address.length > 15 ? {thread.author.address.slice(0, 15) + "..."} : {thread.author.address} })   {getDate(thread.timestamp)}   c/ {thread.cid.slice(0, 8)}     [ handleClickThread(thread.cid)} className="reply-link" >Reply ]   {thread.content ? ( thread.content.length > 2000 ?
{thread.content.slice(0, 2000)} (...)

Post too long.  handleClickThread(thread.cid)} className="ttl-link">Click here  to view.
:
{thread.content}
) : null}
{omittedCount > 0 ? ( {omittedCount} post{omittedCount > 1 ? "s" : ""} omitted. Click  handleClickThread(thread.cid)} className="ttl-link">here  to view. ) : null} {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/ {reply.cid.slice(0, 8)}  
{reply.content ? ( reply.content.length > 1000 ?
{`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content.slice(0, 1000)} (...)

Comment too long.  handleClickThread(thread.cid)} className="ttl-link">Click here  to view.
:
handleQuoteClick(reply, event)}> {`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content}
) : null}
)})}

{thread.author.displayName ? thread.author.displayName.length > 15 ? {thread.author.displayName.slice(0, 15) + " (...)"} : {thread.author.displayName} : Anonymous}   (u/ {thread.author.address.length > 15 ? {thread.author.address.slice(0, 15) + "..."} : {thread.author.address} } ) 
{thread.title ? ( thread.title.length > 30 ? {thread.title.slice(0, 30) + " (...)"} : {thread.title} ) : null}
{getDate(thread.timestamp)}   c/ {thread.cid.slice(0, 8)}
58 KB JPG
{thread.content ? ( thread.content.length > 1500 ?
{thread.content.slice(0, 1500)} (...)

Post too long.  handleClickThread(thread.cid)} className="ttl-link">Click here  to view.
:
{thread.content}
) : null}
{thread.replyCount} Replies / ? Images handleClickThread(thread.cid)} className="button-mobile" >View Thread
{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 > 10 ? {reply.author.address.slice(0, 10) + "..."} : {reply.author.address} } ) 
{getDate(reply.timestamp)}  c/ {reply.cid.slice(0, 8)}
{reply.content ? ( reply.content.length > 1000 ?
{`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content.slice(0, 1000)} (...)

Comment too long.  handleClickThread(thread.cid)} className="ttl-link">Click here  to view.
:
handleQuoteClick(reply, event)}> {`c/${reply.parentCid.slice(0, 8)}`}{
} {reply.content}
) : null}
)})}
)})}
); } export default Board;