From 6f0f22046141e6f348c051f80fb4ce145268f483 Mon Sep 17 00:00:00 2001 From: "plebeius.eth" Date: Wed, 9 Aug 2023 12:11:53 +0200 Subject: [PATCH] implement virtuoso in catalog views --- src/components/views/AllCatalog.jsx | 697 +++++----- src/components/views/Catalog.jsx | 1166 +++++++++-------- src/components/views/SubscriptionsCatalog.jsx | 683 +++++----- src/hooks/stores/useGeneralStore.js | 12 + 4 files changed, 1337 insertions(+), 1221 deletions(-) diff --git a/src/components/views/AllCatalog.jsx b/src/components/views/AllCatalog.jsx index 37af7012..1bd8efa8 100755 --- a/src/components/views/AllCatalog.jsx +++ b/src/components/views/AllCatalog.jsx @@ -5,7 +5,7 @@ import { Link, useNavigate} from 'react-router-dom'; import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; import { Virtuoso } from 'react-virtuoso'; -import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; +import { useAccount, useFeed, usePublishCommentEdit, useSubplebbit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled'; import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled'; @@ -32,103 +32,130 @@ const {version} = packageJson let lastVirtuosoStates = {} -const AllCatalog = () => { +const CatalogPost = ({post}) => { const { - setCaptchaResponse, - setChallengesArray, - defaultSubplebbits, editedComment, + setDeletePost, setIsAuthorDelete, + captchaResponse, setCaptchaResponse, + setIsModerationOpen, + setChallengesArray, setIsAuthorEdit, setIsCaptchaOpen, - isModerationOpen, setIsModerationOpen, - isSettingsOpen, setIsSettingsOpen, + setIsEditModalOpen, setModeratingCommentCid, + setOriginalCommentContent, + setPendingComment, setResolveCaptchaPromise, selectedAddress, setSelectedAddress, selectedStyle, setSelectedThread, - setSelectedTitle, } = 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 [openMenuCid, setOpenMenuCid] = useState(null); + const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); + const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); + const [commentCid, setCommentCid] = useState(null); + const [isModerator, setIsModerator] = useState(false); + + const [, setNewErrorMessage] = useError(); + const [, setNewSuccessMessage] = useSuccess(); + + const account = useAccount(); + const subplebbit = useSubplebbit({subplebbitAddress: thread.subplebbitAddress}); const threadMenuRefs = useRef({}); const postMenuRef = useRef(null); const postMenuCatalogRef = useRef(null); - const virtuosoRef = useRef(); - - const account = useAccount(); - const navigate = useNavigate(); - const [, setNewErrorMessage] = useError(); - const [, setNewSuccessMessage] = useSuccess(); - - const [prevScrollPos, setPrevScrollPos] = useState(0); - const [visible, setVisible] = useState(true); - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [originalCommentContent, setOriginalCommentContent] = useState(null); - const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); - const [deletePost, setDeletePost] = useState(false); - const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); - const [commentCid, setCommentCid] = useState(null); - const [moderatorPermissions, setModeratorPermissions] = useState({}); - const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); - - const addresses = defaultSubplebbits.map(subplebbit => subplebbit.address); - const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: addresses, sortType: 'active'}); - const {subplebbits} = useSubplebbits({subplebbitAddresses: addresses, sortType: 'active'}); - const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); - - const stateString = useFeedStateString(subplebbits); - - const columnWidth = 180; - const windowWidth = useWindowWidth(); - const columnCount = Math.floor(windowWidth / columnWidth); - const rows = useFeedRows(selectedFeed, columnCount); useEffect(() => { - let permissions = {}; + if (subplebbit.roles !== undefined) { + const role = subplebbit.roles[account?.author.address]?.role; - selectedFeed.forEach(thread => { - const subplebbit = subplebbits.find(s => s && s.address === thread.subplebbitAddress); - - if (subplebbit && subplebbit.roles) { - 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 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); + } - if (role === 'moderator' || role === 'admin' || role === 'owner') { - permissions[thread.subplebbitAddress] = true; - } else { - permissions[thread.subplebbitAddress] = false; - } + 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?

+
+ + +
+
+
+ ); } }); - - setModeratorPermissions(permissions); - }, [account?.author.address, selectedFeed, subplebbits]); - - - // 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 {loadMore()} - catch (e) - {await new Promise(resolve => setTimeout(resolve, 1000))} }; + 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'); @@ -141,6 +168,7 @@ const AllCatalog = () => { const onChallenge = async (challenges, comment) => { + setPendingComment(comment); let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges) @@ -168,7 +196,7 @@ const AllCatalog = () => { const handleKeyDown = async (event) => { if (event.key === 'Enter') { - const currentCaptchaResponse = useGeneralStore.getState().captchaResponse; + const currentCaptchaResponse = captchaResponse; resolve(currentCaptchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); @@ -202,7 +230,7 @@ const AllCatalog = () => { const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); - + useEffect(() => { setPublishCommentEditOptions((prevOptions) => ({ ...prevOptions, @@ -236,6 +264,250 @@ const AllCatalog = () => { }, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]); + return ( +
{setIsHoveringOnThread(thread.cid)}} + onMouseLeave={() => {setIsHoveringOnThread('')}}> + {commentMediaInfo?.url ? ( + setSelectedThread(thread.cid)}> + {commentMediaInfo?.type === "webpage" ? ( + thread.thumbnailUrl ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null; + }} /> + ) : null + ) : null} + {commentMediaInfo?.type === "image" ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null;}} /> + ) : null} + {commentMediaInfo?.type === "video" ? ( +
+ ); +}; + + +const CatalogRow = ({row}) => { + const posts = [] + for (const post of row) { + posts.push() + } + return
{posts}
+} + + +const AllCatalog = () => { + const { + defaultSubplebbits, + isModerationOpen, setIsModerationOpen, + isSettingsOpen, setIsSettingsOpen, + originalCommentContent, + selectedAddress, setSelectedAddress, + selectedStyle, + setSelectedTitle, + } = useGeneralStore(state => state); + + const virtuosoRef = useRef(); + + const navigate = useNavigate(); + + const [prevScrollPos, setPrevScrollPos] = useState(0); + const [visible, setVisible] = useState(true); + + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [deletePost, setDeletePost] = useState(false); + const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); + + const addresses = defaultSubplebbits.map(subplebbit => subplebbit.address); + const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: addresses, sortType: 'active'}); + const {subplebbits} = useSubplebbits({subplebbitAddresses: addresses, sortType: 'active'}); + const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); + + const stateString = useFeedStateString(subplebbits); + + const columnWidth = 180; + const windowWidth = useWindowWidth(); + const columnCount = Math.floor(windowWidth / columnWidth); + const rows = useFeedRows(selectedFeed, columnCount); + + // 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]); + // mobile navbar board select functionality const handleSelectChange = (event) => { const selected = event.target.value; @@ -262,279 +534,6 @@ const AllCatalog = () => { }; - const CatalogPost = ({post}) => { - 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 [openMenuCid, setOpenMenuCid] = useState(null); - const isModerator = moderatorPermissions[thread.subplebbitAddress]; - - 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); - } - - return ( -
{setIsHoveringOnThread(thread.cid)}} - onMouseLeave={() => {setIsHoveringOnThread('')}}> - {commentMediaInfo?.url ? ( - setSelectedThread(thread.cid)}> - {commentMediaInfo?.type === "webpage" ? ( - thread.thumbnailUrl ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null; - }} /> - ) : null - ) : null} - {commentMediaInfo?.type === "image" ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null;}} /> - ) : null} - {commentMediaInfo?.type === "video" ? ( -
- ); - }; - - - const CatalogRow = ({row}) => { - const posts = [] - for (const post of row) { - posts.push() - } - return
{posts}
- } - useEffect(() => { const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot) => { @@ -702,7 +701,7 @@ const AllCatalog = () => { itemContent={(index, row) => } useWindowScroll={true} components={{ Footer: () => hasMore ? : null}} - endReached={tryLoadMore} + endReached={loadMore} ref={virtuosoRef} restoreStateFrom={lastVirtuosoState} initialScrollTop={lastVirtuosoState?.scrollTop} diff --git a/src/components/views/Catalog.jsx b/src/components/views/Catalog.jsx index 3c6dd1d6..cb510762 100755 --- a/src/components/views/Catalog.jsx +++ b/src/components/views/Catalog.jsx @@ -35,24 +35,644 @@ const {version} = packageJson let lastVirtuosoStates = {} +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 [openMenuCid, setOpenMenuCid] = useState(null); + const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); + const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); + const [commentCid, setCommentCid] = useState(null); + + const threadMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); + const { subplebbitAddress } = useParams(); + + const [, setNewErrorMessage] = useError(); + const [, setNewSuccessMessage] = useSuccess(); + + const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); + + const account = useAccount(); + + + 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'); + } + 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')); + }; + }); + }; + + + + if (post.type === 'rules') { + return ( +
setIsHoveringOnThread('rules')} + onMouseLeave={() => setIsHoveringOnThread('')}> + +
+ 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}}> +
+
    +
  • handleOptionClick("rules")}>Hide thread
  • + {/* {isModerator ? ( + <> + change rules + + ) : null} */} +
+
+
, document.body + )} +
+ +
+ Rules + {": " + subplebbit.rules?.map((rule, index) => `${index + 1}. ${rule}`).join(' ')} +
+ +
+ ) + } else if (post.type === 'description') { + return ( + <> +
setIsHoveringOnThread('description')} + onMouseLeave={() => setIsHoveringOnThread('')}> + + {subplebbit.suggested?.avatarUrl ? ( + board avatar + ) : null} + + {subplebbit.suggested?.avatarUrl ? ( +
+ + +
+ ) : null} + +
+ 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}}> +
+
    +
  • handleOptionClick("description")}>Hide thread
  • + {/* {isModerator ? ( + <> + change description + + ) : null} */} + {subplebbit.suggested?.avatarUrl ? ( +
  • {setIsImageSearchOpen(true)}} + onMouseLeave={() => {setIsImageSearchOpen(false)}}> + Image search » +
      +
    • handleOptionClick("description")}> + Google +
    • +
    • handleOptionClick("description")}> + Yandex +
    • +
    • handleOptionClick("description")}> + SauceNAO +
    • +
    +
  • + ) : null} +
+
+
, document.body + )} +
+ setSelectedThread("description")}> +
+ Welcome to {subplebbit.title || subplebbit.address} + {": " + subplebbit.description} +
+ +
+ + ) + } else { + return ( +
{setIsHoveringOnThread(thread.cid)}} + onMouseLeave={() => {setIsHoveringOnThread('')}}> + {commentMediaInfo?.url ? ( + setSelectedThread(thread.cid)}> + {commentMediaInfo?.type === "webpage" ? ( + thread.thumbnailUrl ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null; + }} /> + ) : null + ) : null} + {commentMediaInfo?.type === "image" ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null;}} /> + ) : null} + {commentMediaInfo?.type === "video" ? ( +
+ ) + } +}; + + +const CatalogRow = ({row}) => { + const posts = [] + for (const post of row) { + posts.push() + } + return
{posts}
+} + + const Catalog = () => { const { captchaResponse, setCaptchaResponse, setChallengesArray, defaultSubplebbits, - editedComment, - setIsAuthorDelete, - setIsAuthorEdit, setIsCaptchaOpen, isModerationOpen, setIsModerationOpen, + setIsModerator, isSettingsOpen, setIsSettingsOpen, - setModeratingCommentCid, + originalCommentContent, setPendingComment, setPendingCommentIndex, setResolveCaptchaPromise, selectedAddress, setSelectedAddress, selectedStyle, - setSelectedThread, selectedTitle, setSelectedTitle, showPostForm, showPostFormLink, @@ -64,9 +684,6 @@ const Catalog = () => { const subjectRef = useRef(); const commentRef = useRef(); const linkRef = useRef(); - const threadMenuRefs = useRef({}); - const postMenuRef = useRef(null); - const postMenuCatalogRef = useRef(null); const selectedThreadCidRef = useRef(null); const virtuosoRef = useRef(); @@ -78,13 +695,8 @@ const Catalog = () => { const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); - const [isModerator, setIsModerator] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [originalCommentContent, setOriginalCommentContent] = useState(null); - const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); const [deletePost, setDeletePost] = useState(false); - const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); - const [commentCid, setCommentCid] = useState(null); const [executeAnonMode, setExecuteAnonMode] = useState(false); const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); @@ -97,13 +709,15 @@ const Catalog = () => { 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(); @@ -120,7 +734,7 @@ const Catalog = () => { setIsModerator(false); } } - }, [account?.author.address, subplebbit.roles]); + }, [account?.author.address, subplebbit.roles, setIsModerator]); useEffect(() => { @@ -232,7 +846,7 @@ const Catalog = () => { useEffect(() => { if (index !== undefined) { setPendingCommentIndex(index); - navigate(`/profile/c/`); + navigate(`/profile/c/${index}`); } }, [index, navigate, setPendingCommentIndex]); @@ -368,46 +982,6 @@ const Catalog = () => { }; - 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]); - // mobile navbar board select functionality const handleSelectChange = (event) => { const selected = event.target.value; @@ -439,486 +1013,6 @@ const Catalog = () => { }; - const CatalogPost = ({post}) => { - 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 [openMenuCid, setOpenMenuCid] = useState(null); - - 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); - } - - - if (post.type === 'rules') { - return ( -
setIsHoveringOnThread('rules')} - onMouseLeave={() => setIsHoveringOnThread('')}> - -
- 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}}> -
-
    -
  • handleOptionClick("rules")}>Hide thread
  • - {/* {isModerator ? ( - <> - change rules - - ) : null} */} -
-
-
, document.body - )} -
- -
- Rules - {": " + subplebbit.rules.map((rule, index) => `${index + 1}. ${rule}`).join(' ')} -
- -
- ) - } else if (post.type === 'description') { - return ( - <> -
setIsHoveringOnThread('description')} - onMouseLeave={() => setIsHoveringOnThread('')}> - - {subplebbit.suggested?.avatarUrl ? ( - board avatar - ) : null} - - {subplebbit.suggested?.avatarUrl ? ( -
- - -
- ) : null} - -
- 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}}> -
-
    -
  • handleOptionClick("description")}>Hide thread
  • - {/* {isModerator ? ( - <> - change description - - ) : null} */} - {subplebbit.suggested?.avatarUrl ? ( -
  • {setIsImageSearchOpen(true)}} - onMouseLeave={() => {setIsImageSearchOpen(false)}}> - Image search » -
      -
    • handleOptionClick("description")}> - Google -
    • -
    • handleOptionClick("description")}> - Yandex -
    • -
    • handleOptionClick("description")}> - SauceNAO -
    • -
    -
  • - ) : null} -
-
-
, document.body - )} -
- setSelectedThread("description")}> -
- Welcome to {subplebbit.title || subplebbit.address} - {": " + subplebbit.description} -
- -
- - ) - } else { - return ( -
{setIsHoveringOnThread(thread.cid)}} - onMouseLeave={() => {setIsHoveringOnThread('')}}> - {commentMediaInfo?.url ? ( - setSelectedThread(thread.cid)}> - {commentMediaInfo?.type === "webpage" ? ( - thread.thumbnailUrl ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null; - }} /> - ) : null - ) : null} - {commentMediaInfo?.type === "image" ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null;}} /> - ) : null} - {commentMediaInfo?.type === "video" ? ( -
- ) - } - }; - - - const CatalogRow = ({row}) => { - const posts = [] - for (const post of row) { - posts.push() - } - return
{posts}
- } - useEffect(() => { const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot) => { diff --git a/src/components/views/SubscriptionsCatalog.jsx b/src/components/views/SubscriptionsCatalog.jsx index 468cc987..a642e035 100755 --- a/src/components/views/SubscriptionsCatalog.jsx +++ b/src/components/views/SubscriptionsCatalog.jsx @@ -5,7 +5,7 @@ import { Link, useNavigate} from 'react-router-dom'; import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; import { Virtuoso } from 'react-virtuoso'; -import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; +import { useAccount, useFeed, usePublishCommentEdit, useSubplebbit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled'; import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled'; @@ -32,92 +32,128 @@ const {version} = packageJson let lastVirtuosoStates = {} -const SubscriptionsCatalog = () => { +const CatalogPost = ({post}) => { const { - setCaptchaResponse, - setChallengesArray, - defaultSubplebbits, editedComment, + setDeletePost, setIsAuthorDelete, + captchaResponse, setCaptchaResponse, + setIsModerationOpen, + setChallengesArray, setIsAuthorEdit, setIsCaptchaOpen, - isModerationOpen, setIsModerationOpen, - isSettingsOpen, setIsSettingsOpen, + setIsEditModalOpen, setModeratingCommentCid, + setOriginalCommentContent, + setPendingComment, setResolveCaptchaPromise, selectedAddress, setSelectedAddress, selectedStyle, setSelectedThread, - setSelectedTitle, } = useGeneralStore(state => state); - const threadMenuRefs = useRef({}); - const postMenuRef = useRef(null); - const postMenuCatalogRef = useRef(null); - const virtuosoRef = useRef(); + 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 [openMenuCid, setOpenMenuCid] = useState(null); + const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); + const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); + const [commentCid, setCommentCid] = useState(null); + const [isModerator, setIsModerator] = useState(false); - const account = useAccount(); - const navigate = useNavigate(); const [, setNewErrorMessage] = useError(); const [, setNewSuccessMessage] = useSuccess(); - const [prevScrollPos, setPrevScrollPos] = useState(0); - const [visible, setVisible] = useState(true); - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [originalCommentContent, setOriginalCommentContent] = useState(null); - const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); - const [deletePost, setDeletePost] = useState(false); - const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); - const [commentCid, setCommentCid] = useState(null); - const [moderatorPermissions, setModeratorPermissions] = useState({}); - const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); - - const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: account?.subscriptions, sortType: 'active'}); - const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); - const {subplebbits} = useSubplebbits({subplebbitAddresses: account?.subscriptions, sortType: 'active'}); - - const stateString = useFeedStateString(subplebbits); - - const columnWidth = 180; - const windowWidth = useWindowWidth(); - const columnCount = Math.floor(windowWidth / columnWidth); - const rows = useFeedRows(feed, columnCount); + const account = useAccount(); + const subplebbit = useSubplebbit({subplebbitAddress: thread.subplebbitAddress}); + const threadMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); useEffect(() => { - let permissions = {}; + if (subplebbit.roles !== undefined) { + const role = subplebbit.roles[account?.author.address]?.role; - selectedFeed.forEach(thread => { - const subplebbit = subplebbits.find(s => s && s.address === thread.subplebbitAddress); - - if (subplebbit && subplebbit.roles) { - 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 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); + } - if (role === 'moderator' || role === 'admin' || role === 'owner') { - permissions[thread.subplebbitAddress] = true; - } else { - permissions[thread.subplebbitAddress] = false; - } + 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?

+
+ + +
+
+
+ ); } }); - - setModeratorPermissions(permissions); - }, [account?.author.address, selectedFeed, subplebbits]); + }; - // 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 handleAuthorEditClick = (comment) => { + handleOptionClick(comment.cid); + setIsAuthorEdit(true); + setIsAuthorDelete(false); + setCommentCid(comment.cid); + setOriginalCommentContent(comment.content); + setIsEditModalOpen(true); + } + const onChallengeVerification = (challengeVerification) => { @@ -132,6 +168,7 @@ const SubscriptionsCatalog = () => { const onChallenge = async (challenges, comment) => { + setPendingComment(comment); let challengeAnswers = []; try { challengeAnswers = await getChallengeAnswersFromUser(challenges) @@ -159,7 +196,7 @@ const SubscriptionsCatalog = () => { const handleKeyDown = async (event) => { if (event.key === 'Enter') { - const currentCaptchaResponse = useGeneralStore.getState().captchaResponse; + const currentCaptchaResponse = captchaResponse; resolve(currentCaptchaResponse); setIsCaptchaOpen(false); document.removeEventListener('keydown', handleKeyDown); @@ -226,6 +263,253 @@ const SubscriptionsCatalog = () => { }; }, [triggerPublishCommentEdit, publishCommentEdit, publishCommentEditOptions]); + + return ( +
{setIsHoveringOnThread(thread.cid)}} + onMouseLeave={() => {setIsHoveringOnThread('')}}> + {commentMediaInfo?.url ? ( + setSelectedThread(thread.cid)}> + {commentMediaInfo?.type === "webpage" ? ( + thread.thumbnailUrl ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null; + }} /> + ) : null + ) : null} + {commentMediaInfo?.type === "image" ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null;}} /> + ) : null} + {commentMediaInfo?.type === "video" ? ( +
+ ); +}; + + +const CatalogRow = ({row}) => { + const posts = [] + for (const post of row) { + posts.push() + } + return
{posts}
+} + + +const SubscriptionsCatalog = () => { + const { + defaultSubplebbits, + isModerationOpen, setIsModerationOpen, + isSettingsOpen, setIsSettingsOpen, + originalCommentContent, + selectedAddress, setSelectedAddress, + selectedStyle, + setSelectedTitle, + } = useGeneralStore(state => state); + + const virtuosoRef = useRef(); + + const account = useAccount(); + const navigate = useNavigate(); + + const [prevScrollPos, setPrevScrollPos] = useState(0); + const [visible, setVisible] = useState(true); + + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [deletePost, setDeletePost] = useState(false); + const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false); + + const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: account?.subscriptions, sortType: 'active'}); + const [selectedFeed, setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); + const {subplebbits} = useSubplebbits({subplebbitAddresses: account?.subscriptions, sortType: 'active'}); + + const stateString = useFeedStateString(subplebbits); + + const columnWidth = 180; + const windowWidth = useWindowWidth(); + const columnCount = Math.floor(windowWidth / columnWidth); + const rows = useFeedRows(selectedFeed, columnCount); + + + // 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]); + + // desktop navbar board select functionality const handleClickTitle = (title, address) => { setSelectedTitle(title); @@ -252,279 +536,6 @@ const SubscriptionsCatalog = () => { }; - const CatalogPost = ({post}) => { - 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 [openMenuCid, setOpenMenuCid] = useState(null); - const isModerator = moderatorPermissions[thread.subplebbitAddress]; - - 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); - } - - return ( -
{setIsHoveringOnThread(thread.cid)}} - onMouseLeave={() => {setIsHoveringOnThread('')}}> - {commentMediaInfo?.url ? ( - setSelectedThread(thread.cid)}> - {commentMediaInfo?.type === "webpage" ? ( - thread.thumbnailUrl ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null; - }} /> - ) : null - ) : null} - {commentMediaInfo?.type === "image" ? ( - {commentMediaInfo.type} { - e.target.src = fallbackImgUrl - e.target.onerror = null;}} /> - ) : null} - {commentMediaInfo?.type === "video" ? ( -
- ); - }; - - - const CatalogRow = ({row}) => { - const posts = [] - for (const post of row) { - posts.push() - } - return
{posts}
- } - useEffect(() => { const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot) => { diff --git a/src/hooks/stores/useGeneralStore.js b/src/hooks/stores/useGeneralStore.js index 789a112a..1fabcd4a 100644 --- a/src/hooks/stores/useGeneralStore.js +++ b/src/hooks/stores/useGeneralStore.js @@ -20,6 +20,9 @@ const useGeneralStore = create((set) => ({ defaultSubplebbits: [], setDefaultSubplebbits: (subplebbits) => set({ defaultSubplebbits: subplebbits }), + deletePost: false, + setDeletePost: (deletePost) => set({ deletePost }), + editedComment: '', setEditedComment: (comment) => set({ editedComment: comment }), @@ -40,6 +43,12 @@ const useGeneralStore = create((set) => ({ isCaptchaOpen: false, setIsCaptchaOpen: (isOpen) => set({ isCaptchaOpen: isOpen }), + isEditModalOpen: false, + setIsEditModalOpen: (isOpen) => set({ isEditModalOpen: isOpen }), + + isModerator: false, + setIsModerator: (isModerator) => set({ isModerator }), + isModerationOpen: false, setIsModerationOpen: (isOpen) => set({ isModerationOpen: isOpen }), @@ -52,6 +61,9 @@ const useGeneralStore = create((set) => ({ moderatingCommentCid: '', setModeratingCommentCid: (cid) => set({ moderatingCommentCid: cid }), + originalCommentContent: null, + setOriginalCommentContent: (content) => set({ originalCommentContent: content }), + pendingComment: '', setPendingComment: (comment) => set({ pendingComment: comment }),