perf(feed): optimize posts rendering via props refactoring, memoizations

This commit is contained in:
Tom (plebeius.eth)
2025-03-03 23:47:33 +01:00
parent 4bdcfbdd28
commit 797a1f23c6
14 changed files with 206 additions and 112 deletions
+2 -1
View File
@@ -115,10 +115,11 @@ const CatalogPost = ({ post }: { post: Comment }) => {
subplebbitAddress, subplebbitAddress,
timestamp, timestamp,
title, title,
thumbnailUrl,
} = post || {}; } = post || {};
const linkCount = useCountLinksInReplies(post); const linkCount = useCountLinksInReplies(post);
const commentMediaInfo = useCommentMediaInfo(post); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const { hidden } = useHide({ cid }); const { hidden } = useHide({ cid });
+32 -13
View File
@@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Comment } from '@plebbit/plebbit-react-hooks';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
import { getHostname } from '../../lib/utils/url-utils'; import { getHostname } from '../../lib/utils/url-utils';
import useExpandedMediaStore from '../../stores/use-expanded-media-store'; import useExpandedMediaStore from '../../stores/use-expanded-media-store';
@@ -14,12 +13,14 @@ interface MediaProps {
deleted?: boolean; deleted?: boolean;
displayHeight?: string; displayHeight?: string;
displayWidth?: string; displayWidth?: string;
isDescription?: boolean;
isRules?: boolean;
isFloatingEmbed?: boolean; isFloatingEmbed?: boolean;
isOutOfFeed?: boolean; isOutOfFeed?: boolean;
isReply?: boolean; isReply?: boolean;
linkHeight?: number; linkHeight?: number;
linkWidth?: number; linkWidth?: number;
post?: Comment; parentCid?: string;
removed?: boolean; removed?: boolean;
spoiler?: boolean; spoiler?: boolean;
showThumbnail?: boolean; showThumbnail?: boolean;
@@ -130,12 +131,12 @@ interface ImageProps {
displayHeight: string; displayHeight: string;
displayWidth: string; displayWidth: string;
isOutOfFeed: boolean; isOutOfFeed: boolean;
post: Comment | undefined; parentCid?: string;
spoiler?: boolean;
} }
const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, post }: ImageProps) => { const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, parentCid, spoiler }: ImageProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { parentCid, spoiler } = post || {};
const { type, url } = commentMediaInfo || {}; const { type, url } = commentMediaInfo || {};
const isReply = parentCid; const isReply = parentCid;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -181,7 +182,7 @@ const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, pos
{mediaDimensions && `, ${mediaDimensions}`}) {mediaDimensions && `, ${mediaDimensions}`})
</div> </div>
)} )}
{type && !isImageExpanded && <div className={styles.fileInfo}>{`${post?.spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>} {type && !isImageExpanded && <div className={styles.fileInfo}>{`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
</span> </span>
) : ( ) : (
<span <span
@@ -197,8 +198,20 @@ const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, pos
); );
}; };
const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail, setShowThumbnail }: MediaProps) => { const CommentMedia = ({
const { deleted, linkHeight, linkWidth, parentCid, removed, spoiler } = post || {}; commentMediaInfo,
deleted,
isDescription,
isFloatingEmbed,
isRules,
linkHeight,
linkWidth,
parentCid,
removed,
showThumbnail,
setShowThumbnail,
spoiler,
}: MediaProps) => {
const isReply = parentCid; const isReply = parentCid;
const { t } = useTranslation(); const { t } = useTranslation();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -235,14 +248,20 @@ const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail,
displayWidth = '100%'; displayWidth = '100%';
displayHeight = '100%'; displayHeight = '100%';
} }
const { isDescription, isRules } = post || {}; // custom properties, not from api const isOutOfFeed = isDescription || isRules || isFloatingEmbed || spoiler || false; // virtuoso wrapper unneeded
const isOutOfFeed = isDescription || isRules || isFloatingEmbed || spoiler; // virtuoso wrapper unneeded
return ( return (
<span className={styles.content}> <span className={styles.content}>
{commentMediaInfo?.type === 'image' ? ( {commentMediaInfo?.type === 'image' ? (
// images just enlarge when clicked, so they don't need two separate components // images just enlarge when clicked, so they don't need two separate components
<Image commentMediaInfo={commentMediaInfo} displayHeight={displayHeight} displayWidth={displayWidth} isOutOfFeed={isOutOfFeed} post={post} /> <Image
commentMediaInfo={commentMediaInfo}
displayHeight={displayHeight}
displayWidth={displayWidth}
isOutOfFeed={isOutOfFeed}
parentCid={parentCid}
spoiler={spoiler}
/>
) : ( ) : (
<> <>
<span className={`${showThumbnail ? styles.show : styles.hide} ${styles.thumbnail}`}> <span className={`${showThumbnail ? styles.show : styles.hide} ${styles.thumbnail}`}>
@@ -259,9 +278,9 @@ const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail,
setShowThumbnail={setShowThumbnail} setShowThumbnail={setShowThumbnail}
/> />
)} )}
{isMobile && type && <div className={styles.fileInfo}>{`${post?.spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>} {isMobile && type && <div className={styles.fileInfo}>{`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
</span> </span>
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={post?.parentCid} setShowThumbnail={setShowThumbnail} />} {!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={!!parentCid} setShowThumbnail={setShowThumbnail} />}
</> </>
)} )}
</span> </span>
+59 -21
View File
@@ -4,7 +4,7 @@ import { Link, useLocation, useParams } from 'react-router-dom';
import { Comment, useAuthorAvatar, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import { Comment, useAuthorAvatar, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import styles from '../../views/post/post.module.css'; import styles from '../../views/post/post.module.css';
import { getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils'; import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils'; import { isValidURL } from '../../lib/utils/url-utils';
@@ -222,17 +222,21 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }:
); );
}; };
const PostMedia = ({ post, hasThumbnail }: PostProps) => { interface PostMediaProps {
commentMediaInfo: CommentMediaInfo | undefined;
hasThumbnail: boolean;
isDescription: boolean;
isRules: boolean;
spoiler: boolean;
deleted: boolean;
removed: boolean;
linkHeight: number;
linkWidth: number;
parentCid: string;
}
const PostMedia = ({ commentMediaInfo, hasThumbnail, isDescription, isRules, spoiler, deleted, removed, linkHeight, linkWidth, parentCid }: PostMediaProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { spoiler, cid } = post || {};
// Reset state by remounting component when post changes
return <PostMediaContent key={cid} post={post} hasThumbnail={hasThumbnail} spoiler={spoiler} t={t} />;
};
const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasThumbnail: boolean | undefined; spoiler: boolean; t: any }) => {
const { isDescription, isRules } = post || {}; // custom properties, not from api
const commentMediaInfo = useCommentMediaInfo(post);
const { url } = commentMediaInfo || {}; const { url } = commentMediaInfo || {};
let type = commentMediaInfo?.type; let type = commentMediaInfo?.type;
const gifFrameUrl = useFetchGifFirstFrame(url); const gifFrameUrl = useFetchGifFirstFrame(url);
@@ -247,7 +251,7 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
const [showThumbnail, setShowThumbnail] = useState(true); const [showThumbnail, setShowThumbnail] = useState(true);
const mediaDimensions = getMediaDimensions(commentMediaInfo); const mediaDimensions = getMediaDimensions(commentMediaInfo);
console.log('mediaDimensions', mediaDimensions);
return ( return (
<div className={styles.file}> <div className={styles.file}>
<div className={styles.fileText}> <div className={styles.fileText}>
@@ -268,8 +272,7 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
)} )}
{showThumbnail && !hasThumbnail && embedUrl && canEmbed(embedUrl) && ( {showThumbnail && !hasThumbnail && embedUrl && canEmbed(embedUrl) && (
<span> <span>
{' '} -[
[
<span className={styles.closeMedia} onClick={() => setShowThumbnail(false)}> <span className={styles.closeMedia} onClick={() => setShowThumbnail(false)}>
{t('open')} {t('open')}
</span> </span>
@@ -281,10 +284,17 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
<div className={styles.fileThumbnail}> <div className={styles.fileThumbnail}>
<CommentMedia <CommentMedia
commentMediaInfo={commentMediaInfo} commentMediaInfo={commentMediaInfo}
post={post} deleted={deleted}
isDescription={isDescription}
isRules={isRules}
removed={removed}
linkHeight={linkHeight}
linkWidth={linkWidth}
showThumbnail={showThumbnail} showThumbnail={showThumbnail}
setShowThumbnail={setShowThumbnail} setShowThumbnail={setShowThumbnail}
isOutOfFeed={isDescription || isRules} isOutOfFeed={isDescription || isRules}
parentCid={parentCid}
spoiler={spoiler}
/> />
</div> </div>
)} )}
@@ -300,11 +310,13 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
post = editedComment; post = editedComment;
} }
const { author, cid, deleted, link, postCid, reason, removed, subplebbitAddress } = post || {}; const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
const { isDescription, isRules } = post || {}; // custom properties, not from api
const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`); const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`);
const { hidden } = useHide({ cid }); const { hidden } = useHide({ cid });
const commentMediaInfo = useCommentMediaInfo(post); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
return ( return (
@@ -312,7 +324,20 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
<div className={styles.sideArrows}>{'>>'}</div> <div className={styles.sideArrows}>{'>>'}</div>
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}> <div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
<PostInfo openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} /> <PostInfo openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} />
{link && !hidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} hasThumbnail={hasThumbnail} />} {link && !hidden && !(deleted || removed) && isValidURL(link) && (
<PostMedia
commentMediaInfo={commentMediaInfo}
hasThumbnail={hasThumbnail}
isDescription={isDescription}
isRules={isRules}
spoiler={spoiler}
deleted={deleted}
removed={removed}
linkHeight={linkHeight}
linkWidth={linkWidth}
parentCid={parentCid}
/>
)}
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />} {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
</div> </div>
</div> </div>
@@ -321,7 +346,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => { const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { author, cid, content, deleted, link, pinned, postCid, removed, state, subplebbitAddress } = post || {}; const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
const { isDescription, isRules } = post || {}; // custom properties, not from api const { isDescription, isRules } = post || {}; // custom properties, not from api
const params = useParams(); const params = useParams();
const location = useLocation(); const location = useLocation();
@@ -353,7 +378,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
replyCount: 0, replyCount: 0,
}; };
const commentMediaInfo = useCommentMediaInfo(post); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
return ( return (
@@ -373,7 +398,20 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
)} )}
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid} className={shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}> <div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid} className={shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}>
{shouldShowSnow() && hasThumbnail && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} alt='' />} {shouldShowSnow() && hasThumbnail && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} alt='' />}
{link && !isHidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} hasThumbnail={hasThumbnail} />} {link && !isHidden && !(deleted || removed) && isValidURL(link) && (
<PostMedia
commentMediaInfo={commentMediaInfo}
hasThumbnail={hasThumbnail}
isDescription={isDescription}
isRules={isRules}
spoiler={spoiler}
deleted={deleted}
removed={removed}
linkHeight={linkHeight}
linkWidth={linkWidth}
parentCid={parentCid}
/>
)}
<PostInfo isHidden={hidden} openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} /> <PostInfo isHidden={hidden} openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />} {!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
{!isHidden && <CommentContent comment={post} />} {!isHidden && <CommentContent comment={post} />}
@@ -132,8 +132,8 @@ const BlockBoardButton = ({ address }: { address: string }) => {
const PostMenuDesktop = ({ post }: { post: Comment }) => { const PostMenuDesktop = ({ post }: { post: Comment }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { author, cid, isDescription, isRules, link, postCid, subplebbitAddress } = post || {}; const { author, cid, isDescription, isRules, link, thumbnailUrl, linkWidth, linkHeight, postCid, subplebbitAddress } = post || {};
const commentMediaInfo = getCommentMediaInfo(post); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const { thumbnail, type, url } = commentMediaInfo || {}; const { thumbnail, type, url } = commentMediaInfo || {};
const [menuBtnRotated, setMenuBtnRotated] = useState(false); const [menuBtnRotated, setMenuBtnRotated] = useState(false);
@@ -122,9 +122,9 @@ const BlockBoardButton = ({ address }: { address: string }) => {
}; };
const PostMenuMobile = ({ post }: { post: Comment }) => { const PostMenuMobile = ({ post }: { post: Comment }) => {
const { author, cid, deleted, isDescription, isRules, link, parentCid, postCid, removed, subplebbitAddress } = post || {}; const { author, cid, deleted, isDescription, isRules, link, linkHeight, linkWidth, parentCid, postCid, removed, subplebbitAddress, thumbnailUrl } = post || {};
const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress }); const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress });
const commentMediaInfo = getCommentMediaInfo(post); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const { thumbnail, type, url } = commentMediaInfo || {}; const { thumbnail, type, url } = commentMediaInfo || {};
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const { refs, floatingStyles, context } = useFloating({ const { refs, floatingStyles, context } = useFloating({
+32 -6
View File
@@ -27,7 +27,25 @@ import _ from 'lodash';
const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => { const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { author, cid, deleted, link, locked, parentCid, pinned, postCid, reason, removed, shortCid, state, subplebbitAddress, timestamp } = post || {}; const {
author,
cid,
deleted,
link,
linkHeight,
linkWidth,
locked,
parentCid,
pinned,
postCid,
reason,
removed,
shortCid,
state,
subplebbitAddress,
timestamp,
thumbnailUrl,
} = post || {};
const isReply = parentCid; const isReply = parentCid;
const title = post?.title?.trim(); const title = post?.title?.trim();
const { isDescription, isRules } = post || {}; // custom properties, not from api const { isDescription, isRules } = post || {}; // custom properties, not from api
@@ -43,7 +61,7 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
const isInPostPageView = isPostPageView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const commentMediaInfo = useCommentMediaInfo(post); const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const stateString = useStateString(post); const stateString = useStateString(post);
@@ -179,25 +197,33 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
</span> </span>
</span> </span>
</div> </div>
{(hasThumbnail || link) && !(deleted || removed) && <PostMediaContent key={cid} post={post} link={link} t={t} />} {(hasThumbnail || link) && !(deleted || removed) && <PostMediaContent key={cid} post={post} link={link} />}
</> </>
); );
}; };
const PostMediaContent = ({ post, link, t }: { post: any; link: string; t: any }) => { const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
const [showThumbnail, setShowThumbnail] = useState(true); const [showThumbnail, setShowThumbnail] = useState(true);
const { isDescription, isRules } = post || {}; // custom properties, not from api const { isDescription, isRules } = post || {}; // custom properties, not from api
const commentMediaInfo = useCommentMediaInfo(post); const { thumbnailUrl, linkWidth, linkHeight, spoiler, deleted, removed, parentCid } = post || {};
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
return ( return (
hasThumbnail && ( hasThumbnail && (
<CommentMedia <CommentMedia
commentMediaInfo={commentMediaInfo} commentMediaInfo={commentMediaInfo}
post={post} deleted={deleted}
isDescription={isDescription}
isRules={isRules}
removed={removed}
linkHeight={linkHeight}
linkWidth={linkWidth}
showThumbnail={showThumbnail} showThumbnail={showThumbnail}
setShowThumbnail={setShowThumbnail} setShowThumbnail={setShowThumbnail}
isOutOfFeed={isDescription || isRules} isOutOfFeed={isDescription || isRules}
parentCid={parentCid}
spoiler={spoiler}
/> />
) )
); );
+2 -2
View File
@@ -33,8 +33,8 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
// show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update // show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update
const filteredComments = accountComments.filter((comment) => { const filteredComments = accountComments.filter((comment) => {
const { cid, deleted, link, postCid, removed, state, subplebbitAddress, timestamp } = comment || {}; const { cid, deleted, link, postCid, removed, state, subplebbitAddress, timestamp, thumbnailUrl, linkWidth, linkHeight } = comment || {};
const commentMediaInfo = getCommentMediaInfo(comment); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const isMediaShowed = getHasThumbnail(commentMediaInfo, link); const isMediaShowed = getHasThumbnail(commentMediaInfo, link);
return ( return (
+4 -5
View File
@@ -1,10 +1,9 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { Comment } from '@plebbit/plebbit-react-hooks';
import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded } from '../lib/utils/media-utils'; import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded } from '../lib/utils/media-utils';
import { isPendingPostView, isPostPageView } from '../lib/utils/view-utils'; import { isPendingPostView, isPostPageView } from '../lib/utils/view-utils';
export const useCommentMediaInfo = (comment: Comment) => { export const useCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number) => {
const location = useLocation(); const location = useLocation();
const params = useParams(); const params = useParams();
const isInPostPageView = isPostPageView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params);
@@ -12,7 +11,7 @@ export const useCommentMediaInfo = (comment: Comment) => {
// some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false // some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
const fetchThumbnail = useCallback(async () => { const fetchThumbnail = useCallback(async () => {
let commentMediaInfo = getCommentMediaInfo(comment); let commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
if (commentMediaInfo?.type === 'webpage' && !commentMediaInfo.thumbnail) { if (commentMediaInfo?.type === 'webpage' && !commentMediaInfo.thumbnail) {
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(commentMediaInfo); const newMediaInfo = await fetchWebpageThumbnailIfNeeded(commentMediaInfo);
// Fetch the dimensions of the thumbnail // Fetch the dimensions of the thumbnail
@@ -30,7 +29,7 @@ export const useCommentMediaInfo = (comment: Comment) => {
commentMediaInfo = newMediaInfo; commentMediaInfo = newMediaInfo;
} }
return commentMediaInfo; return commentMediaInfo;
}, [comment]); }, [link, thumbnailUrl, linkWidth, linkHeight]);
useEffect(() => { useEffect(() => {
// don't fetch in feed view, it displaces the posts // don't fetch in feed view, it displaces the posts
@@ -39,5 +38,5 @@ export const useCommentMediaInfo = (comment: Comment) => {
} }
}, [fetchThumbnail, isInPostPageView, isInPendingPostView]); }, [fetchThumbnail, isInPostPageView, isInPendingPostView]);
return getCommentMediaInfo(comment); return getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
}; };
+2 -2
View File
@@ -22,10 +22,10 @@ const usePopularPosts = (subplebbits: Subplebbit[]) => {
if (subplebbit?.posts?.pages?.hot?.comments) { if (subplebbit?.posts?.pages?.hot?.comments) {
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) { for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) {
const { deleted, link, locked, pinned, removed, replyCount, timestamp } = post; const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl, timestamp } = post;
try { try {
const commentMediaInfo = getCommentMediaInfo(post); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
if ( if (
+47 -40
View File
@@ -1,5 +1,4 @@
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js'; import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
import { Comment } from '@plebbit/plebbit-react-hooks';
import extName from 'ext-name'; import extName from 'ext-name';
import { canEmbed } from '../../components/embed'; import { canEmbed } from '../../components/embed';
import memoize from 'memoizee'; import memoize from 'memoizee';
@@ -13,7 +12,8 @@ export interface CommentMediaInfo {
thumbnailWidth?: number; thumbnailWidth?: number;
thumbnailHeight?: number; thumbnailHeight?: number;
patternThumbnailUrl?: string; patternThumbnailUrl?: string;
post?: Comment; linkWidth?: number;
linkHeight?: number;
} }
export const getDisplayMediaInfoType = (type: string, t: any) => { export const getDisplayMediaInfoType = (type: string, t: any) => {
@@ -35,19 +35,20 @@ export const getDisplayMediaInfoType = (type: string, t: any) => {
} }
}; };
export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => { export const getHasThumbnail = memoize(
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail; (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
return link && if (!link || !commentMediaInfo) return false;
commentMediaInfo &&
(commentMediaInfo.type === 'image' || const { type, thumbnail, patternThumbnailUrl } = commentMediaInfo;
commentMediaInfo.type === 'video' ||
commentMediaInfo.type === 'audio' || if (type === 'image' || type === 'video' || type === 'audio' || type === 'gif') return true;
commentMediaInfo.type === 'gif' || if (type === 'webpage' && thumbnail) return true;
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) || if (type === 'iframe' && (patternThumbnailUrl || thumbnail)) return true;
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
? true return false;
: false; },
}; { max: 1000 },
);
const getYouTubeVideoId = (url: URL): string | null => { const getYouTubeVideoId = (url: URL): string | null => {
if (url.host.includes('youtu.be')) { if (url.host.includes('youtu.be')) {
@@ -182,55 +183,61 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
} }
}; };
export const getCommentMediaInfo = (comment: Comment): CommentMediaInfo | undefined => { export const getCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number): CommentMediaInfo | undefined => {
if (!comment?.thumbnailUrl && !comment?.link) { if (!thumbnailUrl && !link) {
return; return;
} }
const linkInfo = comment.link ? getLinkMediaInfo(comment.link) : undefined; const linkInfo = link ? getLinkMediaInfo(link) : undefined;
if (linkInfo) { if (linkInfo) {
return { return {
...linkInfo, ...linkInfo,
thumbnail: comment.thumbnailUrl || linkInfo.thumbnail, thumbnail: thumbnailUrl || linkInfo.thumbnail,
post: comment, linkWidth,
linkHeight,
}; };
} }
return; return;
}; };
export const getMediaDimensions = (commentMediaInfo: CommentMediaInfo | undefined): string => { const EMBED_DIMENSIONS = {
'youtube.com': '800x450',
'youtu.be': '800x450',
'instagram.com': '360x420',
'reddit.com': '500x520',
'tiktok.com': '400x780',
'x.com': '550x580',
'twitter.com': '550x580',
'soundcloud.com': '700x166',
} as const;
export const getMediaDimensions = memoize(
(commentMediaInfo: CommentMediaInfo | undefined): string => {
if (!commentMediaInfo) return ''; if (!commentMediaInfo) return '';
const { type, url, post } = commentMediaInfo; const { type, url, linkWidth, linkHeight } = commentMediaInfo;
if (type === 'iframe' && url) { if (type === 'iframe' && url) {
const embedUrl = new URL(url); const embedUrl = new URL(url);
if (canEmbed(embedUrl)) { if (canEmbed(embedUrl)) {
// hardcoded dimensions from embed.module.css const hostname = embedUrl.hostname;
if (embedUrl.hostname.includes('youtube.com') || embedUrl.hostname.includes('youtu.be')) { for (const [site, dimensions] of Object.entries(EMBED_DIMENSIONS)) {
return '800x450'; if (hostname.includes(site)) {
} else if (embedUrl.hostname.includes('instagram.com')) { return dimensions;
return '360x420'; }
} else if (embedUrl.hostname.includes('reddit.com')) {
return '500x520';
} else if (embedUrl.hostname.includes('tiktok.com')) {
return '400x780';
} else if (embedUrl.hostname.includes('x.com') || embedUrl.hostname.includes('twitter.com')) {
return '550x580';
} else if (embedUrl.hostname.includes('soundcloud.com')) {
return '700x166';
} }
} }
} else if (type === 'audio') { } else if (type === 'audio') {
return '700x240'; // hardcoded dimensions from embed.module.css return '700x240';
} else if (type === 'image' || type === 'video' || type === 'gif') { } else if (type === 'image' || type === 'video' || type === 'gif') {
// media dimensions calculated by API if (linkWidth && linkHeight) {
if (post?.linkWidth && post?.linkHeight) { return `${linkWidth}x${linkHeight}`;
return `${post.linkWidth}x${post.linkHeight}`;
} }
} }
return ''; return '';
}; },
{ max: 1000 },
);
const thumbnailUrlsDb = localForageLru.createInstance({ name: 'plebchanThumbnailUrls', size: 500 }); const thumbnailUrlsDb = localForageLru.createInstance({ name: 'plebchanThumbnailUrls', size: 500 });
+3 -1
View File
@@ -53,7 +53,9 @@ const useCatalogFiltersStore = create(
// filterItems // filterItems
} = state; } = state;
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(comment), comment?.link); const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link);
// const title = comment?.title?.toLowerCase() || ''; // const title = comment?.title?.toLowerCase() || '';
// const content = comment?.content?.toLowerCase() || ''; // const content = comment?.content?.toLowerCase() || '';
+4 -3
View File
@@ -24,7 +24,8 @@ import SubplebbitRules from '../../components/subplebbit-rules';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const threadsWithoutImagesFilter = (comment: Comment) => { const threadsWithoutImagesFilter = (comment: Comment) => {
if (!getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) { const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) {
return false; return false;
} }
return true; return true;
@@ -78,14 +79,14 @@ const Board = () => {
const filteredComments = useMemo( const filteredComments = useMemo(
() => () =>
accountComments.filter((comment) => { accountComments.filter((comment) => {
const { cid, deleted, postCid, removed, state, timestamp } = comment || {}; const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {};
return ( return (
!deleted && !deleted &&
!removed && !removed &&
timestamp > Date.now() / 1000 - 60 * 60 && timestamp > Date.now() / 1000 - 60 * 60 &&
state === 'succeeded' && state === 'succeeded' &&
cid && cid &&
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(comment), comment?.link) : true) && (hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
cid === postCid && cid === postCid &&
comment?.subplebbitAddress === subplebbitAddress && comment?.subplebbitAddress === subplebbitAddress &&
!feed.some((post) => post.cid === cid) !feed.some((post) => post.cid === cid)
+4 -3
View File
@@ -22,7 +22,8 @@ import styles from './catalog.module.css';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const threadsWithoutImagesFilter = (comment: Comment) => { const threadsWithoutImagesFilter = (comment: Comment) => {
if (!getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) { const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) {
return false; return false;
} }
return true; return true;
@@ -98,14 +99,14 @@ const Catalog = () => {
const filteredComments = useMemo( const filteredComments = useMemo(
() => () =>
accountComments.filter((comment) => { accountComments.filter((comment) => {
const { cid, deleted, postCid, removed, state, timestamp } = comment || {}; const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {};
return ( return (
!deleted && !deleted &&
!removed && !removed &&
timestamp > Date.now() / 1000 - 60 * 60 && timestamp > Date.now() / 1000 - 60 * 60 &&
state === 'succeeded' && state === 'succeeded' &&
cid && cid &&
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(comment), comment?.link) : true) && (hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
cid === postCid && cid === postCid &&
comment?.subplebbitAddress === subplebbitAddress && comment?.subplebbitAddress === subplebbitAddress &&
!feed.some((post) => post.cid === cid) !feed.some((post) => post.cid === cid)
@@ -26,8 +26,8 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
}; };
const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => { const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => {
const { cid, content, subplebbitAddress, title } = post || {}; const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(post); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
return ( return (
<div className={styles.popularThread} key={cid}> <div className={styles.popularThread} key={cid}>