Files
5chan/src/components/views/Catalog.jsx
T
2023-09-16 17:29:14 +02:00

1660 lines
65 KiB
React
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useLayoutEffect, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async';
import { Link, 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 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,
isModerator,
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 (
<AlertModal selectedStyle={selectedStyle}>
<div className='author-delete-alert'>
<p>Are you sure you want to delete this post?</p>
<div className="author-delete-buttons">
<button onClick={onClose}>No</button>
<button
onClick={() => {
setIsAuthorDelete(true);
setIsAuthorEdit(false);
setPublishCommentEditOptions(prevOptions => ({
...prevOptions,
commentCid: comment.cid,
subplebbitAddress: comment.subplebbitAddress,
deleted: true,
}));
setTriggerPublishCommentEdit(true);
onClose();
}}
>
Yes
</button>
</div>
</div>
</AlertModal>
);
}
});
};
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(
<PostPreview selectedStyle={selectedStyle}>
<div ref={popupRef}
className="post-preview"
onMouseOver={() => 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,
}}>
<span className="post-subject" >
Rules 
</span> by 
<span className="post-author-admin"> ## Board Admins </span>
<span className='post-ago'>
{thread.timestamp ? getFormattedTime(thread.timestamp) : null}
</span>
</div>
</PostPreview>, document.body
)
: null }
<div className='thread'
ref={el => {
threadRefs.current['rules'] = el;
}}
onMouseLeave={()=>{handleMouseOnLeaveThread()}}>
<BoardForm selectedStyle={selectedStyle} style={{all: 'unset'}}
onMouseOver={() => {
setIsHoveringForMenu('rules');
handleMouseOnLeaveThread();
}}
onMouseLeave={() => setIsHoveringForMenu(false)}>
<div className='meta' title="(R)eplies / (L)ink Replies">
R:&nbsp;<b>0</b>&nbsp;/&nbsp;L:&nbsp;<b>0</b>
<div className='thread-icons'
style={{position: 'absolute', top: '-2px', right: '15px'}}>
<span className="thread-icon sticky-icon" title="Sticky"
style={{
imageRendering: "pixelated",}} />
<span className="thread-icon closed-icon" title="Closed"
style={{
imageRendering: "pixelated",}} />
</div>
<PostMenu
style={{ display: isHoveringForMenu === "rules" ? 'inline-block' : 'none',
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
zIndex: '999'}}
title="Post menu"
ref={el => {
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"));
}}
>
</PostMenu>
</div>
{createPortal(
<PostMenuCatalog selectedStyle={selectedStyle}
ref={el => {postMenuCatalogRef.current = el}}
onClick={(event) => event.stopPropagation()}
style={{position: "absolute",
top: menuPosition.top + 7,
left: menuPosition.left}}>
<div className={`post-menu-thread post-menu-thread-${"rules"}`}
style={{ display: openMenuCid === "rules" ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<a target="_blank" rel="noreferrer"
style={{color: 'inherit', textDecoration: 'none'}}
href={`https://pleb.bz/p/${selectedAddress}/?redirect=plebchan.eth.limo`}>
</a>
{/* {isModerator ? (
<>
change rules
</>
) : null} */}
</ul>
</div>
</PostMenuCatalog>, document.body
)}
</BoardForm>
<Link style={{all: "unset", cursor: "pointer"}} to={`/p/${selectedAddress}/rules`}>
<div className="teaser" style={{maxHeight: '312px'}}>
<div style={{cursor: 'pointer'}}>
<span ref={textRef}
onMouseOver={() => {
setIsHoveringOnThread('rules');
setIsHoveringForMenu('rules');
const rect = threadRefs.current['rules'].getBoundingClientRect();
setPopupPosition({top: rect.top + window.scrollY, left: rect.left});
}}
onMouseLeave={() => setIsHoveringForMenu(false)}>
<b>Rules</b>
{": " + subplebbit.rules?.map((rule, index) => `${index + 1}. ${rule}`).join(' ')}
</span>
</div>
</div>
</Link>
</div>
</>
)
} else if (post.type === 'description') {
return (
<>
{isHoveringOnThread === 'description' ?
createPortal(
<PostPreview selectedStyle={selectedStyle}>
<div ref={popupRef}
className="post-preview"
onMouseOver={() => 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,
}}>
<span className="post-subject" >
Welcome to {subplebbit.title || subplebbit.address}
</span> by 
<span className="post-author-admin"> ## Board Admins </span>
<span className='post-ago'>
{thread.timestamp ? getFormattedTime(thread.timestamp) : null}
</span>
</div>
</PostPreview>, document.body
)
: null }
<div className='thread'
ref={el => {
threadRefs.current['description'] = el;
}}
onMouseLeave={()=>{handleMouseOnLeaveThread()}}>
<Link style={{all: 'unset', cursor: 'pointer'}} to={`/p/${selectedAddress}/description`}>
{subplebbit.suggested?.avatarUrl ? (
<img className='card'
onMouseOver={() => {
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}
</Link>
{subplebbit.suggested?.avatarUrl ? (
<div className='thread-icons'>
<span className="thread-icon sticky-icon" title="Sticky"
style={{
imageRendering: "pixelated",}} />
<span className="thread-icon closed-icon" title="Closed"
style={{
imageRendering: "pixelated",}} />
</div>
) : null}
<BoardForm selectedStyle={selectedStyle} style={{all: 'unset'}}
onMouseOver={() => {
setIsHoveringForMenu('description');
handleMouseOnLeaveThread();
}}
onMouseLeave={() => setIsHoveringForMenu(false)}>
<div className='meta' title="(R)eplies / (L)ink Replies">
R:&nbsp;<b>0</b>&nbsp;/&nbsp;L:&nbsp;<b>0</b>
{subplebbit.suggested?.avatarUrl ? null : (
<div className='thread-icons'
style={{position: 'absolute', top: '-2px', right: '15px'}}>
<span className="thread-icon sticky-icon" title="Sticky"
style={{
imageRendering: "pixelated",}} />
<span className="thread-icon closed-icon" title="Closed"
style={{
imageRendering: "pixelated",}} />
</div>
)}
<PostMenu
style={{ display: isHoveringForMenu === "description" ? 'inline-block' : 'none',
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
zIndex: '999'}}
title="Post menu"
ref={el => {
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"));
}}
>
</PostMenu>
</div>
{createPortal(
<PostMenuCatalog selectedStyle={selectedStyle}
ref={el => {postMenuCatalogRef.current = el}}
onClick={(event) => event.stopPropagation()}
style={{position: "absolute",
top: menuPosition.top + 7,
left: menuPosition.left}}>
<div className={`post-menu-thread post-menu-thread-${"description"}`}
style={{ display: openMenuCid === "description" ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<a target="_blank" rel="noreferrer"
style={{color: 'inherit', textDecoration: 'none'}}
href={`https://pleb.bz/p/${selectedAddress}/?redirect=plebchan.eth.limo`}>
</a>
{/* {isModerator ? (
<>
change description
</>
) : null} */}
{subplebbit.suggested?.avatarUrl ? (
<li
onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search »
<ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick("description")}>
<a
href={`https://lens.google.com/uploadbyurl?url=${subplebbit.suggested?.avatarUrl}`}
target="_blank" rel="noreferrer"
>Google</a>
</li>
<li onClick={() => handleOptionClick("description")}>
<a
href={`https://yandex.com/images/search?url=${subplebbit.suggested?.avatarUrl}`}
target="_blank" rel="noreferrer"
>Yandex</a>
</li>
<li onClick={() => handleOptionClick("description")}>
<a
href={`https://saucenao.com/search.php?url=${subplebbit.suggested?.avatarUrl}`}
target="_blank" rel="noreferrer"
>SauceNAO</a>
</li>
</ul>
</li>
) : null}
</ul>
</div>
</PostMenuCatalog>, document.body
)}
</BoardForm>
<Link style={{all: "unset", cursor: "pointer"}} to={`/p/${selectedAddress}/description`}
onClick={() => setSelectedThread("description")}>
<div className="teaser" style={{maxHeight: '170px'}}>
<div style={{cursor: 'pointer'}}>
<span ref={textRef}
onMouseOver={() => {
setIsHoveringOnThread('description');
setIsHoveringForMenu('description');
const rect = threadRefs.current['description'].getBoundingClientRect();
setPopupPosition({top: rect.top + window.scrollY, left: rect.left});
}}
onMouseLeave={() => setIsHoveringForMenu(false)}>
<b>Welcome to {subplebbit.title || subplebbit.address}!</b>
{": " + subplebbit.description}
</span>
</div>
</div>
</Link>
</div>
</>
)
} else {
return (
<>
{isHoveringOnThread === thread.cid ?
createPortal(
<PostPreview selectedStyle={selectedStyle}>
<div ref={popupRef}
className="post-preview"
onMouseOver={() => 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,
}}>
<span className="post-subject" >
{thread.title ? `${thread.title} ` : "Posted "}
</span>
by
<span className="post-author">
{thread.author.displayName ?` ${thread.author.displayName} ` : " Anonymous "}
</span>
<span className='post-ago'>
{thread.timestamp ? getFormattedTime(thread.timestamp) : null}
</span>
{thread.replyCount > 0 ?
<div className='post-last'>
Last reply by
<span className="post-author"> Anonymous </span>
<span className='post-ago'>{getFormattedTime(thread.lastReplyTimestamp)}</span>
</div>
: null}
</div>
</PostPreview>, document.body
)
: null }
<div key={`thread-`} className="thread"
ref={el => {
threadRefs.current[thread.cid] = el;
}}
onMouseLeave={()=>{handleMouseOnLeaveThread()}}>
{commentMediaInfo?.url ? (
<Link style={{all: "unset", cursor: "pointer"}} key={`link-`} to={`/p/${selectedAddress}/c/${thread.cid}`}
onClick={() => 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 ? (
<span className="file-thumb" style={{width: displayWidth, height: displayHeight}}>
<img className="card" ref={imageRef} key={`img-`}
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
onError={(e) => {
e.target.src = fallbackImgUrl
e.target.onerror = null;
}} />
</span>
) : null
) : null}
{commentMediaInfo?.type === "iframe" && (thread.thumbnailUrl || commentMediaInfo.thumbnail) ? (
<span className="file-thumb" style={{width: displayWidth, height: displayHeight}}>
<img className="card" ref={imageRef} key={`img-`}
src={commentMediaInfo.thumbnail} alt={commentMediaInfo.type}
onError={(e) => { e.target.src = fallbackImgUrl }} />
</span>
) : null}
{commentMediaInfo?.type === "image" ? (
<span className="file-thumb" style={{width: displayWidth, height: displayHeight}}>
<img className="card" ref={imageRef} key={`img-`}
src={commentMediaInfo.url} alt={commentMediaInfo.type}
onError={(e) => {
e.target.src = fallbackImgUrl
e.target.onerror = null;}} />
</span>
) : null}
{commentMediaInfo?.type === "video" ? (
<span className="file-thumb" style={{width: displayWidth, height: displayHeight}}>
<video className="card" ref={imageRef} key={`fti-`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
</span>
) : null}
{commentMediaInfo?.type === "audio" ? (
<audio className="card" ref={imageRef} controls
key={`fti-`}
src={commentMediaInfo.url}
alt={commentMediaInfo.type}
onError={(e) => e.target.src = fallbackImgUrl} />
) : null}
</Link>
) : null}
{isMediaShowed ? (
<div key={`ti-`} className="thread-icons" >
{thread.pinned ? (
<span key={`si-`} className="thread-icon sticky-icon" title="Sticky"
style={{
imageRendering: "pixelated",}} />
) : null}
{thread.locked ? (
<span key={`li-`} className="thread-icon closed-icon" title="Closed"
style={{
imageRendering: "pixelated",}} />
) : null}
</div>
) : null}
<BoardForm selectedStyle={selectedStyle}
onMouseOver={() => {
setIsHoveringForMenu(thread.cid);
handleMouseOnLeaveThread();
}}
onMouseLeave={() => setIsHoveringForMenu(false)}
style={{ all: "unset"}}>
<div key={`meta-`} className="meta" title="(R)eplies / (L)ink Replies" >
R:&nbsp;<b key={`b-`}>{thread.replyCount}</b>
{linkCount > 0 ? (
<>
&nbsp;/
L:&nbsp;<b key={`i-`}>{linkCount}</b>
</>
) : null}
{isMediaShowed ? null : (
<div className='thread-icons'
style={{position: 'absolute', top: '-2px', right: '15px'}}>
{thread.pinned ? (
<span key={`si-`} className="thread-icon sticky-icon" title="Sticky"
style={{
imageRendering: "pixelated",}} />
) : null}
{thread.locked ? (
<span key={`li-`} className="thread-icon closed-icon" title="Closed"
style={{
imageRendering: "pixelated",}} />
) : null}
</div>
)}
<PostMenu
style={{ display: isHoveringForMenu === thread.cid ? 'inline-block' : 'none',
position: 'absolute', lineHeight: '1em', marginTop: '-1px', outline: 'none',
zIndex: '999'}}
key={`pmb-`}
title="Post menu"
ref={el => {
threadMenuRefs.current[thread.cid] = el;
postMenuRef.current = el;
}}
className='post-menu-button'
id='post-menu-button-catalog'
rotated={openMenuCid === thread.cid}
onClick={(event) => {
event.stopPropagation();
const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect();
setMenuPosition({top: rect.top + window.scrollY, left: rect.left});
setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid));
}}
>
</PostMenu>
</div>
{createPortal(
<PostMenuCatalog selectedStyle={selectedStyle}
ref={el => {postMenuCatalogRef.current = el}}
onClick={(event) => event.stopPropagation()}
style={{position: "absolute",
top: menuPosition.top + 7,
left: menuPosition.left}}>
<div className={`post-menu-thread post-menu-thread-${thread.cid}`}
style={{ display: openMenuCid === thread.cid ? 'block' : 'none' }}
>
<ul className="post-menu-catalog">
<a target="_blank" rel="noreferrer"
style={{color: 'inherit', textDecoration: 'none'}}
href={`https://pleb.bz/p/${selectedAddress}/c/${thread.cid}?redirect=plebchan.eth.limo`}>
<li onClick={() => {
handleOptionClick(thread.cid);
}}>Share thread</li>
</a>
<VerifiedAuthor commentCid={thread.cid}>{({ authorAddress }) => (
<>
{authorAddress === account?.author.address ||
authorAddress === account?.signer.address ? (
<>
<li onClick={() => handleAuthorEditClick(thread)}>Edit post</li>
<li onClick={() => handleAuthorDeleteClick(thread)}>Delete post</li>
</>
) : null}
{isModerator ? (
<>
{authorAddress === account?.author.address ||
authorAddress === account?.signer.address ? (
null
) : (
<li onClick={() => {
setModeratingCommentCid(thread.cid)
setIsModerationOpen(true);
handleOptionClick(thread.cid);
setDeletePost(true);
}}>
Delete post
</li>
)}
<li
onClick={() => {
setModeratingCommentCid(thread.cid)
setIsModerationOpen(true);
handleOptionClick(thread.cid);
}}>
Mod tools
</li>
</>
) : null}
</>
)}</VerifiedAuthor>
{isMediaShowed ? (
<li
onMouseOver={() => {setIsImageSearchOpen(true)}}
onMouseLeave={() => {setIsImageSearchOpen(false)}}>
Image search »
<ul className="dropdown-menu post-menu-catalog"
style={{display: isImageSearchOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://lens.google.com/uploadbyurl?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Google</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://yandex.com/images/search?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>Yandex</a>
</li>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://saucenao.com/search.php?url=${commentMediaInfo.url}`}
target="_blank" rel="noreferrer"
>SauceNAO</a>
</li>
</ul>
</li>
) : null
}
<li
onMouseOver={() => {setIsClientRedirectMenuOpen(true)}}
onMouseLeave={() => {setIsClientRedirectMenuOpen(false)}}>
View on »
<ul className="dropdown-menu post-menu-catalog"
style={{display: isClientRedirectMenuOpen ? 'block': 'none'}}>
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://plebbitapp.eth.limo/#/p/${selectedAddress}/c/${thread.cid}`}
target="_blank" rel="noreferrer"
>Plebbit</a>
</li>
{/* <li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://seedit.eth.limo/#/p/${selectedAddress}/c/${thread.cid}`}
target="_blank" rel="noreferrer"
>Seedit</a>
</li> */}
<li onClick={() => handleOptionClick(thread.cid)}>
<a
href={`https://plebones.netlify.app/#/p/${selectedAddress}/c/${thread.cid}`}
target="_blank" rel="noreferrer"
>Plebones</a>
</li>
</ul>
</li>
</ul>
</div>
</PostMenuCatalog>, document.body
)}
</BoardForm>
<Link style={{all: "unset", cursor: "pointer"}} key={`link2-`} to={`/p/${selectedAddress}/c/${thread.cid}`}
onClick={() => setSelectedThread(thread.cid)}>
<div key={`t-`} className="teaser" style={{maxHeight: `calc(320px - ${displayHeight})`}}>
<div style={{cursor:"pointer"}}>
<span key={`teaser-width${thread.cid}`} ref={textRef}
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)}>
<b key={`b2-`}>{thread.title ? `${thread.title}` : null}</b>
{thread.content ? `: ${thread.content}` : null}
</span>
</div>
</div>
</Link>
</div>
</>
)
}
};
const CatalogRow = ({row}) => {
const posts = []
for (const post of row) {
posts.push(<CatalogPost key={post?.cid} post={post} />)
}
return <div>{posts}</div>
}
const Catalog = () => {
const {
captchaResponse, setCaptchaResponse,
setChallengesArray,
defaultSubplebbits,
setIsCaptchaOpen,
isModerationOpen, setIsModerationOpen,
setIsModerator,
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') {
setIsModerator(true);
} else {
setIsModerator(false);
}
}
}, [account?.author.address, subplebbit.roles, setIsModerator]);
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();
if (subjectRef.current.value === "") {
setNewErrorMessage('Subject field is mandatory');
return;
}
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);
}
};
useEffect(() => {
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]);
const lastVirtuosoState = lastVirtuosoStates[`${selectedAddress}-catalog`];
return (
<>
<Helmet>
<title>{((selectedTitle ? selectedTitle : selectedAddress) + " - Catalog - plebchan")}</title>
</Helmet>
<Container>
<CreateBoardModal
selectedStyle={selectedStyle}
isOpen={isCreateBoardOpen}
closeModal={() => setIsCreateBoardOpen(false)} />
<SettingsModal
selectedStyle={selectedStyle}
isOpen={isSettingsOpen}
closeModal={() => setIsSettingsOpen(false)} />
<ModerationModal
selectedStyle={selectedStyle}
isOpen={isModerationOpen}
closeModal={() => {setIsModerationOpen(false); setDeletePost(false)}}
deletePost={deletePost} />
<EditModal
selectedStyle={selectedStyle}
isOpen={isEditModalOpen}
closeModal={() => setIsEditModalOpen(false)}
originalCommentContent={originalCommentContent} />
<NavBar selectedStyle={selectedStyle}>
<>
<span className="boardList">
[
<Link to={`/p/all`} onClick={() => window.scrollTo(0, 0)}>All</Link>
 / 
<Link to={`/p/subscriptions`} onClick={() => window.scrollTo(0, 0)}>Subscriptions</Link>
]&nbsp;[
{defaultSubplebbits.map((subplebbit, index) => (
<span className="boardList" key={`span-${subplebbit.address}`}>
{index === 0 ? null : "\u00a0"}
<Link to={`/p/${subplebbit.address}`} key={`a-${subplebbit.address}`} onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</Link>
{index !== defaultSubplebbits.length - 1 ? " /" : null}
</span>
))}
]
</span>
<span className="nav">
[
<span id="button-span" style={{cursor: 'pointer'}} onClick={
() => {
window.electron && window.electron.isElectron ? (
setIsCreateBoardOpen(true)
) : (
alert(
'You can create a board with the desktop version of plebchan:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n'
)
)
}
}>Create Board</span>
]
[
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
]
[
<Link to="/">Home</Link>
]
</span>
<div id="board-nav-mobile" style={{ top: visible ? 0 : '-23px' }}>
<div className="nav-container">
<div className="board-select">
<strong>Board</strong>
&nbsp;
<select id="board-select-mobile" value={selectedAddress} onChange={handleSelectChange}>
<option value="all">All</option>
<option value="subscriptions">Subscriptions</option>
{!defaultSubplebbits.some(subplebbit => subplebbit.address === selectedAddress) && (
<option value={selectedAddress}>{selectedAddress}</option>
)}
{defaultSubplebbits.map(subplebbit => (
<option key={`option-${subplebbit.address}`} value={subplebbit.address}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</option>
))}
</select> 
<span id="button-span" style={{cursor: 'pointer'}} onClick={
() => alert(
'You can create a board with the desktop version of plebchan:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n'
)
}>Create Board</span>
</div>
<div className="page-jump">
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
&nbsp;
<Link to="/" onClick={() => {handleStyleChange({target: {value: "Yotsuba"}}); window.scrollTo(0, 0);}}>Home</Link>
</div>
</div>
</div>
<div id="separator-mobile">&nbsp;</div>
<div id="separator-mobile">&nbsp;</div>
</>
</NavBar>
<Header selectedStyle={selectedStyle}>
<>
<div className="banner">
<ImageBanner />
</div>
<>
<div className="board-title">{subplebbit.title ?? null}</div>
<div className="board-address">p/{subplebbit.address}
<OfflineIndicator
address={subplebbit.address}
className="offline"
tooltipPlace="top" />
</div>
</>
</>
</Header>
<Break selectedStyle={selectedStyle} />
<PostForm selectedStyle={selectedStyle}>
<PostFormLink id="post-form-link" showPostFormLink={showPostFormLink} selectedStyle={selectedStyle} >
<div id="post-form-link-desktop">
[
<Link to={`/p/${subplebbitAddress}/catalog/post`} onClick={useClickForm()} onMouseOver={(event) => event.target.style.cursor='pointer'}>Start a New Thread</Link>
]
</div>
<div id="post-form-link-mobile">
<span className="btn-wrap">
<Link to={`/p/${subplebbitAddress}/catalog/post`} onClick={useClickForm()} onMouseOver={(event) => event.target.style.cursor='pointer'}>Start a New Thread</Link>
</span>
</div>
</PostFormLink>
<PostFormTable id="post-form" showPostForm={showPostForm} selectedStyle={selectedStyle} className="post-form">
<tbody>
<tr data-type="Name">
<td id="td-name">Name</td>
<td>
{account && account?.author && account?.author.displayName ? (
<input name="name" type="text" tabIndex={1} value={account?.author?.displayName} ref={nameRef} disabled />
) : (
<input name="name" type="text" placeholder="Anonymous" tabIndex={1} ref={nameRef} />
)}
</td>
</tr>
<tr data-type="Subject">
<td>Subject</td>
<td>
<input name="sub" type="text" tabIndex={3} ref={subjectRef}/>
<input id="post-button" type="submit" value="Post" tabIndex={6}
onClick={handleSubmit} />
</td>
</tr>
<tr data-type="Comment">
<td>Comment</td>
<td>
<textarea name="com" cols="48" rows="4" tabIndex={4} wrap="soft" ref={commentRef} />
</td>
</tr>
<tr data-type="File">
<td>Embed File</td>
<td>
<input name="embed" type="text" tabIndex={7} placeholder="Paste link" ref={linkRef} />
<button id="t-help" type="button" onClick={
() => alert("- Embedding media is optional, posts can be text-only. \n- A CAPTCHA challenge will appear after posting. \n- The CAPTCHA is case-sensitive.")
} data-tip="Help">?</button>
</td>
</tr>
</tbody>
</PostFormTable>
</PostForm>
<BoardStats subplebbitAddress={subplebbitAddress} />
<TopBar selectedStyle={selectedStyle}>
<hr />
<span className="style-changer">
Style:
 
<select id="style-selector" onChange={handleStyleChange} value={selectedStyle}>
<option value="Yotsuba">Yotsuba</option>
<option value="Yotsuba-B">Yotsuba B</option>
<option value="Futaba">Futaba</option>
<option value="Burichan">Burichan</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Photon">Photon</option>
</select>
</span>
<div className="return-button" id="return-button-desktop">
[
<Link to={`/p/${selectedAddress}`} onClick={()=> {window.scrollTo(0, 0)}}>Return</Link>
]
{subplebbit.roles && subplebbit?.roles[account?.author?.address]?.role === "admin" ? (
<BoardSettings subplebbit={subplebbit} />
) : null}
</div>
{subplebbit.state === "succeeded" ? (
<>
<span className="subscribe-button-desktop">
[
<span id="subscribe" style={{cursor: 'pointer'}}>
<span onClick={() => handleSubscribe()}>
{subscribed ? "Unsubscribe" : "Subscribe"}
</span>
</span>
]
</span>
<span className="subscribe-button-mobile">
<span className="btn-wrap" onClick={() => handleSubscribe()}>
{subscribed ? "Unsubscribe" : "Subscribe"}
</span>
</span>
</>
) : (
<div id="stats" style={{float: "right", marginTop: "5px"}}>
<span className={stateString ? "ellipsis" : ""}>{stateString}</span>
</div>
)}
<div id="return-button-mobile">
<span className="btn-wrap-catalog btn-wrap">
<Link to={`/p/${selectedAddress}`} onClick={()=> {window.scrollTo(0, 0)}}>Return</Link>
</span>
</div>
<hr />
</TopBar>
<Tooltip id="tooltip" className="tooltip" />
<Threads selectedStyle={selectedStyle}>
{subplebbit?.state !== "failed" && subplebbit.state === "succeeded" ? (
<Virtuoso
increaseViewportBy={{bottom: 600, top: 600}}
totalCount={rows?.length || 0}
data={rows}
style={{maxWidth: '100%'}}
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
useWindowScroll={true}
components={{ Footer: () => hasMore ? <CatalogLoader /> : null}}
endReached={loadMore}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
initialScrollTop={lastVirtuosoState?.scrollTop}
/>
) : (<CatalogLoader />)}
</Threads>
<Footer selectedStyle={selectedStyle}>
<Break id="break" selectedStyle={selectedStyle} style={{
marginTop: "-36px",
width: "100%",
}} />
<Break selectedStyle={selectedStyle} style={{
width: "100%",
}} />
<span className="style-changer" style={{
float: "right",
marginTop: "2px",
}}>
Style:
 
<select id="style-selector" onChange={handleStyleChange} value={selectedStyle}>
<option value="Yotsuba">Yotsuba</option>
<option value="Yotsuba-B">Yotsuba B</option>
<option value="Futaba">Futaba</option>
<option value="Burichan">Burichan</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Photon">Photon</option>
</select>
</span>
<NavBar selectedStyle={selectedStyle} style={{
marginTop: "42px",
}}>
<>
<span className="boardList">
[
<Link to={`/p/all`} onClick={() => window.scrollTo(0, 0)}>All</Link>
 / 
<Link to={`/p/subscriptions`} onClick={() => window.scrollTo(0, 0)}>Subscriptions</Link>
]&nbsp;
</span>
{defaultSubplebbits.map((subplebbit, index) => (
<span className="boardList" key={`span-${subplebbit.address}`}>
{index === 0 ? null : "\u00a0"}
<Link to={`/p/${subplebbit.address}`} key={`a-${subplebbit.address}`} onClick={() => {
setSelectedTitle(subplebbit.title);
setSelectedAddress(subplebbit.address);
}}
>{subplebbit.title ? subplebbit.title : subplebbit.address}</Link>
{index !== defaultSubplebbits.length - 1 ? " /" : null}
</span>
))}
<span className="nav">
[
<span id="button-span" style={{cursor: 'pointer'}} onClick={
() => {
window.electron && window.electron.isElectron ? (
setIsCreateBoardOpen(true)
) : (
alert(
'You can create a board with the desktop version of plebchan:\nhttps://github.com/plebbit/plebchan/releases/latest\n\nIf you are comfortable with the command line, use plebbit-cli:\nhttps://github.com/plebbit/plebbit-cli\n\n'
)
)
}
}>Create Board</span>
]
[
<Link to={`/p/${selectedAddress}/catalog/settings`} onClick={() => setIsSettingsOpen(true)}>Settings</Link>
]
[
<Link to="/" onClick={() => handleStyleChange({target: {value: "Yotsuba"}}
)}>Home</Link>
]
</span>
</>
</NavBar>
<div id="version">
plebchan v{version}{commitRef}. GPL-2.0
</div>
<div className="footer-links"
style={{
textAlign: "center",
fontSize: "x-small",
fontFamily: "arial",
marginTop: "5px",
marginBottom: "15px",
}}>
<a style={{textDecoration: 'underline'}} href="https://plebbit.com" target="_blank" rel="noopener noreferrer">About</a>
&nbsp;&nbsp;
<a style={{textDecoration: 'underline'}} href="https://github.com/plebbit/plebchan/releases/latest" target="_blank" rel="noopener noreferrer">App</a>
&nbsp;&nbsp;
<a style={{textDecoration: 'underline'}} href="https://twitter.com/plebchan_eth" target="_blank" rel="noopener noreferrer">Twitter</a>
&nbsp;&nbsp;
<a style={{textDecoration: 'underline'}} href="https://t.me/plebbit" target="_blank" rel="noopener noreferrer">Telegram</a>
</div>
</Footer>
</Container>
</>
);
}
export default Catalog;