diff --git a/src/components/views/AllCatalog.jsx b/src/components/views/AllCatalog.jsx index 50947405..0f687f5b 100755 --- a/src/components/views/AllCatalog.jsx +++ b/src/components/views/AllCatalog.jsx @@ -1,46 +1,127 @@ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import InfiniteScroll from 'react-infinite-scroller'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate} from 'react-router-dom'; +import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; -import { useFeed, useSubplebbits } from '@plebbit/plebbit-react-hooks'; +import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; -import { Container, NavBar, Header, Break} from '../styled/views/Board.styled'; -import { Threads } from '../styled/views/Catalog.styled'; -import { TopBar, Footer } from '../styled/views/Thread.styled'; +import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled'; +import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled'; +import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; import CatalogLoader from '../CatalogLoader'; +import EditModal from '../modals/EditModal'; import ImageBanner from '../ImageBanner'; +import ModerationModal from '../modals/ModerationModal'; import OfflineIndicator from '../OfflineIndicator'; import SettingsModal from '../modals/SettingsModal'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; import handleStyleChange from '../../utils/handleStyleChange'; +import useError from '../../hooks/useError'; import useFeedStateString from '../../hooks/useFeedStateString'; +import useSuccess from '../../hooks/useSuccess'; import packageJson from '../../../package.json' const {version} = packageJson const AllCatalog = () => { const { + setCaptchaResponse, + setChallengesArray, defaultSubplebbits, + editedComment, + setIsAuthorDelete, + setIsAuthorEdit, + setIsCaptchaOpen, + isModerationOpen, setIsModerationOpen, isSettingsOpen, setIsSettingsOpen, - setSelectedAddress, + setModeratingCommentCid, + setResolveCaptchaPromise, + selectedAddress, setSelectedAddress, selectedStyle, setSelectedThread, setSelectedTitle, } = useGeneralStore(state => state); - const navigate = useNavigate(); + const threadMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); + const account = useAccount(); + const navigate = useNavigate(); + const [, setNewErrorMessage] = useError(); + const [, setNewSuccessMessage] = useSuccess(); + const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); + const [isHoveringOnThread, setIsHoveringOnThread] = 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 [menuPosition, setMenuPosition] = useState({top: 0, left: 0}); + const [openMenuCid, setOpenMenuCid] = useState(null); + const [moderatorPermissions, setModeratorPermissions] = useState({}); + const addresses = defaultSubplebbits.map(subplebbit => subplebbit.address); - const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: addresses, sortType: 'new'}); - const {subplebbits} = useSubplebbits({subplebbitAddresses: addresses, sortType: 'new'}); - const [setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); + 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); + + useEffect(() => { + let permissions = {}; + + 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') { + permissions[thread.subplebbitAddress] = true; + } else { + permissions[thread.subplebbitAddress] = false; + } + } + }); + + setModeratorPermissions(permissions); + }, [account?.author.address, selectedFeed, subplebbits]); + + + const handleOptionClick = () => { + setOpenMenuCid(null); + }; + + + const handleOutsideClick = useCallback((e) => { + if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { + setOpenMenuCid(null); + } + }, [openMenuCid, postMenuRef, postMenuCatalogRef]); + + + useEffect(() => { + if (openMenuCid !== null) { + document.addEventListener('click', handleOutsideClick); + } else { + document.removeEventListener('click', handleOutsideClick); + } + + return () => { + document.removeEventListener('click', handleOutsideClick); + }; + }, [openMenuCid, handleOutsideClick]); + + // mobile navbar scroll effect useEffect(() => { const debouncedHandleScroll = debounce(() => { @@ -61,12 +142,149 @@ const AllCatalog = () => { {await new Promise(resolve => setTimeout(resolve, 1000))} }; - // desktop navbar board select functionality - const handleClickTitle = (title, address) => { - setSelectedTitle(title); - setSelectedAddress(address); - setSelectedFeed(feed.filter(feed => feed.title === title)); - }; + + const onChallengeVerification = (challengeVerification) => { + if (challengeVerification.challengeSuccess === true) { + setNewSuccessMessage('Challenge Success'); + } + else if (challengeVerification.challengeSuccess === false) { + setNewErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); + } + }; + + + const onChallenge = async (challenges, comment) => { + let challengeAnswers = []; + try { + challengeAnswers = await getChallengeAnswersFromUser(challenges) + } + catch (error) { + setNewErrorMessage(error); + } + if (challengeAnswers) { + await comment.publishChallengeAnswers(challengeAnswers) + } + }; + + + const getChallengeAnswersFromUser = async (challenges) => { + setChallengesArray(challenges); + + return new Promise((resolve, reject) => { + const imageString = challenges?.challenges[0].challenge; + const imageSource = `data:image/png;base64,${imageString}`; + const challengeImg = new Image(); + challengeImg.src = imageSource; + + challengeImg.onload = () => { + setIsCaptchaOpen(true); + + const handleKeyDown = async (event) => { + if (event.key === 'Enter') { + const currentCaptchaResponse = useGeneralStore.getState().captchaResponse; + resolve(currentCaptchaResponse); + setIsCaptchaOpen(false); + document.removeEventListener('keydown', handleKeyDown); + event.preventDefault(); + } + }; + + setCaptchaResponse(''); + document.addEventListener('keydown', handleKeyDown); + + setResolveCaptchaPromise(resolve); + }; + + challengeImg.onerror = () => { + reject(setNewErrorMessage('Could not load challenges')); + }; + }); + }; + + const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({ + commentCid: commentCid, + content: editedComment || undefined, + subplebbitAddress: selectedAddress, + onChallenge, + onChallengeVerification, + onError: (error) => { + setNewErrorMessage(error); + }, + }); + + + const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); + + + const handleAuthorDeleteClick = (commentCid) => { + handleOptionClick(commentCid); + + 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); + } + + + useEffect(() => { + setPublishCommentEditOptions((prevOptions) => ({ + ...prevOptions, + commentCid: commentCid, + content: editedComment || undefined, + })); + }, [commentCid, editedComment]); + + + useEffect(() => { + if (editedComment !== '') { + setTriggerPublishCommentEdit(true); + } + }, [editedComment, setIsAuthorEdit]); + + + useEffect(() => { + if (publishCommentEditOptions && triggerPublishCommentEdit) { + (async () => { + await publishCommentEdit(); + setTriggerPublishCommentEdit(false); + })(); + } + }, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]); + // mobile navbar board select functionality const handleSelectChange = (event) => { @@ -86,6 +304,13 @@ const AllCatalog = () => { navigate(`/p/${selected}`); }; + // desktop navbar board select functionality + const handleClickTitle = (title, address) => { + setSelectedTitle(title); + setSelectedAddress(address); + setSelectedFeed(feed.filter(feed => feed.title === title)); + }; + return ( <> @@ -97,6 +322,16 @@ const AllCatalog = () => { selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} /> + {setIsModerationOpen(false); setDeletePost(false)}} + deletePost={deletePost} /> + setIsEditModalOpen(false)} + originalCommentContent={originalCommentContent} /> <> @@ -210,84 +445,189 @@ const AllCatalog = () => { {feed.length > 0 ? ( - {feed.map((thread, index) => { - const commentMediaInfo = getCommentMediaInfo(thread); - const fallbackImgUrl = "assets/filedeleted-res.gif"; - return ( - setSelectedThread(thread.cid)}> -
- {commentMediaInfo?.url ? ( - - {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" ? ( - + pageStart={0} + loadMore={tryLoadMore} + hasMore={hasMore} + > + {feed.map((thread, index) => { + const commentMediaInfo = getCommentMediaInfo(thread); + const fallbackImgUrl = "assets/filedeleted-res.gif"; + const isModerator = moderatorPermissions[thread.subplebbitAddress]; + 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 && ( - commentMediaInfo.type === 'image' || - commentMediaInfo.type === 'video' || - (commentMediaInfo.type === 'webpage' && - commentMediaInfo.thumbnail))) ? ( - // */ - - ) : ( - // */ - - ) } -
+ {commentMediaInfo?.type === "image" ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null;}} /> + ) : null} + {commentMediaInfo?.type === "video" ? ( +
- - )})} - + +
+ )})} +
) : ( )} diff --git a/src/components/views/SubscriptionsCatalog.jsx b/src/components/views/SubscriptionsCatalog.jsx index 300a9e20..b2fabbf3 100755 --- a/src/components/views/SubscriptionsCatalog.jsx +++ b/src/components/views/SubscriptionsCatalog.jsx @@ -1,45 +1,125 @@ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import InfiniteScroll from 'react-infinite-scroller'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate} from 'react-router-dom'; +import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; -import { useAccount, useFeed, useSubplebbits } from '@plebbit/plebbit-react-hooks'; +import { useAccount, useFeed, usePublishCommentEdit, useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; -import { Container, NavBar, Header, Break} from '../styled/views/Board.styled'; -import { Threads } from '../styled/views/Catalog.styled'; -import { TopBar, Footer } from '../styled/views/Thread.styled'; +import { Container, NavBar, Header, Break, PostMenu, BoardForm } from '../styled/views/Board.styled'; +import { Threads, PostMenuCatalog } from '../styled/views/Catalog.styled'; +import { TopBar, Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; +import CatalogLoader from '../CatalogLoader'; +import EditModal from '../modals/EditModal'; import ImageBanner from '../ImageBanner'; +import ModerationModal from '../modals/ModerationModal'; import OfflineIndicator from '../OfflineIndicator'; import SettingsModal from '../modals/SettingsModal'; import getCommentMediaInfo from '../../utils/getCommentMediaInfo'; import handleStyleChange from '../../utils/handleStyleChange'; +import useError from '../../hooks/useError'; import useFeedStateString from '../../hooks/useFeedStateString'; +import useSuccess from '../../hooks/useSuccess'; import packageJson from '../../../package.json' const {version} = packageJson const SubscriptionsCatalog = () => { const { + setCaptchaResponse, + setChallengesArray, defaultSubplebbits, + editedComment, + setIsAuthorDelete, + setIsAuthorEdit, + setIsCaptchaOpen, + isModerationOpen, setIsModerationOpen, isSettingsOpen, setIsSettingsOpen, - setSelectedAddress, + setModeratingCommentCid, + setResolveCaptchaPromise, + selectedAddress, setSelectedAddress, selectedStyle, setSelectedThread, setSelectedTitle, } = useGeneralStore(state => state); + const threadMenuRefs = useRef({}); + const postMenuRef = useRef(null); + const postMenuCatalogRef = useRef(null); + const account = useAccount(); const navigate = useNavigate(); + const [, setNewErrorMessage] = useError(); + const [, setNewSuccessMessage] = useSuccess(); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); - const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: account?.subscriptions, sortType: 'new'}); - const [setSelectedFeed] = useState(feed.sort((a, b) => b.timestamp - a.timestamp)); - const {subplebbits} = useSubplebbits({subplebbitAddresses: account?.subscriptions, sortType: 'new'}); + const [isHoveringOnThread, setIsHoveringOnThread] = 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 [menuPosition, setMenuPosition] = useState({top: 0, left: 0}); + const [openMenuCid, setOpenMenuCid] = useState(null); + const [moderatorPermissions, setModeratorPermissions] = useState({}); + + 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); + + useEffect(() => { + let permissions = {}; + + 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') { + permissions[thread.subplebbitAddress] = true; + } else { + permissions[thread.subplebbitAddress] = false; + } + } + }); + + setModeratorPermissions(permissions); + }, [account?.author.address, selectedFeed, subplebbits]); + + + const handleOptionClick = () => { + setOpenMenuCid(null); + }; + + + const handleOutsideClick = useCallback((e) => { + if (openMenuCid !== null && !postMenuRef.current.contains(e.target) && !postMenuCatalogRef.current.contains(e.target)) { + setOpenMenuCid(null); + } + }, [openMenuCid, postMenuRef, postMenuCatalogRef]); + + + useEffect(() => { + if (openMenuCid !== null) { + document.addEventListener('click', handleOutsideClick); + } else { + document.removeEventListener('click', handleOutsideClick); + } + + return () => { + document.removeEventListener('click', handleOutsideClick); + }; + }, [openMenuCid, handleOutsideClick]); + // mobile navbar scroll effect useEffect(() => { const debouncedHandleScroll = debounce(() => { @@ -60,12 +140,155 @@ const SubscriptionsCatalog = () => { {await new Promise(resolve => setTimeout(resolve, 1000))} }; - // desktop navbar board select functionality - const handleClickTitle = (title, address) => { - setSelectedTitle(title); - setSelectedAddress(address); - setSelectedFeed(feed.filter(feed => feed.title === title)); - }; + + const onChallengeVerification = (challengeVerification) => { + if (challengeVerification.challengeSuccess === true) { + setNewSuccessMessage('Challenge Success'); + } + else if (challengeVerification.challengeSuccess === false) { + setNewErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); + } + }; + + + const onChallenge = async (challenges, comment) => { + let challengeAnswers = []; + try { + challengeAnswers = await getChallengeAnswersFromUser(challenges) + } + catch (error) { + setNewErrorMessage(error); + } + if (challengeAnswers) { + await comment.publishChallengeAnswers(challengeAnswers) + } + }; + + + const getChallengeAnswersFromUser = async (challenges) => { + setChallengesArray(challenges); + + return new Promise((resolve, reject) => { + const imageString = challenges?.challenges[0].challenge; + const imageSource = `data:image/png;base64,${imageString}`; + const challengeImg = new Image(); + challengeImg.src = imageSource; + + challengeImg.onload = () => { + setIsCaptchaOpen(true); + + const handleKeyDown = async (event) => { + if (event.key === 'Enter') { + const currentCaptchaResponse = useGeneralStore.getState().captchaResponse; + resolve(currentCaptchaResponse); + setIsCaptchaOpen(false); + document.removeEventListener('keydown', handleKeyDown); + event.preventDefault(); + } + }; + + setCaptchaResponse(''); + document.addEventListener('keydown', handleKeyDown); + + setResolveCaptchaPromise(resolve); + }; + + challengeImg.onerror = () => { + reject(setNewErrorMessage('Could not load challenges')); + }; + }); + }; + + const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({ + commentCid: commentCid, + content: editedComment || undefined, + subplebbitAddress: selectedAddress, + onChallenge, + onChallengeVerification, + onError: (error) => { + setNewErrorMessage(error); + }, + }); + + + const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); + + + const handleAuthorDeleteClick = (commentCid) => { + handleOptionClick(commentCid); + + 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); + } + + + useEffect(() => { + setPublishCommentEditOptions((prevOptions) => ({ + ...prevOptions, + commentCid: commentCid, + content: editedComment || undefined, + })); + }, [commentCid, editedComment]); + + + useEffect(() => { + if (editedComment !== '') { + setTriggerPublishCommentEdit(true); + } + }, [editedComment, setIsAuthorEdit]); + + + useEffect(() => { + if (publishCommentEditOptions && triggerPublishCommentEdit) { + (async () => { + await publishCommentEdit(); + setTriggerPublishCommentEdit(false); + })(); + } + }, [publishCommentEditOptions, triggerPublishCommentEdit, publishCommentEdit]); + + // desktop navbar board select functionality + const handleClickTitle = (title, address) => { + setSelectedTitle(title); + setSelectedAddress(address); + setSelectedFeed(feed.filter(feed => feed.title === title)); + }; // mobile navbar board select functionality const handleSelectChange = (event) => { @@ -96,6 +319,16 @@ const SubscriptionsCatalog = () => { selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} /> + {setIsModerationOpen(false); setDeletePost(false)}} + deletePost={deletePost} /> + setIsEditModalOpen(false)} + originalCommentContent={originalCommentContent} /> <> @@ -215,88 +448,193 @@ const SubscriptionsCatalog = () => { - { feed.length < 1 ? ( - null - ) : ( + {feed.length > 0 ? ( - {feed.map((thread, index) => { - const commentMediaInfo = getCommentMediaInfo(thread); - const fallbackImgUrl = "assets/filedeleted-res.gif"; - return ( - setSelectedThread(thread.cid)}> -
- {commentMediaInfo?.url ? ( - - {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" ? ( - + pageStart={0} + loadMore={tryLoadMore} + hasMore={hasMore} + > + {feed.map((thread, index) => { + const commentMediaInfo = getCommentMediaInfo(thread); + const fallbackImgUrl = "assets/filedeleted-res.gif"; + const isModerator = moderatorPermissions[thread.subplebbitAddress]; + 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 && ( - commentMediaInfo.type === 'image' || - commentMediaInfo.type === 'video' || - (commentMediaInfo.type === 'webpage' && - commentMediaInfo.thumbnail))) ? ( - // */ - - ) : ( - // */ - - ) } -
+ {commentMediaInfo?.type === "image" ? ( + {commentMediaInfo.type} { + e.target.src = fallbackImgUrl + e.target.onerror = null;}} /> + ) : null} + {commentMediaInfo?.type === "video" ? ( +
- - )})} - + +
+ )})} +
+ ) : ( + )}