From 52b7243035ee334afd7f913d1e2bbb594bdb1bf6 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 25 May 2023 13:27:09 +0200 Subject: [PATCH 01/16] fix styling and positioning of post menu --- src/components/styled/views/Board.styled.jsx | 66 ++++++++------------ src/components/views/Board.jsx | 2 +- src/components/views/Thread.jsx | 4 +- 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/components/styled/views/Board.styled.jsx b/src/components/styled/views/Board.styled.jsx index 4b991b81..d669bdb8 100644 --- a/src/components/styled/views/Board.styled.jsx +++ b/src/components/styled/views/Board.styled.jsx @@ -1721,15 +1721,13 @@ export const BoardForm = styled.div` } #post-menu { - ul { - background-color: #f0e0d6; - border: 1px solid #d9bfb7; - border-bottom: 1px solid #d9bfb7; - border-right-width: 2px; - } + border-right: 1px solid #d9bfb7; + border-bottom: 2px solid #d9bfb7; li { - border-bottom: 1px solid #ccc; + border: 1px solid #d9bfb7; + border-bottom: none; + background-color: #f0e0d6; :hover { background-color: #ffe; @@ -1746,14 +1744,13 @@ export const BoardForm = styled.div` } #post-menu { - ul { - background-color: #d6daf0; - border: 1px solid #b7c5d9; - border-right-width: 2px; - } - + border-right: 1px solid #b7c5d9; + border-bottom: 2px solid #b7c5d9; + li { - border-bottom: 1px solid #b7c5d9; + border: 1px solid #b7c5d9; + border-bottom: none; + background-color: #d6daf0; :hover { background-color: #eef2ff; @@ -1768,14 +1765,11 @@ export const BoardForm = styled.div` #post-menu { font-size: 13px !important; - - ul { - background-color: #f0e0d6; - border: 1px solid #d9bfb7; - border-bottom: none; - } - + border: 1px solid #d9bfb7; + border-bottom: none; + li { + background-color: #f0e0d6; border-bottom: 1px solid #d9bfb7; :hover { @@ -1791,14 +1785,11 @@ export const BoardForm = styled.div` #post-menu { font-size: 13px !important; - - ul { - background-color: #d6daf0; - border: 1px solid #b7c5d9; - border-bottom: none; - } - + border: 1px solid #b7c5d9; + border-bottom: none; + li { + background-color: #d6daf0; border-bottom: 1px solid #b7c5d9; :hover { @@ -1814,21 +1805,18 @@ export const BoardForm = styled.div` } #post-menu { - ul { - background-color: #282a2e; - border: 1px solid #000; - border-bottom: none; - } + border: 1px solid #000; + border-bottom: none; li { + background-color: #282a2e; border-bottom: 1px solid #000; :hover { background-color: #1d1f21; } } - } -`; + }`; case 'Photon': return `.highlighted { @@ -1837,13 +1825,11 @@ export const BoardForm = styled.div` } #post-menu { - ul { - background-color: #ddd; - border: 1px solid #ccc; - border-bottom: none; - } + border: 1px solid #ccc; + border-bottom: none; li { + background-color: #ddd; border-bottom: 1px solid #ccc; :hover { diff --git a/src/components/views/Board.jsx b/src/components/views/Board.jsx index 0edd2c99..76227bf6 100644 --- a/src/components/views/Board.jsx +++ b/src/components/views/Board.jsx @@ -835,7 +835,7 @@ const Board = () => { onClick={() => { const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect(); const menu = document.querySelector(`.post-menu-thread-${thread.cid}`); - menu.style.top = `calc(${rect.top}px + 17px)`; + menu.style.top = `calc(${rect.top + window.scrollY}px - 258px)`; menu.style.left = `${rect.left}px`; setRotatedStates(prevState => ({ diff --git a/src/components/views/Thread.jsx b/src/components/views/Thread.jsx index 63fd9117..9c0ad93b 100644 --- a/src/components/views/Thread.jsx +++ b/src/components/views/Thread.jsx @@ -796,7 +796,7 @@ const Thread = () => { onClick={() => { const rect = threadMenuRefs.current[comment.cid].getBoundingClientRect(); const menu = document.querySelector(`.post-menu-thread-${comment.cid}`); - menu.style.top = `calc(${rect.top}px + 17px)`; + menu.style.top = `calc(${rect.top + window.scrollY}px + 17px)`; menu.style.left = `${rect.left}px`; setRotatedStates(prevState => ({ @@ -968,7 +968,7 @@ const Thread = () => { onClick={() => { const rect = replyMenuRefs.current[reply.cid].getBoundingClientRect(); const menu = document.querySelector(`.post-menu-reply-${reply.cid}`); - menu.style.top = `calc(${rect.top}px + 17px)`; + menu.style.top = `calc(${rect.top + window.scrollY}px + 17px)`; menu.style.left = `${rect.left}px`; setRotatedStates(prevState => ({ From cfa1489b16453a6f60af794b556f2de66d686495 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 25 May 2023 13:42:58 +0200 Subject: [PATCH 02/16] add post menu UI, mod logic, styling to catalog --- src/components/styled/GlobalStyle.styled.jsx | 40 +++ .../styled/views/Catalog.styled.jsx | 167 +++++++++ src/components/views/Catalog.jsx | 330 +++++++++++++++--- 3 files changed, 492 insertions(+), 45 deletions(-) diff --git a/src/components/styled/GlobalStyle.styled.jsx b/src/components/styled/GlobalStyle.styled.jsx index aed763c3..adc961e4 100644 --- a/src/components/styled/GlobalStyle.styled.jsx +++ b/src/components/styled/GlobalStyle.styled.jsx @@ -54,4 +54,44 @@ export const GlobalStyle = createGlobalStyle` max-height: 100% !important; } } + + .post-menu-catalog { + position: absolute; + font-size: 12px; + line-height: 1.3em; + list-style: none; + + ul { + padding: 0; + margin: 0; + white-space: nowrap; + } + + li { + cursor: pointer; + position: relative; + padding: 2px 4px; + } + + .dropdown { + position: relative; + display: inline-block; + } + + .dropdown-menu { + display: none; + position: absolute; + left: 100%; + top: 0; + } + + .dropdown:hover .dropdown-menu { + display: block; + } + + a { + text-decoration: none; + color: inherit; + } + } `; \ No newline at end of file diff --git a/src/components/styled/views/Catalog.styled.jsx b/src/components/styled/views/Catalog.styled.jsx index d1b58403..786a59da 100644 --- a/src/components/styled/views/Catalog.styled.jsx +++ b/src/components/styled/views/Catalog.styled.jsx @@ -77,4 +77,171 @@ export const Threads = styled.div` display: block; padding: 0 15px; } +`; + +export const PostMenuCatalog = styled.div` + .post-menu-catalog { + position: relative; + font-size: 12px; + padding: 0; + + li { + cursor: pointer; + position: relative; + padding: 2px 4px; + } + + .dropdown { + position: relative; + display: inline-block; + } + + .dropdown-menu { + display: none; + position: absolute; + } + + .dropdown:hover .dropdown-menu { + display: block; + } + + a { + text-decoration: none; + color: inherit; + } + } + + ${({ selectedStyle }) => { + switch (selectedStyle) { + case 'Yotsuba': + return `.highlighted { + background-color: #f0c0b0 !important; + border: 1px solid #d99f91 !important; + border-left: none !important; + border-top: none !important; + } + + .post-menu-catalog { + border-right: 1px solid #d9bfb7; + border-bottom: 2px solid #d9bfb7; + + li { + border: 1px solid #d9bfb7; + border-bottom: none; + background-color: #f0e0d6; + + :hover { + background-color: #ffe; + } + } + }`; + + case 'Yotsuba-B': + return `.highlighted { + background-color: #d6bad0 !important; + border: 1px solid #ba9dbf !important; + border-left: none !important; + border-top: none !important; + } + + .post-menu-catalog { + border-right: 1px solid #b7c5d9; + border-bottom: 2px solid #b7c5d9; + + li { + border: 1px solid #b7c5d9; + border-bottom: none; + background-color: #d6daf0; + + :hover { + background-color: #eef2ff; + } + } + }`; + + case 'Futaba': + return `.highlighted { + background-color: #f0c0b0 !important; + } + + .post-menu-catalog { + font-size: 13px !important; + border: 1px solid #d9bfb7; + border-bottom: none; + + li { + background-color: #f0e0d6; + border-bottom: 1px solid #d9bfb7; + + :hover { + background-color: #ffe; + } + } + }`; + + case 'Burichan': + return `.highlighted { + background-color: #d6bad0 !important; + } + + .post-menu-catalog { + font-size: 13px !important; + border: 1px solid #b7c5d9; + border-bottom: none; + + li { + background-color: #d6daf0; + border-bottom: 1px solid #b7c5d9; + + :hover { + background-color: #eef2ff; + } + } + }`; + + case 'Tomorrow': + return `.highlighted { + background-color: #1d1d21 !important; + border: 1px solid #111 !important; + } + + .post-menu-catalog { + border: 1px solid #000; + border-bottom: none; + + li { + background-color: #282a2e; + border-bottom: 1px solid #000; + + :hover { + background-color: #1d1f21; + } + } + }`; + + case 'Photon': + return `.highlighted { + background-color: #ccc !important; + border: 1px solid #ccc !important; + } + + .post-menu-catalog { + border: 1px solid #ccc; + border-bottom: none; + + li { + background-color: #ddd; + border-bottom: 1px solid #ccc; + + :hover { + background-color: #eee; + } + } + }`; + + + default: + return ''; + } + }} `; \ No newline at end of file diff --git a/src/components/views/Catalog.jsx b/src/components/views/Catalog.jsx index ea854e12..7a9067c0 100644 --- a/src/components/views/Catalog.jsx +++ b/src/components/views/Catalog.jsx @@ -1,16 +1,20 @@ -import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import InfiniteScroll from 'react-infinite-scroller'; import { Link, useNavigate, useParams } from 'react-router-dom'; +import { confirmAlert } from 'react-confirm-alert'; import { Tooltip } from 'react-tooltip'; -import { useFeed, usePublishComment, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks'; +import { useAccount, useFeed, usePublishComment, usePublishCommentEdit, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; -import { Container, NavBar, Header, Break, PostForm, PostFormLink, PostFormTable } 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, PostForm, PostFormLink, PostFormTable, 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'; @@ -28,8 +32,11 @@ const Catalog = () => { captchaResponse, setCaptchaResponse, setChallengesArray, defaultSubplebbits, + editedComment, setIsCaptchaOpen, + isModerationOpen, setIsModerationOpen, isSettingsOpen, setIsSettingsOpen, + setModeratingCommentCid, setPendingComment, setPendingCommentIndex, setResolveCaptchaPromise, @@ -45,25 +52,56 @@ const Catalog = () => { const subjectRef = useRef(); const commentRef = useRef(); const linkRef = useRef(); + const threadMenuRefs = useRef({}); const navigate = useNavigate(); const [errorMessage, setErrorMessage] = useState(null); - const [successMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); useError(errorMessage, [errorMessage]); useSuccess(successMessage, [successMessage]); const [triggerPublishComment, setTriggerPublishComment] = useState(false); const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); + const [isHoveringOnThread, setIsHoveringOnThread] = useState(false); + 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 [menuPosition, setMenuPosition] = useState({top: 0, left: 0}); + const [openMenuCid, setOpenMenuCid] = useState(null); + const { feed, hasMore, loadMore } = useFeed({subplebbitAddresses: [`${selectedAddress}`], sortType: 'new'}); const { subplebbitAddress } = useParams(); const subplebbit = useSubplebbit({subplebbitAddress: selectedAddress}); + const account = useAccount(); const stateString = useStateString(subplebbit); + 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]); + + + const handleOptionClick = () => { + setOpenMenuCid(null); + }; + + useEffect(() => { setSelectedAddress(subplebbitAddress); }, [subplebbitAddress, setSelectedAddress]); @@ -85,6 +123,8 @@ const Catalog = () => { setErrorMessage(errorString); } }, [errorString]); + + const { subscribed, subscribe, unsubscribe } = useSubscribe({subplebbitAddress: selectedAddress}); // temporary title from JSON, gets subplebbitAddress from URL @@ -119,8 +159,12 @@ const Catalog = () => { const onChallengeVerification = (challengeVerification) => { if (challengeVerification.challengeSuccess === true) { - navigate(`/p/${selectedAddress}/c/${challengeVerification.publication?.cid}`); - console.log('challenge success'); + if (challengeVerification.publication?.cid !== undefined) { + navigate(`/p/${subplebbitAddress}/c/${challengeVerification.publication?.cid}`); + console.log('challenge success'); + } else { + setSuccessMessage('Challenge Success'); + } } else if (challengeVerification.challengeSuccess === false) { setErrorMessage('Challenge Failed', {reason: challengeVerification.reason, errors: challengeVerification.errors}); @@ -254,6 +298,94 @@ const Catalog = () => { }); }; + + const [publishCommentEditOptions, setPublishCommentEditOptions] = useState({ + commentCid: commentCid, + content: editedComment || undefined, + subplebbitAddress: selectedAddress || subplebbitAddress, + onChallenge, + onChallengeVerification, + onError: (error) => { + setErrorMessage(error); + }, + }); + + + const {error, publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions); + + useEffect(() => { + if (error) { + setErrorMessage(error); + } + }, [error]); + + + const handleAuthorDeleteClick = (commentCid) => { + handleOptionClick(commentCid); + + confirmAlert({ + customUI: ({ onClose }) => { + return ( + +
+

Are you sure you want to delete this post?

+
+ + +
+
+
+ ); + } + }); + }; + + + const handleAuthorEditClick = (comment) => { + handleOptionClick(comment.cid); + 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]); + + + 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; @@ -295,6 +427,16 @@ const Catalog = () => { selectedStyle={selectedStyle} isOpen={isSettingsOpen} closeModal={() => setIsSettingsOpen(false)} /> + setIsModerationOpen(false)} + deletePost={deletePost} /> + setIsEditModalOpen(false)} + originalCommentContent={originalCommentContent} /> <> @@ -484,45 +626,44 @@ const Catalog = () => { 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" ? ( +
{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" ? (
- )})} ) : () From ada945054d8ca92e7ac904a32115912eafc42a81 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 25 May 2023 15:40:16 +0200 Subject: [PATCH 03/16] implement createPortal in board view for post menu --- src/components/views/Board.jsx | 304 +++++++++++++++++---------------- 1 file changed, 154 insertions(+), 150 deletions(-) diff --git a/src/components/views/Board.jsx b/src/components/views/Board.jsx index 76227bf6..bf6fb50e 100644 --- a/src/components/views/Board.jsx +++ b/src/components/views/Board.jsx @@ -1,4 +1,5 @@ import React, { Fragment, useCallback, 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'; @@ -10,6 +11,7 @@ import { debounce } from 'lodash'; import useGeneralStore from '../../hooks/stores/useGeneralStore'; import { Container, NavBar, Header, Break, PostFormLink, PostFormTable, PostForm, TopBar, BoardForm, PostMenu } from '../styled/views/Board.styled'; import { Footer, AuthorDeleteAlert } from '../styled/views/Thread.styled'; +import { PostMenuCatalog } from '../styled/views/Catalog.styled'; import EditModal from '../modals/EditModal'; import ImageBanner from '../ImageBanner'; import ModerationModal from '../modals/ModerationModal'; @@ -81,10 +83,11 @@ const Board = () => { const [triggerPublishCommentEdit, setTriggerPublishCommentEdit] = useState(false); const [selectedFeed, setSelectedFeed] = useState(feed); const [deletePost, setDeletePost] = useState(false); - const [rotatedStates, setRotatedStates] = useState({}); const [isImageSearchOpen, setIsImageSearchOpen] = useState(false); const [isModerator, setIsModerator] = useState(false); const [commentCid, setCommentCid] = useState(null); + const [menuPosition, setMenuPosition] = useState({top: 0, left: 0}); + const [openMenuCid, setOpenMenuCid] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null); @@ -105,11 +108,8 @@ const Board = () => { }, [account?.author.address, subplebbit.roles]); - const handleOptionClick = (threadCid) => { - setRotatedStates(prevState => ({ - ...prevState, - [threadCid]: false - })); + const handleOptionClick = () => { + setOpenMenuCid(null); }; @@ -831,89 +831,91 @@ const Board = () => { title="Post menu" ref={el => threadMenuRefs.current[thread.cid] = el} className='post-menu-button' - rotated={rotatedStates[thread.cid]} + rotated={openMenuCid === thread.cid} onClick={() => { const rect = threadMenuRefs.current[thread.cid].getBoundingClientRect(); - const menu = document.querySelector(`.post-menu-thread-${thread.cid}`); - menu.style.top = `calc(${rect.top + window.scrollY}px - 258px)`; - menu.style.left = `${rect.left}px`; - - setRotatedStates(prevState => ({ - ...prevState, - [thread.cid]: !prevState[thread.cid] - })); + setMenuPosition({top: rect.top + window.scrollY, left: rect.left}); + setOpenMenuCid(prevCid => (prevCid === thread.cid ? null : thread.cid)); }} > ▶ -
-
    -
  • handleOptionClick(thread.cid)}>Hide thread
  • - {thread.author.shortAddress === account?.author.shortAddress ? ( - <> -
  • handleAuthorEditClick(thread)}>Edit post
  • -
  • handleAuthorDeleteClick(thread.cid)}>Delete post
  • - - ) : null} - {isModerator ? ( - <> - {thread.author.shortAddress === account?.author.shortAddress ? ( - null - ) : ( -
  • { + {createPortal( + +
    +
      +
    • handleOptionClick(thread.cid)}>Hide thread
    • + {thread.author.shortAddress === account?.author.shortAddress ? ( + <> +
    • handleAuthorEditClick(thread)}>Edit post
    • +
    • handleAuthorDeleteClick(thread.cid)}>Delete post
    • + + ) : null} + {isModerator ? ( + <> + {thread.author.shortAddress === account?.author.shortAddress ? ( + null + ) : ( +
    • { + setModeratingCommentCid(thread.cid) + setIsModerationOpen(true); + handleOptionClick(thread.cid); + setDeletePost(true); + }}> + Delete post +
    • + )} +
    • { setModeratingCommentCid(thread.cid) setIsModerationOpen(true); handleOptionClick(thread.cid); - setDeletePost(true); }}> - Delete post + Mod tools
    • - )} -
    • { - setModeratingCommentCid(thread.cid) - setIsModerationOpen(true); - handleOptionClick(thread.cid); - }}> - Mod tools -
    • - - ) : null} - {(commentMediaInfo && ( - commentMediaInfo.type === 'image' || - (commentMediaInfo.type === 'webpage' && - commentMediaInfo.thumbnail))) ? ( -
    • {setIsImageSearchOpen(true)}} - onMouseLeave={() => {setIsImageSearchOpen(false)}}> - Image search » -
        -
      • handleOptionClick(thread.cid)}> - Google -
      • -
      • handleOptionClick(thread.cid)}> - Yandex -
      • -
      • handleOptionClick(thread.cid)}> - SauceNAO -
      • -
      -
    • - ) : null - } -
    -
    + + ) : null} + {(commentMediaInfo && ( + commentMediaInfo.type === 'image' || + (commentMediaInfo.type === 'webpage' && + commentMediaInfo.thumbnail))) ? ( +
  • {setIsImageSearchOpen(true)}} + onMouseLeave={() => {setIsImageSearchOpen(false)}}> + Image search » +
      +
    • handleOptionClick(thread.cid)}> + Google +
    • +
    • handleOptionClick(thread.cid)}> + Yandex +
    • +
    • handleOptionClick(thread.cid)}> + SauceNAO +
    • +
    +
  • + ) : null + } +
+
+ , document.body + )}