feat(catalog): add media, styling

This commit is contained in:
plebeius.eth
2024-04-12 17:47:30 +02:00
parent f124d03366
commit 65c99ab188
7 changed files with 197 additions and 82 deletions
+12 -6
View File
@@ -2,10 +2,11 @@ import { useTranslation } from 'react-i18next';
import { Link, useLocation, useParams } from 'react-router-dom';
import { useSubscribe } from '@plebbit/plebbit-react-hooks';
import styles from './board-buttons.module.css';
import { isPostPageView } from '../../lib/utils/view-utils';
import { isCatalogView, isPostPageView } from '../../lib/utils/view-utils';
interface BoardButtonsProps {
address: string | undefined;
isInCatalogView?: boolean;
}
const OptionsButton = () => {
@@ -13,11 +14,11 @@ const OptionsButton = () => {
return <button className='button'>{t('options')}</button>;
};
const CatalogButton = ({ address }: BoardButtonsProps) => {
const CatalogButton = ({ address, isInCatalogView }: BoardButtonsProps) => {
const { t } = useTranslation();
return (
<button className='button'>
<Link to={`/p/${address}/catalog`}>{t('catalog')}</Link>
<Link to={isInCatalogView ? `/p/${address}` : `/p/${address}/catalog`}>{t(isInCatalogView ? 'return' : 'catalog')}</Link>
</button>
);
};
@@ -79,20 +80,25 @@ export const MobileBoardButtons = () => {
export const DesktopBoardButtons = () => {
const { subplebbitAddress: address } = useParams<{ subplebbitAddress: string }>();
const isInPostPage = isPostPageView(useLocation().pathname, useParams());
const location = useLocation();
const params = useParams();
const isInPostPage = isPostPageView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
return (
<div className={styles.desktopBoardButtons}>
<hr />
{isInPostPage ? (
<>
[<ReturnButton address={address} />] [<CatalogButton address={address} />] [<BottomButton />] [<OptionsButton />]
[<ReturnButton address={address} />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />] [<BottomButton />] [<OptionsButton />]
<span className={styles.subscribeButton}>
[<SubscribeButton address={address} />]
</span>
</>
) : (
<>
[<OptionsButton />] [<CatalogButton address={address} />]
[<OptionsButton />] [<CatalogButton address={address} isInCatalogView={isInCatalogView} />]
<span className={styles.subscribeButton}>
[<SubscribeButton address={address} />]
</span>
@@ -1,39 +1,59 @@
.row {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
justify-content: center;
text-align: center;
padding-bottom: 20px;
}
.post {
width: 180px;
font-size: 12px;
/* fix text overflowing */
position: relative;
max-height: 320px;
padding: 10px 0 3px 0;
vertical-align: top;
display: inline-block;
overflow: hidden;
}
.postHeader {
/* make cursors like button */
cursor: pointer;
.mediaPaddingWrapper {
padding-bottom: 5px;
display: flex;
justify-content: center;
align-items: center;
}
.postStats {
font-weight: bold;
}
.media {
height: 180px;
}
/* media and no media must be the same height, or the infinite scroll bugs out */
.mediaWrapper {
overflow: hidden;
width: 180px;
height: 180px;
background-color: var(--media-thumbnail-background-color);
width: var(--width);
height: var(--height);
max-width: 150px;
max-height: 150px;
}
/* add some padding to rows when there's no image */
.noMedia {
height: 2px;
.post img, .post video, .post audio {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
width: 100%;
height: 100%;
min-width: 50px;
min-height: 50px;
object-fit: contain;
}
.meta {
font-size: 11px;
line-height: 8px;
padding: 2px 0 1px 0;
}
.teaser {
display: block;
padding: 0 15px;
overflow: hidden;
word-wrap: break-word;
}
.post a {
color: unset;
text-decoration: none;
}
+81 -5
View File
@@ -1,10 +1,88 @@
import styles from './catalog-row.module.css';
import { Comment } from '@plebbit/plebbit-react-hooks';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import styles from './catalog-row.module.css';
import { Link } from 'react-router-dom';
const CatalogPostMedia = ({ commentMediaInfo, link }: { commentMediaInfo: any; link: string }) => {
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
const iframeThumbnail = patternThumbnailUrl || thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
let thumbnailComponent: React.ReactNode = null;
if (type === 'image') {
thumbnailComponent = <img src={url} alt='' />;
} else if (type === 'video') {
thumbnailComponent = thumbnail ? <img src={thumbnail} alt='' /> : <video src={`${url}#t=0.001`} />;
} else if (type === 'webpage') {
thumbnailComponent = <img src={thumbnail} alt='' />;
} else if (type === 'iframe') {
thumbnailComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' /> : null;
} else if (type === 'gif' && gifFrameUrl) {
thumbnailComponent = <img src={gifFrameUrl} alt='' />;
} else if (type === 'audio') {
thumbnailComponent = <audio src={url} controls />;
}
return thumbnailComponent;
};
const CatalogPost = ({ post }: { post: Comment }) => {
const { title, cid } = post || {};
const { cid, content, isDescription, isRules, link, linkHeight, linkWidth, replyCount, subplebbitAddress, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(post);
const { type } = commentMediaInfo || {};
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
return <div className={styles.post}>{title || 'this is a post without a title'}</div>;
let displayWidth, displayHeight;
const maxThumbnailSize = 150;
if (linkWidth && linkHeight) {
let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight));
displayWidth = `${linkWidth * scale}px`;
displayHeight = `${linkHeight * scale}px`;
} else {
displayWidth = `${maxThumbnailSize}px`;
displayHeight = `${maxThumbnailSize}px`;
}
if (type === 'audio') {
displayWidth = '150px';
displayHeight = '75px';
}
const thumbnailDimensions = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties;
const postLink = `/p/${subplebbitAddress}/${isDescription ? 'description' : isRules ? 'rules' : `c/${cid}`}`;
const linkCount = useCountLinksInReplies(post);
return (
<div className={styles.post}>
{hasThumbnail && (
<Link to={postLink}>
<div className={styles.mediaPaddingWrapper}>
<div className={styles.mediaWrapper} style={thumbnailDimensions}>
<CatalogPostMedia commentMediaInfo={commentMediaInfo} link={link} />
</div>
</div>
</Link>
)}
<div className={styles.meta}>
R: <b>{replyCount || '0'}</b>
{linkCount > 0 && (
<span>
{' '}
/ L: <b>{linkCount}</b>
</span>
)}
</div>
<Link to={postLink}>
<div className={styles.teaser}>
<b>{title && `${title}${content ? ': ' : ''}`}</b>
{content}
</div>
</Link>
</div>
);
};
interface CatalogRowProps {
@@ -16,9 +94,7 @@ interface CatalogRowProps {
const CatalogRow = ({ description, index, row, rules }: CatalogRowProps) => {
const rowPosts = index === 0 ? [...(rules?.content?.length > 0 ? [rules] : []), ...(description?.content?.length > 0 ? [description] : []), ...row] : row;
const posts = rowPosts.map((post, index) => <CatalogPost key={index} post={post} />);
return <div className={styles.row}>{posts}</div>;
};
+33 -32
View File
@@ -36,8 +36,11 @@ const ThumbnailSmall = ({ style, children, thumbnailSmallPadding }: ThumbnailPro
);
const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => {
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
let displayWidth, displayHeight;
const maxThumbnailSize = isMobile || isReply ? 125 : 250;
if (linkWidth && linkHeight) {
let scale = Math.min(1, maxThumbnailSize / Math.max(linkWidth, linkHeight));
displayWidth = `${linkWidth * scale}px`;
@@ -47,7 +50,7 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
displayHeight = `${maxThumbnailSize}px`;
}
if (commentMediaInfo?.type === 'audio') {
if (type === 'audio') {
displayWidth = '250px';
displayHeight = '75px';
}
@@ -58,24 +61,21 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
}
let thumbnailComponent: React.ReactNode = null;
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(commentMediaInfo?.type === 'gif' ? commentMediaInfo.url : undefined);
if (commentMediaInfo?.type === 'image') {
thumbnailComponent = <img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (commentMediaInfo?.type === 'video') {
thumbnailComponent = commentMediaInfo.thumbnail ? (
<img src={commentMediaInfo.thumbnail} alt='' />
) : (
<video src={`${commentMediaInfo.url}#t=0.001`} onClick={() => setShowThumbnail(false)} />
);
} else if (commentMediaInfo?.type === 'webpage') {
thumbnailComponent = <img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (commentMediaInfo?.type === 'iframe') {
const iframeThumbnail = patternThumbnailUrl || thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
if (type === 'image') {
thumbnailComponent = <img src={url} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (type === 'video') {
thumbnailComponent = thumbnail ? <img src={thumbnail} alt='' /> : <video src={`${url}#t=0.001`} onClick={() => setShowThumbnail(false)} />;
} else if (type === 'webpage') {
thumbnailComponent = <img src={thumbnail} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (type === 'iframe') {
thumbnailComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' onClick={() => setShowThumbnail(false)} /> : null;
} else if (commentMediaInfo?.type === 'gif' && gifFrameUrl) {
} else if (type === 'gif' && gifFrameUrl) {
thumbnailComponent = <img src={gifFrameUrl} alt='' onClick={() => setShowThumbnail(false)} />;
} else if (commentMediaInfo?.type === 'audio') {
thumbnailComponent = <audio src={commentMediaInfo.url} controls />;
} else if (type === 'audio') {
thumbnailComponent = <audio src={url} controls />;
}
const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop;
@@ -92,30 +92,31 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
const Media = ({ commentMediaInfo, isMobile, isReply, setShowThumbnail }: MediaProps) => {
const { t } = useTranslation();
const { thumbnail, type, url } = commentMediaInfo || {};
const mediaClass = isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp;
return (
<span className={mediaClass}>
{commentMediaInfo?.type === 'iframe' ? (
<Embed url={commentMediaInfo.url} />
) : commentMediaInfo?.type === 'gif' ? (
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} />
) : commentMediaInfo?.type === 'video' ? (
<video src={commentMediaInfo.url} controls autoPlay loop muted />
) : commentMediaInfo?.type === 'image' ? (
<img src={commentMediaInfo.url} alt='' onClick={() => setShowThumbnail(true)} />
) : commentMediaInfo?.type === 'webpage' ? (
<img src={commentMediaInfo.thumbnail} alt='' onClick={() => setShowThumbnail(true)} />
{type === 'iframe' && url ? (
<Embed url={url} />
) : type === 'gif' ? (
<img src={url} alt='' onClick={() => setShowThumbnail(true)} />
) : type === 'video' ? (
<video src={url} controls autoPlay loop muted />
) : type === 'image' ? (
<img src={url} alt='' onClick={() => setShowThumbnail(true)} />
) : type === 'webpage' ? (
<img src={thumbnail} alt='' onClick={() => setShowThumbnail(true)} />
) : null}
{isMobile && commentMediaInfo?.type && (
{isMobile && type && (
<div className={styles.fileInfo}>
<a href={commentMediaInfo.url} target='_blank' rel='noopener noreferrer'>
{commentMediaInfo.url.length > 30 ? commentMediaInfo?.url.slice(0, 30) + '...' : commentMediaInfo?.url}
<a href={url} target='_blank' rel='noopener noreferrer'>
{url && url.length > 30 ? url.slice(0, 30) + '...' : url}
</a>{' '}
({commentMediaInfo?.type})
({type})
</div>
)}
{isMobile && (commentMediaInfo?.type === 'iframe' || commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'audio') && (
{isMobile && (type === 'iframe' || type === 'video' || type === 'audio') && (
<div className={styles.closeButton}>
<span className='button' onClick={() => setShowThumbnail(true)}>
{t('close')}
+5
View File
@@ -24,3 +24,8 @@ export const isPostPageView = (pathname: string, params: ParamsType): boolean =>
const decodedPathname = decodeURIComponent(pathname);
return params.subplebbitAddress && params.commentCid ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/c/${params.commentCid}`) : false;
};
export const isCatalogView = (pathname: string, params: ParamsType): boolean => {
const decodedPathname = decodeURIComponent(pathname);
return params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}/catalog`) : false;
};
+4
View File
@@ -2,6 +2,10 @@
padding-top: 10px;
}
.catalog {
padding-top: 20px;
}
@media (max-width: 640px) {
.footer {
padding-left: 5px;
+19 -16
View File
@@ -118,7 +118,7 @@ const Catalog = () => {
});
window.addEventListener('scroll', setLastVirtuosoState);
return () => window.removeEventListener('scroll', setLastVirtuosoState);
}, []);
}, [subplebbitAddress]);
const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + 'catalog'];
@@ -128,21 +128,24 @@ const Catalog = () => {
}, [title, shortAddress]);
return (
<div>
<Virtuoso
increaseViewportBy={{ bottom: 1200, top: 1200 }}
totalCount={rows?.length || 0}
data={rows}
itemContent={(index, row) => (
<CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} />
)}
useWindowScroll={true}
components={{ Footer }}
endReached={loadMore}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
initialScrollTop={lastVirtuosoState?.scrollTop}
/>
<div className={styles.content}>
<hr />
<div className={styles.catalog}>
<Virtuoso
increaseViewportBy={{ bottom: 1200, top: 1200 }}
totalCount={rows?.length || 0}
data={rows}
itemContent={(index, row) => (
<CatalogRow description={index === 0 && includeDescription && descriptionPost} index={index} row={row} rules={index === 0 && includeRules && rulesPost} />
)}
useWindowScroll={true}
components={{ Footer }}
endReached={loadMore}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
initialScrollTop={lastVirtuosoState?.scrollTop}
/>
</div>
</div>
);
};