mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
+2
-7
@@ -1,8 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { isHomeView } from './lib/utils/view-utils';
|
||||
import { useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { useDefaultSubplebbitAddresses } from './hooks/use-default-subplebbits';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import styles from './app.module.css';
|
||||
import Catalog from './views/catalog';
|
||||
@@ -21,9 +19,6 @@ const App = () => {
|
||||
const location = useLocation();
|
||||
const isInHomeView = isHomeView(location.pathname);
|
||||
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
|
||||
// add theme className to body so it can set the correct body background in index.css
|
||||
const [theme] = useTheme();
|
||||
useEffect(() => {
|
||||
@@ -40,7 +35,7 @@ const App = () => {
|
||||
|
||||
const boardLayout = (
|
||||
<>
|
||||
<BoardNav subplebbits={subplebbits} />
|
||||
<BoardNav />
|
||||
<BoardBanner />
|
||||
<MobileBoardButtons />
|
||||
<PostForm />
|
||||
@@ -53,7 +48,7 @@ const App = () => {
|
||||
return (
|
||||
<div className={`${styles.app} ${isInHomeView ? 'yotsuba' : theme}`}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Home subplebbits={subplebbits} />} />
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route element={boardLayout}>
|
||||
<Route path='/p/:subplebbitAddress' element={<Board />} />
|
||||
<Route path='/p/:subplebbitAddress/c/:commentCid' element={<PostPage />} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { useState } from 'react';
|
||||
import styles from './board-banner.module.css';
|
||||
|
||||
const totalBanners = 52;
|
||||
const totalBanners = 53;
|
||||
|
||||
const ImageBanner = () => {
|
||||
const [imagePath] = useState(() => {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { isCatalogView } from '../../lib/utils/view-utils';
|
||||
import styles from './board-nav.module.css';
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
|
||||
interface BoardNavProps {
|
||||
subplebbits: (Subplebbit | undefined)[];
|
||||
currentSubplebbit?: string | undefined;
|
||||
subplebbitAddresses: string[];
|
||||
subplebbitAddress?: string;
|
||||
}
|
||||
|
||||
const BoardNavDesktop = ({ subplebbits }: BoardNavProps) => {
|
||||
const BoardNavDesktop = ({ subplebbitAddresses }: BoardNavProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
|
||||
|
||||
@@ -17,16 +17,12 @@ const BoardNavDesktop = ({ subplebbits }: BoardNavProps) => {
|
||||
<div className={styles.boardNavDesktop}>
|
||||
<span className={styles.boardList}>
|
||||
[
|
||||
{subplebbits.map((subplebbit: any, index: number) => {
|
||||
const address = subplebbit?.address || '';
|
||||
const title = subplebbit?.title || '';
|
||||
{subplebbitAddresses.map((address: string, index: number) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{index === 0 ? null : ' '}
|
||||
<Link to={`/p/${address}${isInCatalogView ? '/catalog' : ''}`} title={title || ''}>
|
||||
{address.includes('.') ? address : title || address.slice(0, 10).concat('...')}
|
||||
</Link>
|
||||
{index !== subplebbits.length - 1 ? ' /' : null}
|
||||
<Link to={`/p/${address}${isInCatalogView ? '/catalog' : ''}`}>{address.includes('.') ? address : address.slice(0, 10).concat('...')}</Link>
|
||||
{index !== address.length - 1 ? ' /' : null}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
@@ -39,24 +35,23 @@ const BoardNavDesktop = ({ subplebbits }: BoardNavProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
const BoardNavMobile = ({ subplebbitAddresses, subplebbitAddress }: BoardNavProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const displaySubplebbitAddress = currentSubplebbit && currentSubplebbit.length > 30 ? currentSubplebbit.slice(0, 30).concat('...') : currentSubplebbit;
|
||||
const displaySubplebbitAddress = subplebbitAddress && subplebbitAddress.length > 30 ? subplebbitAddress.slice(0, 30).concat('...') : subplebbitAddress;
|
||||
|
||||
const currentSubplebbitIsInList = subplebbits.some((subplebbit: any) => subplebbit?.address === currentSubplebbit);
|
||||
const currentSubplebbitIsInList = subplebbitAddresses.some((address: string) => address === subplebbitAddress);
|
||||
|
||||
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
|
||||
|
||||
const boardSelect = (
|
||||
<select value={currentSubplebbit || 'all'} onChange={(e) => navigate(`/p/${e.target.value}${isInCatalogView ? '/catalog' : ''}`)}>
|
||||
{!currentSubplebbitIsInList && currentSubplebbit && <option value={currentSubplebbit}>{displaySubplebbitAddress}</option>}
|
||||
<select value={subplebbitAddress || 'all'} onChange={(e) => navigate(`/p/${e.target.value}${isInCatalogView ? '/catalog' : ''}`)}>
|
||||
{!currentSubplebbitIsInList && subplebbitAddress && <option value={subplebbitAddress}>{displaySubplebbitAddress}</option>}
|
||||
<option value='all'>{t('all')}</option>
|
||||
<option value='subscriptions'>{t('subscriptions')}</option>
|
||||
{subplebbits.map((subplebbit: any, index: number) => {
|
||||
{subplebbitAddresses.map((subplebbit: any, index: number) => {
|
||||
const address = subplebbit?.address || '';
|
||||
const title = subplebbit?.title || '';
|
||||
const subplebbitAddress = address?.includes('.') ? address : title || address?.slice(0, 10).concat('...');
|
||||
const subplebbitAddress = address?.includes('.') ? address : address?.slice(0, 10).concat('...');
|
||||
return (
|
||||
<option key={index} value={address}>
|
||||
{subplebbitAddress}
|
||||
@@ -80,12 +75,13 @@ const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BoardNav = ({ subplebbits }: BoardNavProps) => {
|
||||
const { subplebbitAddress: address } = useParams();
|
||||
const BoardNav = () => {
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbitAddress } = useParams();
|
||||
return (
|
||||
<>
|
||||
<BoardNavDesktop subplebbits={subplebbits} />
|
||||
<BoardNavMobile subplebbits={subplebbits} currentSubplebbit={address} />
|
||||
<BoardNavDesktop subplebbitAddresses={subplebbitAddresses} />
|
||||
<BoardNavMobile subplebbitAddresses={subplebbitAddresses} subplebbitAddress={subplebbitAddress} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
@@ -29,7 +29,13 @@ const CatalogPostMedia = ({ commentMediaInfo, link }: { commentMediaInfo: any; l
|
||||
return thumbnailComponent;
|
||||
};
|
||||
|
||||
const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
interface CatalogPostProps {
|
||||
post: Comment;
|
||||
openMenu: boolean;
|
||||
toggleMenu: () => void;
|
||||
}
|
||||
|
||||
const CatalogPost = ({ openMenu, post, toggleMenu }: CatalogPostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cid, content, isDescription, isRules, link, linkHeight, linkWidth, locked, pinned, replyCount, subplebbitAddress, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
@@ -65,7 +71,18 @@ const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const [menuBtnRotated, setMenuBtnRotated] = useState(false);
|
||||
useEffect(() => {
|
||||
const handlePageClick = () => {
|
||||
if (openMenu) {
|
||||
toggleMenu();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handlePageClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handlePageClick);
|
||||
};
|
||||
}, [openMenu, toggleMenu]);
|
||||
|
||||
return (
|
||||
<div className={styles.post}>
|
||||
@@ -93,8 +110,11 @@ const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
<span
|
||||
className={styles.postMenuBtn}
|
||||
title='Thread Menu'
|
||||
onClick={() => setMenuBtnRotated(!menuBtnRotated)}
|
||||
style={{ transform: menuBtnRotated ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleMenu();
|
||||
}}
|
||||
style={{ transform: openMenu ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
@@ -116,8 +136,19 @@ interface CatalogRowProps {
|
||||
}
|
||||
|
||||
const CatalogRow = ({ row }: CatalogRowProps) => {
|
||||
const posts = row.map((post, index) => <CatalogPost key={index} post={post} />);
|
||||
return <div className={styles.row}>{posts}</div>;
|
||||
const [postWithMenuOpen, setPostWithMenuOpen] = useState<number | null>(null);
|
||||
|
||||
const handleToggleMenu = (index: number) => {
|
||||
setPostWithMenuOpen(postWithMenuOpen === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
{row.map((post, index) => (
|
||||
<CatalogPost key={index} post={post} openMenu={postWithMenuOpen === index} toggleMenu={() => handleToggleMenu(index)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogRow;
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbnailSmall span, .thumbnailSmall a {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
float: left;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './comment-media.module.css';
|
||||
import { CommentMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { CommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||
import Embed from '../embed';
|
||||
import Embed, { canEmbed } from '../embed';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useWindowWidth from '../../hooks/use-window-width';
|
||||
import { getHostname } from '../../lib/utils/url-utils';
|
||||
|
||||
interface MediaProps {
|
||||
commentMediaInfo?: CommentMediaInfo;
|
||||
isMobile: boolean;
|
||||
isOutOfFeed?: boolean; // virtuoso wrapper unneeded
|
||||
isReply: boolean;
|
||||
linkHeight?: number;
|
||||
@@ -35,10 +36,11 @@ const ThumbnailSmall = ({ style, children, thumbnailSmallPadding }: ThumbnailPro
|
||||
</span>
|
||||
);
|
||||
|
||||
const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => {
|
||||
const Thumbnail = ({ commentMediaInfo, isOutOfFeed, isReply, linkHeight, linkWidth, setShowThumbnail }: MediaProps) => {
|
||||
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
|
||||
|
||||
let displayWidth, displayHeight;
|
||||
const isMobile = useWindowWidth() < 640;
|
||||
const maxThumbnailSize = isMobile || isReply ? 125 : 250;
|
||||
|
||||
if (linkWidth && linkHeight) {
|
||||
@@ -51,8 +53,8 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
|
||||
}
|
||||
|
||||
if (type === 'audio') {
|
||||
displayWidth = '250px';
|
||||
displayHeight = '75px';
|
||||
displayWidth = '100%';
|
||||
displayHeight = '100%';
|
||||
}
|
||||
|
||||
if (isOutOfFeed) {
|
||||
@@ -63,6 +65,7 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
|
||||
let thumbnailComponent: React.ReactNode = null;
|
||||
const iframeThumbnail = patternThumbnailUrl || thumbnail;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, url);
|
||||
|
||||
if (type === 'image') {
|
||||
thumbnailComponent = <img src={url} alt='' onClick={() => setShowThumbnail(false)} />;
|
||||
@@ -81,18 +84,31 @@ const Thumbnail = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeigh
|
||||
const thumbnailSmallPadding = isMobile ? styles.thumbnailMobile : styles.thumbnailReplyDesktop;
|
||||
const thumbnailDimensions = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties;
|
||||
|
||||
const linkWithoutThumbnail = url && new URL(url);
|
||||
|
||||
return isMobile || isReply ? (
|
||||
<ThumbnailSmall style={thumbnailDimensions} thumbnailSmallPadding={thumbnailSmallPadding}>
|
||||
{thumbnailComponent}
|
||||
{isMobile &&
|
||||
!hasThumbnail &&
|
||||
linkWithoutThumbnail &&
|
||||
(canEmbed(linkWithoutThumbnail) ? (
|
||||
<span onClick={() => setShowThumbnail(false)}>{getHostname(url)}</span>
|
||||
) : (
|
||||
<a href={url} target='_blank' rel='noreferrer'>
|
||||
{getHostname(url) || (url.length > 30 ? url.slice(0, 30) + '...' : url)}
|
||||
</a>
|
||||
))}
|
||||
</ThumbnailSmall>
|
||||
) : (
|
||||
<ThumbnailBig style={thumbnailDimensions}>{thumbnailComponent}</ThumbnailBig>
|
||||
);
|
||||
};
|
||||
|
||||
const Media = ({ commentMediaInfo, isMobile, isReply, setShowThumbnail }: MediaProps) => {
|
||||
const Media = ({ commentMediaInfo, isReply, setShowThumbnail }: MediaProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const isMobile = useWindowWidth() < 640;
|
||||
const mediaClass = isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp;
|
||||
|
||||
return (
|
||||
@@ -127,23 +143,26 @@ const Media = ({ commentMediaInfo, isMobile, isReply, setShowThumbnail }: MediaP
|
||||
);
|
||||
};
|
||||
|
||||
const CommentMedia = ({ commentMediaInfo, isMobile, isOutOfFeed, isReply, linkHeight, linkWidth, showThumbnail, setShowThumbnail }: MediaProps) => {
|
||||
const CommentMedia = ({ commentMediaInfo, isOutOfFeed, isReply, linkHeight, linkWidth, showThumbnail, setShowThumbnail }: MediaProps) => {
|
||||
const isMobile = useWindowWidth() < 640;
|
||||
const { type, url } = commentMediaInfo || {};
|
||||
return (
|
||||
<span className={styles.content}>
|
||||
<span className={`${showThumbnail ? styles.show : styles.hide} ${styles.thumbnail}`}>
|
||||
<Thumbnail
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isMobile={isMobile}
|
||||
isOutOfFeed={isOutOfFeed}
|
||||
isReply={isReply}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
showThumbnail={showThumbnail}
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
/>
|
||||
{isMobile && commentMediaInfo?.type && <div className={styles.fileInfo}>{commentMediaInfo.type}</div>}
|
||||
{url && (
|
||||
<Thumbnail
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isOutOfFeed={isOutOfFeed}
|
||||
isReply={isReply}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
showThumbnail={showThumbnail}
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
/>
|
||||
)}
|
||||
{isMobile && type && <div className={styles.fileInfo}>{type}</div>}
|
||||
</span>
|
||||
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isMobile={isMobile} isReply={isReply} setShowThumbnail={setShowThumbnail} />}
|
||||
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={isReply} setShowThumbnail={setShowThumbnail} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,4 +67,15 @@
|
||||
width: 100% !important;
|
||||
height: 50vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.soundcloudEmbed {
|
||||
height: 166px; /* default height of soundcloud embeds */
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.soundcloudEmbed {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ const Embed = ({ url }: EmbedProps) => {
|
||||
if (spotifyHosts.has(parsedUrl.host)) {
|
||||
return <SpotifyEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (soundcloudHosts.has(parsedUrl.host)) {
|
||||
return <SoundcloudEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
};
|
||||
|
||||
interface EmbedComponentProps {
|
||||
@@ -258,6 +261,24 @@ const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const soundcloudHosts = new Set(['soundcloud.com', 'www.soundcloud.com', 'on.soundcloud.com', 'api.soundcloud.com', 'w.soundcloud.com']);
|
||||
|
||||
// not officially documented https://stackoverflow.com/questions/20870270/how-to-get-soundcloud-embed-code-by-soundcloud-com-url
|
||||
const SoundcloudEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
return (
|
||||
<iframe
|
||||
className={styles.soundcloudEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://w.soundcloud.com/player/?url=${parsedUrl.href}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const canEmbedHosts = new Set<string>([
|
||||
...youtubeHosts,
|
||||
...xHosts,
|
||||
@@ -267,6 +288,7 @@ const canEmbedHosts = new Set<string>([
|
||||
...instagramHosts,
|
||||
...odyseeHosts,
|
||||
...bitchuteHosts,
|
||||
...soundcloudHosts,
|
||||
...streamableHosts,
|
||||
...spotifyHosts,
|
||||
]);
|
||||
|
||||
@@ -55,12 +55,10 @@ const Markdown = ({ content }: MarkdownProps) => {
|
||||
|
||||
remarkPlugins.push([blockquoteToGreentext]);
|
||||
|
||||
const doubleNewlineContent = content?.replace(/\n/g, ' \n\n');
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
<ReactMarkdown
|
||||
children={doubleNewlineContent}
|
||||
children={content}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={[[rehypeSanitize, customSchema]]}
|
||||
components={{
|
||||
|
||||
@@ -12,6 +12,7 @@ import useWindowWidth from '../../hooks/use-window-width';
|
||||
import styles from './post.module.css';
|
||||
import Markdown from '../markdown';
|
||||
import CommentMedia from '../comment-media';
|
||||
import { canEmbed } from '../embed';
|
||||
|
||||
interface PostProps {
|
||||
index?: number;
|
||||
@@ -35,6 +36,7 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
||||
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) + '(...)' : content;
|
||||
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const { type, url } = commentMediaInfo || {};
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
@@ -56,15 +58,15 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
||||
<span className={`${styles.hideButton} ${styles.hideThread}`} />
|
||||
</span>
|
||||
)}
|
||||
{commentMediaInfo?.url && (
|
||||
{url && (
|
||||
<div className={styles.file}>
|
||||
<div className={styles.fileText}>
|
||||
{t('link')}:{' '}
|
||||
<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.length > 30 ? url.slice(0, 30) + '...' : url}
|
||||
</a>{' '}
|
||||
({commentMediaInfo?.type})
|
||||
{!showThumbnail && (commentMediaInfo?.type === 'iframe' || commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'audio') && (
|
||||
({type})
|
||||
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
@@ -74,11 +76,20 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
{showThumbnail && !hasThumbnail && (
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
<span className={styles.closeMedia} onClick={() => setShowThumbnail(false)}>
|
||||
{t('open')}
|
||||
</span>
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
{(hasThumbnail || (!hasThumbnail && !showThumbnail)) && (
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isMobile={false}
|
||||
isOutOfFeed={isDescription || isRules} // virtuoso wrapper unneeded
|
||||
isReply={false}
|
||||
linkHeight={linkHeight}
|
||||
@@ -182,6 +193,8 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
||||
const authorRole = roles?.[address]?.role;
|
||||
|
||||
const commentMediaInfo = getCommentMediaInfo(reply);
|
||||
const { type, url } = commentMediaInfo || {};
|
||||
const embedUrl = url && new URL(url);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
@@ -231,14 +244,14 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{link && (
|
||||
{url && (
|
||||
<div className={styles.file}>
|
||||
<div className={styles.fileText}>
|
||||
{t('link')}:{' '}
|
||||
<a href={link} target='_blank' rel='noopener noreferrer'>
|
||||
{link.length > 30 ? link?.slice(0, 30) + '...' : link}
|
||||
</a>
|
||||
{!showThumbnail && (commentMediaInfo?.type === 'iframe' || commentMediaInfo?.type === 'video' || commentMediaInfo?.type === 'audio') && (
|
||||
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
@@ -248,11 +261,20 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
{showThumbnail && !hasThumbnail && embedUrl && canEmbed(embedUrl) && (
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
<span className={styles.closeMedia} onClick={() => setShowThumbnail(false)}>
|
||||
{t('open')}
|
||||
</span>
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
{(hasThumbnail || (!hasThumbnail && !showThumbnail)) && (
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isMobile={false}
|
||||
isReply={true}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
@@ -344,10 +366,9 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
{(hasThumbnail || link) && (
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isMobile={true}
|
||||
isOutOfFeed={isDescription || isRules} // virtuoso wrapper unneeded
|
||||
isReply={false}
|
||||
linkHeight={linkHeight}
|
||||
@@ -427,10 +448,9 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
|
||||
<span className={styles.replyToPost}>{shortCid}</span>
|
||||
</span>
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
{(hasThumbnail || link) && (
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
isMobile={true}
|
||||
isReply={false}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
|
||||
@@ -12,7 +12,7 @@ import SubplebbitRules from '../../components/subplebbit-rules';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const Subplebbit = () => {
|
||||
const Board = () => {
|
||||
const { t } = useTranslation();
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const subplebbitAddresses = useMemo(() => [subplebbitAddress], [subplebbitAddress]) as string[];
|
||||
@@ -58,7 +58,7 @@ const Subplebbit = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{createdAt && (
|
||||
{feed.length > 0 && (
|
||||
<>
|
||||
{rules && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
|
||||
{description && description.length > 0 && (
|
||||
@@ -82,4 +82,4 @@ const Subplebbit = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Subplebbit;
|
||||
export default Board;
|
||||
|
||||
@@ -12,60 +12,54 @@ import styles from './catalog.module.css';
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const useFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subplebbit: Subplebbit) => {
|
||||
const modifiedFeed = useMemo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { address, createdAt, description, rules, shortAddress, suggested, title } = subplebbit || {};
|
||||
const { avatarUrl } = suggested || {};
|
||||
|
||||
const feedWithDescriptionAndRules = useMemo(() => {
|
||||
if (!isFeedLoaded) {
|
||||
return []; // prevent rules and description from appearing while feed is loading
|
||||
}
|
||||
if (!subplebbit?.description && !subplebbit?.rules) {
|
||||
if (!description && !rules) {
|
||||
return feed;
|
||||
}
|
||||
const _feed = [...feed];
|
||||
if (subplebbit?.description) {
|
||||
if (description && description.length > 0) {
|
||||
_feed.unshift({
|
||||
isDescription: true,
|
||||
subplebbitAddress: subplebbit?.address,
|
||||
timestamp: subplebbit?.createdAt,
|
||||
subplebbitAddress: address,
|
||||
timestamp: createdAt,
|
||||
author: { displayName: '## Board Mods' },
|
||||
content: subplebbit?.description,
|
||||
link: subplebbit?.suggested?.avatarUrl,
|
||||
title: 'Welcome to ' + (subplebbit?.title || `p/${subplebbit?.shortAddress}`),
|
||||
content: description,
|
||||
link: avatarUrl,
|
||||
title: 'Welcome to ' + (title || `p/${shortAddress}`),
|
||||
pinned: true,
|
||||
locked: true,
|
||||
});
|
||||
}
|
||||
if (subplebbit?.rules) {
|
||||
if (rules && rules.length > 0) {
|
||||
_feed.unshift({
|
||||
isRules: true,
|
||||
subplebbitAddress: subplebbit?.address,
|
||||
timestamp: subplebbit?.createdAt,
|
||||
subplebbitAddress: address,
|
||||
timestamp: createdAt,
|
||||
author: { displayName: '## Board Mods' },
|
||||
content: subplebbit?.rules.map((rule: string, index: number) => `${index + 1}. ${rule}`).join('\n'),
|
||||
content: rules.map((rule: string, index: number) => `${index + 1}. ${rule}`).join('\n'),
|
||||
title: 'Rules',
|
||||
pinned: true,
|
||||
locked: true,
|
||||
});
|
||||
}
|
||||
return _feed;
|
||||
}, [
|
||||
feed,
|
||||
subplebbit?.description,
|
||||
subplebbit?.rules,
|
||||
subplebbit?.address,
|
||||
isFeedLoaded,
|
||||
subplebbit?.createdAt,
|
||||
subplebbit?.title,
|
||||
subplebbit?.shortAddress,
|
||||
subplebbit?.suggested?.avatarUrl,
|
||||
]);
|
||||
}, [feed, description, rules, address, isFeedLoaded, createdAt, title, shortAddress, avatarUrl]);
|
||||
|
||||
// Memoize rows calculation, ensuring it updates on changes to the modified feed or column count
|
||||
const rows = useMemo(() => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < modifiedFeed.length; i += columnCount) {
|
||||
rows.push(modifiedFeed.slice(i, i + columnCount));
|
||||
for (let i = 0; i < feedWithDescriptionAndRules.length; i += columnCount) {
|
||||
rows.push(feedWithDescriptionAndRules.slice(i, i + columnCount));
|
||||
}
|
||||
return rows;
|
||||
}, [modifiedFeed, columnCount]);
|
||||
}, [feedWithDescriptionAndRules, columnCount]);
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,9 @@ import { useRef } from 'react';
|
||||
import styles from './home.module.css';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Subplebbit, useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
interface HomeProps {
|
||||
subplebbits: (Subplebbit | undefined)[];
|
||||
}
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
|
||||
const isValidAddress = (address: string): boolean => {
|
||||
if (address.includes('/') || address.includes('\\') || address.includes(' ')) {
|
||||
@@ -69,7 +66,7 @@ const InfoBox = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Boards = ({ subplebbits }: HomeProps) => {
|
||||
const Boards = ({ subplebbits }: { subplebbits: (Subplebbit | undefined)[] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -83,8 +80,8 @@ const Boards = ({ subplebbits }: HomeProps) => {
|
||||
<h3>Default SFW</h3>
|
||||
<div className={styles.list}>
|
||||
{subplebbits
|
||||
.filter((subplebbit): subplebbit is Subplebbit => subplebbit !== undefined)
|
||||
.map((subplebbit) => {
|
||||
.filter((subplebbit: Subplebbit | undefined): subplebbit is Subplebbit => subplebbit !== undefined)
|
||||
.map((subplebbit: Subplebbit) => {
|
||||
const address = subplebbit.address;
|
||||
return (
|
||||
<div className={styles.subplebbit} key={address}>
|
||||
@@ -221,7 +218,10 @@ const Footer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Home = ({ subplebbits }: HomeProps) => {
|
||||
const Home = () => {
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Link to='/'>
|
||||
|
||||
Reference in New Issue
Block a user