From 0962c10685e914162796526419ab315fa439bd5e Mon Sep 17 00:00:00 2001 From: "plebeius.eth" Date: Sun, 28 Apr 2024 20:08:41 +0200 Subject: [PATCH] feat: add reply modal --- .../challenge-modal.module.css | 1 - .../challenge-modal/challenge-modal.tsx | 3 +- src/components/post/post.tsx | 101 +++++++++++++----- src/components/reply-modal/index.ts | 1 + .../reply-modal/reply-modal.module.css | 60 +++++++++++ src/components/reply-modal/reply-modal.tsx | 80 ++++++++++++++ 6 files changed, 220 insertions(+), 26 deletions(-) create mode 100644 src/components/reply-modal/index.ts create mode 100644 src/components/reply-modal/reply-modal.module.css create mode 100644 src/components/reply-modal/reply-modal.tsx diff --git a/src/components/challenge-modal/challenge-modal.module.css b/src/components/challenge-modal/challenge-modal.module.css index e46f9186..64a25b19 100644 --- a/src/components/challenge-modal/challenge-modal.module.css +++ b/src/components/challenge-modal/challenge-modal.module.css @@ -74,7 +74,6 @@ } .challengeFooter button { - margin: 5px 5px 0 0; cursor: pointer; filter: var(--filter80); text-transform: capitalize; diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index d9b8c1d0..ea8c2f33 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -5,6 +5,7 @@ import { Challenge as ChallengeType } from '@plebbit/plebbit-react-hooks'; import { getPublicationType } from '../../lib/utils/challenge-utils'; import useChallenges from '../../hooks/use-challenges'; import styles from './challenge-modal.module.css'; +import _ from 'lodash'; interface ChallengeProps { challenge: ChallengeType; @@ -62,7 +63,7 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
- +
{title && (
diff --git a/src/components/post/post.tsx b/src/components/post/post.tsx index 09a34523..5cf7dc01 100644 --- a/src/components/post/post.tsx +++ b/src/components/post/post.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Role, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { Role, useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; import { getFormattedDate } from '../../lib/utils/time-utils'; @@ -11,10 +11,13 @@ import useReplies from '../../hooks/use-replies'; import useStateString from '../../hooks/use-state-string'; import useWindowWidth from '../../hooks/use-window-width'; import styles from './post.module.css'; -import LoadingEllipsis from '../loading-ellipsis'; -import Markdown from '../markdown'; import CommentMedia from '../comment-media'; import { canEmbed } from '../embed'; +import LoadingEllipsis from '../loading-ellipsis'; +import Markdown from '../markdown'; +import ReplyModal from '../reply-modal'; +import _ from 'lodash'; +import { getDisplayMediaInfoType } from '../../lib/utils/media-utils'; interface PostProps { index?: number; @@ -22,9 +25,10 @@ interface PostProps { reply?: any; roles?: Role[]; showAllReplies?: boolean; + openReplyModal?: (cid: string) => void; } -const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => { +const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => { const { t } = useTranslation(); const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, postCid, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {}; const { address, displayName, shortAddress } = author || {}; @@ -53,6 +57,10 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => { const [menuBtnRotated, setMenuBtnRotated] = useState(false); + // pending reply by account is not yet published + const account = useAccount(); + const accountShortAddress = account?.author?.shortAddress; + return (
@@ -115,7 +123,7 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => { {displayName || _.capitalize(t('anonymous'))} {authorRole && ` ## Board ${authorRole}`}{' '} - {!(isDescription || isRules) && (u/{shortAddress}) } + {!(isDescription || isRules) && (u/{shortAddress || accountShortAddress}) } {getFormattedDate(timestamp)} @@ -124,13 +132,13 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => { {!(isDescription || isRules) && ( - !cid && e.preventDefault()}> + !cid && e.preventDefault()}> c/ {!cid ? ( {state === 'failed' ? 'Failed' : 'Pending'} ) : ( - + openReplyModal && openReplyModal(cid)}> {shortCid} )} @@ -187,15 +195,15 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => { {!(pinned && !isInPostPage) && replies && (showAllReplies ? replies : replies.slice(-5)).map((reply, index) => ( -
- +
+
))}
); }; -const ReplyDesktop = ({ reply, roles }: PostProps) => { +const ReplyDesktop = ({ reply, roles, openReplyModal }: PostProps) => { const { t } = useTranslation(); const { author, cid, content, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, state, subplebbitAddress, timestamp } = reply || {}; const { address, displayName, shortAddress } = author || {}; @@ -216,6 +224,10 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
{stateString !== 'Failed' ? : stateString}
); + // pending reply by account is not yet published + const account = useAccount(); + const accountShortAddress = account?.author?.shortAddress; + return (
{'>>'}
@@ -229,18 +241,18 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => { {displayName || _.capitalize(t('anonymous'))} {authorRole && ` ## Board ${authorRole}`}{' '} - (u/{shortAddress}) + (u/{shortAddress || accountShortAddress}) {getFormattedDate(timestamp)} - !cid && e.preventDefault()}> + !cid && e.preventDefault()}> c/ {!cid ? ( {state === 'failed' ? 'Failed' : 'Pending'} ) : ( - + openReplyModal && openReplyModal(cid)}> {shortCid} )} @@ -314,7 +326,7 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => { )} - {!cid && ( + {!cid && state === 'pending' && ( <>
{loadingString} @@ -327,7 +339,7 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => { ); }; -const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { +const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => { const { t } = useTranslation(); const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {}; const { address, displayName, shortAddress } = author || {}; @@ -349,6 +361,10 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { const replies = useReplies(post); + // pending reply by account is not yet published + const account = useAccount(); + const accountShortAddress = account?.author?.shortAddress; + return (
@@ -366,7 +382,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { {displayName || _.capitalize(t('anonymous'))} {authorRole && ` ## Board ${authorRole}`}{' '} - {!(isDescription || isRules) && (u/{shortAddress || address?.slice(0, 12) + '...'})} + {!(isDescription || isRules) && (u/{shortAddress || accountShortAddress})} {pinned && ( @@ -394,7 +410,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { {!cid ? ( {state === 'failed' ? 'Failed' : 'Pending'} ) : ( - + openReplyModal && openReplyModal(cid)}> {shortCid} )} @@ -441,7 +457,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { replies && (showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
- +
))}
@@ -449,7 +465,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => { ); }; -const ReplyMobile = ({ reply, roles }: PostProps) => { +const ReplyMobile = ({ reply, roles, openReplyModal }: PostProps) => { const { t } = useTranslation(); const { author, content, cid, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, state, subplebbitAddress, timestamp } = reply || {}; const { address, displayName, shortAddress } = author || {}; @@ -461,6 +477,15 @@ const ReplyMobile = ({ reply, roles }: PostProps) => { const isReplyingToReply = postCid !== parentCid; + // pending reply by account is not yet published + const account = useAccount(); + const accountShortAddress = account?.author?.shortAddress; + + const stateString = useStateString(reply); + const loadingString = stateString && ( +
{stateString !== 'Failed' ? : stateString}
+ ); + return (
@@ -472,7 +497,7 @@ const ReplyMobile = ({ reply, roles }: PostProps) => { {displayName || _.capitalize(t('anonymous'))} {authorRole && ` ## Board ${authorRole}`}{' '} - (u/{shortAddress}) + (u/{shortAddress || accountShortAddress}) {pinned && ( @@ -489,7 +514,7 @@ const ReplyMobile = ({ reply, roles }: PostProps) => { {!cid ? ( {state === 'failed' ? 'Failed' : 'Pending'} ) : ( - + openReplyModal && openReplyModal(cid)}> {shortCid} )} @@ -516,6 +541,12 @@ const ReplyMobile = ({ reply, roles }: PostProps) => { )} + {!cid && state === 'pending' && ( + <> +
+ {loadingString} + + )} )}
@@ -527,14 +558,36 @@ const ReplyMobile = ({ reply, roles }: PostProps) => { const Post = ({ post, showAllReplies = false }: PostProps) => { const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress }); const isMobile = useWindowWidth() < 640; + + const [showReplyModal, setShowReplyModal] = useState(false); + const [activeCid, setActiveCid] = useState(null); + + const openReplyModal = (cid: string) => { + if (activeCid && activeCid !== cid) { + closeModal(); + setActiveCid(cid); + setShowReplyModal(true); + } else if (!activeCid) { + setActiveCid(cid); + setShowReplyModal(true); + } else { + return; + } + }; + + const closeModal = () => { + setActiveCid(null); + setShowReplyModal(false); + }; + return (
- {showReplyModal && } + {showReplyModal && activeCid && }
{isMobile ? ( - + ) : ( - + )}
diff --git a/src/components/reply-modal/index.ts b/src/components/reply-modal/index.ts new file mode 100644 index 00000000..b6861856 --- /dev/null +++ b/src/components/reply-modal/index.ts @@ -0,0 +1 @@ +export { default } from './reply-modal'; diff --git a/src/components/reply-modal/reply-modal.module.css b/src/components/reply-modal/reply-modal.module.css new file mode 100644 index 00000000..5acd55f6 --- /dev/null +++ b/src/components/reply-modal/reply-modal.module.css @@ -0,0 +1,60 @@ +.container { + position: fixed; + top: calc(50% - 150px); + left: calc(50% - 150px); + display: block; + padding: 2px; + font-size: 10pt; + border-left: none; + border-top: none; + z-index: 1000; + background-color: var(--challenge-modal-background-color); + border: var(--challenge-modal-border); +} + +.title { + font-size: var(--challenge-modal-title-font-size); + text-align: center; + margin-bottom: 1px; + padding: 0; + height: 18px; + line-height: 18px; + cursor: move; + font-weight: var(--challenge-modal-title-font-weight); + background-color: var(--challenge-modal-title-background-color); + color: var(--challenge-modal-title-text-color); + border: var(--challenge-modal-title-border); +} + +.closeIcon { + all: unset; + float: right; + cursor: pointer; + margin-bottom: -4px; + width: 18px; + height: 18px; + border: none; + image-rendering: pixelated; + background-image: var(--close-button-background-image); +} + +.container input, .container textarea { + border: 1px solid #aaa; + font-family: Arial, Helvetica, sans-serif; + font-size: 10pt; + outline: medium none; + width: 298px; + filter: var(--filter80); + padding: 2px; + margin-bottom: 1px; +} + +.content textarea { + vertical-align: top; +} + +.footer button { + text-transform: capitalize; + float: right; + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx new file mode 100644 index 00000000..eda69f8d --- /dev/null +++ b/src/components/reply-modal/reply-modal.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import Draggable from 'react-draggable'; +import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; +import { useAccount } from '@plebbit/plebbit-react-hooks'; +import { isValidURL } from '../../lib/utils/url-utils'; +import useReply from '../../hooks/use-reply'; +import styles from './reply-modal.module.css'; +import _ from 'lodash'; + +interface ReplyModalProps { + closeModal: () => void; + parentCid: string; +} + +const ReplyModal = ({ closeModal, parentCid }: ReplyModalProps) => { + const { t } = useTranslation(); + const { subplebbitAddress } = useParams() as { subplebbitAddress: string }; + const { setContent, resetContent, replyIndex, publishReply } = useReply({ cid: parentCid, subplebbitAddress }); + + const account = useAccount(); + const { displayName } = account?.author || {}; + + const textRef = useRef(null); + const urlRef = useRef(null); + + const onPublishReply = () => { + const currentContent = textRef.current?.value || ''; + const currentUrl = urlRef.current?.value || ''; + + if (!currentContent.trim() && !currentUrl) { + alert(`Cannot post empty comment`); + return; + } + + if (currentUrl && !isValidURL(currentUrl)) { + alert('The provided link is not a valid URL.'); + return; + } + publishReply(); + }; + + useEffect(() => { + if (typeof replyIndex === 'number') { + closeModal(); + resetContent(); + } + }, [replyIndex, resetContent, closeModal]); + + // react-draggable requires a ref to the modal node + const nodeRef = useRef(null); + + return ( + +
+
+ Reply to c/{parentCid && Plebbit.getShortCid(parentCid)} +
+
+
+ +
+
+ setContent.link(e.target.value)} /> +
+
+