feat(reply): add floating quote preview

This commit is contained in:
Tom (plebeius.eth)
2024-06-05 22:18:45 +02:00
parent 1911b623d9
commit b44fb15078
7 changed files with 115 additions and 21 deletions
@@ -1,9 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Trans, useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';
import { Comment, useAccount, useBlock } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { useFloating, shift, size, autoUpdate, Placement } from '@floating-ui/react';
import styles from '../post.module.css';
import { getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail } from '../../../lib/utils/media-utils';
import { getFormattedDate } from '../../../lib/utils/time-utils';
@@ -19,9 +20,59 @@ import LoadingEllipsis from '../../loading-ellipsis';
import Markdown from '../../markdown';
import PostMenuDesktop from './post-menu-desktop/';
import EditMenu from '../edit-menu/edit-menu';
import { PostProps } from '../post';
import Post, { PostProps } from '../post';
import _ from 'lodash';
const ReplyLink = ({
reply,
subplebbitAddress,
hoveredCid,
setHoveredCid,
}: {
reply: Comment;
subplebbitAddress: string;
hoveredCid: string | null;
setHoveredCid: (cid: string | null) => void;
}) => {
const [placement, setPlacement] = useState<Placement>('right');
const { refs, floatingStyles, update } = useFloating({
placement,
middleware: [
shift({ padding: 10 }),
size({
apply({ availableWidth, elements }) {
if (availableWidth >= 250) {
elements.floating.style.maxWidth = `${availableWidth - 5}px`;
} else {
setPlacement('left');
}
},
}),
],
whileElementsMounted: autoUpdate,
});
useEffect(() => {
update();
}, [placement, update]);
return (
<span className={styles.backlink}>
<Link to={`/p/${subplebbitAddress}/c/${reply?.cid}`} ref={refs.setReference} onMouseOver={() => setHoveredCid(reply?.cid)} onMouseLeave={() => setHoveredCid(null)}>
c/{reply?.shortCid}
</Link>
{hoveredCid === reply?.cid &&
createPortal(
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
<Post post={reply} showReplies={false} />
</div>,
document.body,
)}
</span>
);
};
const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
const { t } = useTranslation();
const { author, cid, locked, pinned, parentCid, postCid, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
@@ -46,6 +97,8 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: address, subplebbitAddress });
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
return (
<div className={styles.postInfo}>
{!isHidden && <EditMenu commentCid={cid} isAccountCommentAuthor={isAccountCommentAuthor} isAccountMod={isAccountMod} isCommentAuthorMod={isCommentAuthorMod} />}
@@ -111,9 +164,7 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
replies.map(
(reply: Comment, index: number) =>
reply?.parentCid === cid && (
<span key={index} className={styles.backlink}>
<Link to={`/p/${subplebbitAddress}/c/${reply?.cid}`}>c/{reply?.shortCid}</Link>
</span>
<ReplyLink key={index} reply={reply} subplebbitAddress={subplebbitAddress} hoveredCid={hoveredCid} setHoveredCid={setHoveredCid} />
),
)}
</div>
@@ -224,7 +275,7 @@ const PostMessage = ({ post }: PostProps) => {
);
};
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
const { cid, content, link, pinned, replyCount, subplebbitAddress } = post || {};
const { isDescription, isRules } = post || {}; // custom properties, not from api
const params = useParams();
@@ -241,11 +292,10 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
const repliesCount = pinned ? replyCount : replyCount - 5;
const linksCount = pinned ? totallinksCount : totallinksCount - visiblelinksCount;
// scroll to reply if pathname is reply permalink (backlink)
const replyRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const replyIndex = replies.findIndex((reply) => location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`));
if (replyIndex !== -1 && replyRefs.current[replyIndex]) {
replyRefs.current[replyIndex]?.scrollIntoView();
}
@@ -253,9 +303,13 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
return (
<div className={styles.postDesktop}>
<div className={styles.hrWrapper}>
<hr />
</div>
{showAllReplies ? (
<div className={styles.hrWrapper}>
<hr />
</div>
) : (
<div className={styles.replyQuotePreviewSpacer} />
)}
<div className={isHidden ? styles.postDesktopBlocked : ''}>
{!isInPostPageView && !isDescription && !isRules && (
<span className={styles.hideButtonWrapper}>
@@ -289,6 +343,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
!isDescription &&
!isRules &&
replies &&
showReplies &&
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
return (
@@ -181,7 +181,7 @@ const PostMessageMobile = ({ post }: PostProps) => {
);
};
const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
const { t } = useTranslation();
const { cid, content, pinned, replyCount, subplebbitAddress } = post || {};
const { isDescription, isRules } = post || {}; // custom properties, not from api
@@ -234,6 +234,7 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
!isDescription &&
!isRules &&
replies &&
showReplies &&
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
return (
+13
View File
@@ -420,6 +420,19 @@
background: var(--reply-highlight-background-color) !important;
}
.replyQuotePreview {
background: var(--quote-preview-background);
border: var(--quote-preview-border);
border-right: var(--quote-preview-border-right);
border-bottom: var(--quote-preview-border-bottom);
padding-right: 7px;
padding-left: 2px;
}
.postDesktop .replyQuotePreviewSpacer {
padding: 1.5px 0;
}
@media (max-width: 640px) {
.postDesktop {
display: none;
+4 -3
View File
@@ -11,10 +11,11 @@ export interface PostProps {
reply?: any;
roles?: Role[];
showAllReplies?: boolean;
showReplies?: boolean;
openReplyModal?: (cid: string) => void;
}
const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
const Post = ({ post, showAllReplies = false, showReplies = true, openReplyModal }: PostProps) => {
const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress });
const isMobile = useIsMobile();
@@ -22,9 +23,9 @@ const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
<div className={styles.thread}>
<div className={styles.postContainer}>
{isMobile ? (
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
) : (
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
)}
</div>
</div>
+1 -1
View File
@@ -93,7 +93,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => {
<input
type='text'
defaultValue={displayName}
placeholder={displayName ? undefined : _.capitalize(t('anonymous'))}
placeholder={displayName ? undefined : _.capitalize(t('name'))}
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
/>
</div>
+1 -1
View File
@@ -6,7 +6,7 @@ interface EditCommentPrivileges {
}
const useEditCommentPrivileges = ({ commentAuthorAddress, subplebbitAddress }: EditCommentPrivileges) => {
const accountAuthorAddress = useAccount().author?.address;
const accountAuthorAddress = useAccount()?.author?.address;
const { roles } = useSubplebbit({ subplebbitAddress }) || {};
const commentAuthorRole = roles?.[commentAuthorAddress]?.role;
+27 -3
View File
@@ -151,7 +151,11 @@
--post-quotelink-text-color: navy;
--post-quotelink-text-color-hover: red;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #f0e0d6;
--quote-preview-border: 1px solid #d9bfb7;
--quote-preview-border-right: 2px solid #d9bfb7;
--quote-preview-border-bottom: 2px solid #d9bfb7;
/* reply desktop */
--side-arrows-color: #e0bfb7;
@@ -346,7 +350,11 @@
--post-quotelink-text-color: #d00;
--post-quotelink-text-color-hover: #d00;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #d6daf0;
--quote-preview-border: 1px solid #b7c5d9;
--quote-preview-border-right: 2px solid #b7c5d9;
--quote-preview-border-bottom: 2px solid #b7c5d9;
/* reply desktop */
--side-arrows-color: #b7c5d9;
@@ -513,6 +521,10 @@
--post-quotelink-text-color-hover: red;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #f0e0d6;
--quote-preview-border: 1px solid rgba(0,0,0,.2);
--quote-preview-border-right: 2px solid rgba(0,0,0,.2);
--quote-preview-border-bottom: 2px solid rgba(0,0,0,.2);
/* reply desktop */
--side-arrows-color: #e0bfb7;
@@ -687,7 +699,11 @@
--post-quotelink-text-color: #d00;
--post-quotelink-text-color-hover: #d00;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #d6daf0;
--quote-preview-border: 1px solid rgba(0,0,0,.2);
--quote-preview-border-right: 2px solid rgba(0,0,0,.2);
--quote-preview-border-bottom: 2px solid rgba(0,0,0,.2);
/* reply desktop */
--side-arrows-color: #b7c5d9;
@@ -874,6 +890,10 @@
--post-quotelink-text-color-hover: #81a2be;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #282a2e;
--quote-preview-border: 1px solid #333;
--quote-preview-border-right: 1px solid #333;
--quote-preview-border-bottom: 1px solid #333;
/* reply desktop */
--side-arrows-color: #c5c8c6;
@@ -1058,6 +1078,10 @@
--post-quotelink-text-color-hover: #f30;
--post-quotelink-text-decoration: underline;
--post-quotelink-text-decoration-hover: underline;
--quote-preview-background: #ddd;
--quote-preview-border: 1px solid #ccc;
--quote-preview-border-right: 1px solid #ccc;
--quote-preview-border-bottom: 1px solid #ccc;
/* reply desktop */
--side-arrows-color: #333;