From 88dcfcaca8cdfb38d3bc3f25863451abefb0d1d2 Mon Sep 17 00:00:00 2001 From: "Tom (plebeius.eth)" Date: Sat, 8 Jun 2024 11:27:29 +0200 Subject: [PATCH] feat(catalog): add catalog post previews --- .../catalog-row/catalog-row.module.css | 37 +++- src/components/catalog-row/catalog-row.tsx | 166 ++++++++++++++---- .../post/post-desktop/post-desktop.tsx | 4 +- .../post-menu-mobile/post-menu-mobile.tsx | 2 +- .../reply-quote-preview.tsx | 3 +- ...privileges.ts => use-author-privileges.ts} | 8 +- src/index.css | 4 + src/lib/utils/time-utils.ts | 35 ++++ src/themes.css | 34 +++- 9 files changed, 249 insertions(+), 44 deletions(-) rename src/hooks/{use-edit-comment-privileges.ts => use-author-privileges.ts} (78%) diff --git a/src/components/catalog-row/catalog-row.module.css b/src/components/catalog-row/catalog-row.module.css index e97bb7c5..66259f7e 100644 --- a/src/components/catalog-row/catalog-row.module.css +++ b/src/components/catalog-row/catalog-row.module.css @@ -83,8 +83,8 @@ visibility: hidden; } -.post:hover .postMenu { - visibility: visible; +.postMenuVisible { + visibility: visible !important; } .loadingSkeleton { @@ -95,4 +95,37 @@ .fileDeleted { image-rendering: pixelated; +} + +.postPreview { + background-color: var(--catalog-post-preview-background-color); + border-radius: 3px; + padding: 5px 8px 4px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: var(--catalog-post-preview-color); + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.postPreview .postSubject { + font-weight: 700; + color: var(--catalog-post-preview-subject-color); +} + +.postPreview .postAuthor { + color: var(--catalog-post-preview-author-color); + font-weight: 700; +} + +.postPreview .postLast { + color: var(--catalog-post-preview-last-color); + font-size: 90%; + margin-top: 3px; +} + +.postPreview .capcode { + color: var(--catalog-post-preview-author-capcode-color); } \ No newline at end of file diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index 85c92292..70b04ccb 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -1,14 +1,20 @@ -import { Link, useLocation } from 'react-router-dom'; +import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Comment } from '@plebbit/plebbit-react-hooks'; -import styles from './catalog-row.module.css'; +import { Link, useLocation } from 'react-router-dom'; +import { Comment, useComment } from '@plebbit/plebbit-react-hooks'; +import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; +import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isAllView } from '../../lib/utils/view-utils'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; +import useEditCommentPrivileges from '../../hooks/use-author-privileges'; import PostMenuDesktop from '../post/post-desktop/post-menu-desktop/'; +import styles from './catalog-row.module.css'; +import _ from 'lodash'; import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; interface CatalogPostMediaProps { commentMediaInfo: any; @@ -77,7 +83,8 @@ export const CatalogPostMedia = ({ commentMediaInfo, isOutOfFeed, linkWidth, lin const CatalogPost = ({ post }: { post: Comment }) => { const { t } = useTranslation(); - const { cid, content, isDescription, isRules, link, linkHeight, linkWidth, locked, pinned, replyCount, subplebbitAddress, title } = post || {}; + const { author, cid, content, isDescription, isRules, lastChildCid, link, linkHeight, linkWidth, locked, pinned, replyCount, subplebbitAddress, timestamp, title } = + post || {}; const commentMediaInfo = getCommentMediaInfo(post); const hasThumbnail = getHasThumbnail(commentMediaInfo, link); @@ -95,37 +102,132 @@ const CatalogPost = ({ post }: { post: Comment }) => { ); + const [hoveredCid, setHoveredCid] = useState(null); + const [showPortal, setShowPortal] = useState(false); + const placementRef = useRef('right-start'); + const availableWidthRef = useRef(0); + const timeoutRef = useRef(null); + + const { refs, floatingStyles, update } = useFloating({ + placement: placementRef.current, + middleware: [ + shift({ padding: 10 }), + offset({ mainAxis: -7 }), + size({ + apply({ availableWidth, elements }) { + availableWidthRef.current = availableWidth; + if (availableWidth >= 250) { + elements.floating.style.maxWidth = `${availableWidth - 12}px`; + } else if (placementRef.current === 'right-start') { + placementRef.current = 'left-start'; + } + }, + }), + ], + whileElementsMounted: autoUpdate, + }); + + useEffect(() => { + const handleResize = () => { + const availableWidth = availableWidthRef.current; + if (availableWidth >= 250) { + placementRef.current = 'right-start'; + } else { + placementRef.current = 'left-start'; + } + update(); + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [update]); + + const handleMouseOver = () => { + setHoveredCid(cid); + timeoutRef.current = setTimeout(() => setShowPortal(true), 250); + }; + + const handleMouseLeave = () => { + setHoveredCid(null); + setShowPortal(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + const lastReply = useComment({ commentCid: lastChildCid }); + + const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({ + commentAuthorAddress: author?.address, + subplebbitAddress, + }); + const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({ + commentAuthorAddress: lastReply?.author?.address, + subplebbitAddress, + }); + return ( -
- {hasThumbnail ? ( - -
- {threadIcons} - + <> +
+
+ {hasThumbnail ? ( + +
+ {threadIcons} + +
+ + ) : ( + threadIcons + )} +
+ R: {replyCount || '0'} + {linkCount > 0 && ( + + {' '} + / L: {linkCount} + + )} + + +
- - ) : ( - threadIcons - )} -
- R: {replyCount || '0'} - {linkCount > 0 && ( - - {' '} - / L: {linkCount} - - )} - - - -
- -
- {title && `${title}${content ? ': ' : ''}`} - {content} + +
+ {title && `${title}${content ? ': ' : ''}`} + {content} +
+
- -
+
+ {hoveredCid === cid && + showPortal && + createPortal( +
+ {title} + {' by '} + + {author?.displayName || _.capitalize(t('anonymous'))} + {isCatalogPostAuthorMod && {` ## Board ${catalogPostAuthorRole}`}} + + {getFormattedTimeAgo(timestamp)} + {replyCount > 0 && ( +
+ Last reply by{' '} + + {lastReply?.author?.displayName || _.capitalize(t('anonymous'))} + {isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`} + + {getFormattedTimeAgo(lastReply?.timestamp)} +
+ )} +
, + document.body, + )} + ); }; diff --git a/src/components/post/post-desktop/post-desktop.tsx b/src/components/post/post-desktop/post-desktop.tsx index 2a2d507f..4bc805bd 100644 --- a/src/components/post/post-desktop/post-desktop.tsx +++ b/src/components/post/post-desktop/post-desktop.tsx @@ -9,7 +9,7 @@ import { getFormattedDate } from '../../../lib/utils/time-utils'; import { isValidURL } from '../../../lib/utils/url-utils'; import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils'; import useCountLinksInReplies from '../../../hooks/use-count-links-in-replies'; -import useEditCommentPrivileges from '../../../hooks/use-edit-comment-privileges'; +import useEditCommentPrivileges from '../../../hooks/use-author-privileges'; import useReplies from '../../../hooks/use-replies'; import useStateString from '../../../hooks/use-state-string'; import CommentMedia from '../../comment-media'; @@ -248,7 +248,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
)}
- {!isInPostPageView && !isDescription && !isRules && ( + {!isInPostPageView && !isDescription && !isRules && showReplies && ( diff --git a/src/components/post/post-mobile/post-menu-mobile/post-menu-mobile.tsx b/src/components/post/post-mobile/post-menu-mobile/post-menu-mobile.tsx index 18d3aef6..75bd5c94 100644 --- a/src/components/post/post-mobile/post-menu-mobile/post-menu-mobile.tsx +++ b/src/components/post/post-mobile/post-menu-mobile/post-menu-mobile.tsx @@ -6,7 +6,7 @@ import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDis import styles from './post-menu-mobile.module.css'; import { getCommentMediaInfo } from '../../../../lib/utils/media-utils'; import { copyShareLinkToClipboard, isValidURL } from '../../../../lib/utils/url-utils'; -import useEditCommentPrivileges from '../../../../hooks/use-edit-comment-privileges'; +import useEditCommentPrivileges from '../../../../hooks/use-author-privileges'; import EditMenu from '../../edit-menu/edit-menu'; interface PostMenuMobileProps { diff --git a/src/components/post/reply-quote-preview/reply-quote-preview.tsx b/src/components/post/reply-quote-preview/reply-quote-preview.tsx index a054fe15..0e39e5b2 100644 --- a/src/components/post/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/post/reply-quote-preview/reply-quote-preview.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Link } from 'react-router-dom'; import { Comment } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react'; import useIsMobile from '../../../hooks/use-is-mobile'; import styles from '../post.module.css'; @@ -129,7 +128,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i onMouseOver={() => handleMouseOver(quotelinkReply?.cid)} onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)} > - {`c/${quotelinkReply?.cid && Plebbit.getShortCid(quotelinkReply.cid)}`} + {`c/${quotelinkReply?.shortCid}`}
{hoveredCid === quotelinkReply?.cid && diff --git a/src/hooks/use-edit-comment-privileges.ts b/src/hooks/use-author-privileges.ts similarity index 78% rename from src/hooks/use-edit-comment-privileges.ts rename to src/hooks/use-author-privileges.ts index 92b43b8c..09b40d08 100644 --- a/src/hooks/use-edit-comment-privileges.ts +++ b/src/hooks/use-author-privileges.ts @@ -1,11 +1,11 @@ import { useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks'; -interface EditCommentPrivileges { +interface AuthorPrivilegesProps { commentAuthorAddress: string; subplebbitAddress: string; } -const useEditCommentPrivileges = ({ commentAuthorAddress, subplebbitAddress }: EditCommentPrivileges) => { +const useAuthorPrivileges = ({ commentAuthorAddress, subplebbitAddress }: AuthorPrivilegesProps) => { const accountAuthorAddress = useAccount()?.author?.address; const { roles } = useSubplebbit({ subplebbitAddress }) || {}; @@ -15,7 +15,7 @@ const useEditCommentPrivileges = ({ commentAuthorAddress, subplebbitAddress }: E const isAccountMod = accountAuthorRole === 'admin' || accountAuthorRole === 'owner' || accountAuthorRole === 'moderator'; const isAccountCommentAuthor = accountAuthorAddress === commentAuthorAddress; - return { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor }; + return { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor, commentAuthorRole, accountAuthorRole }; }; -export default useEditCommentPrivileges; +export default useAuthorPrivileges; diff --git a/src/index.css b/src/index.css index c8b6c706..2ed703e2 100644 --- a/src/index.css +++ b/src/index.css @@ -30,6 +30,10 @@ hr { background: var(--reply-highlight-background-color) !important; } +.capitalize { + text-transform: capitalize; +} + @media (max-width: 640px) { .button { font-size: var(--button-font-size-mobile); diff --git a/src/lib/utils/time-utils.ts b/src/lib/utils/time-utils.ts index 05ca793b..5c332bd8 100644 --- a/src/lib/utils/time-utils.ts +++ b/src/lib/utils/time-utils.ts @@ -33,3 +33,38 @@ export const getFormattedDate = (commentTimestamp: number) => { } return string; }; + +export const getFormattedTimeAgo = (unixTimestamp: number): string => { + const currentTime = Date.now() / 1000; + const timeDifference = currentTime - unixTimestamp; + const t = i18next.t; + + if (timeDifference < 120) { + return t('time_1_minute_ago'); + } + if (timeDifference < 3600) { + return t('time_x_minutes_ago', { count: Math.floor(timeDifference / 60) }); + } + if (timeDifference < 7200) { + return t('time_1_hour_ago'); + } + if (timeDifference < 86400) { + return t('time_x_hours_ago', { count: Math.floor(timeDifference / 3600) }); + } + if (timeDifference < 172800) { + return t('time_1_day_ago'); + } + if (timeDifference < 2592000) { + return t('time_x_days_ago', { count: Math.floor(timeDifference / 86400) }); + } + if (timeDifference < 5184000) { + return t('time_1_month_ago'); + } + if (timeDifference < 31104000) { + return t('time_x_months_ago', { count: Math.floor(timeDifference / 2592000) }); + } + if (timeDifference < 62208000) { + return t('time_1_year_ago'); + } + return t('time_x_years_ago', { count: Math.floor(timeDifference / 31104000) }); +}; diff --git a/src/themes.css b/src/themes.css index 83197713..54c95b97 100644 --- a/src/themes.css +++ b/src/themes.css @@ -25,6 +25,12 @@ --catalog-post-menu-desktop-btn-hover-color: red; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-subject-color: #cc1105; + --catalog-post-preview-author-color: #00a550; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #f0e0d6; @@ -226,6 +232,11 @@ --catalog-post-menu-desktop-btn-hover-color: #d00; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-author-color: #00a550; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #d6daf0; @@ -421,6 +432,12 @@ --catalog-post-menu-desktop-btn-hover-color: red; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-subject-color: #cc1105; + --catalog-post-preview-author-color: #00a550; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #f0e0d6; @@ -590,6 +607,11 @@ --catalog-post-menu-desktop-btn-hover-color: red; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-author-color: #00a550; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #d6daf0; @@ -766,12 +788,18 @@ --catalog-post-menu-desktop-btn-hover-color: #5f89ac; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-subject-color: #b294bb; + --catalog-post-preview-author-color: #00a550; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #282a2e; --challenge-modal-border: 1px solid #111; --challenge-modal-title-background-color: #282a2e; - --challenge-modal-title-text-color: #c5c8c6; + --challenge-modal-title-text-color: #b0cab9; --challenge-modal-title-border: unset; --challenge-modal-title-font-size: 10pt; --challenge-modal-title-font-weight: 700; @@ -969,6 +997,10 @@ --catalog-post-menu-desktop-btn-hover-color: #f30; --catalog-post-menu-desktop-btn-opacity: 0.5; --catalog-post-menu-desktop-btn-hover-opacity: 1; + --catalog-post-preview-background-color: #181f24; + --catalog-post-preview-color: #dedede; + --catalog-post-preview-last-color: #bbbfbd; + --catalog-post-preview-author-capcode-color: purple; /* challenge modal */ --challenge-modal-background-color: #ddd;