mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+1
-1
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@capacitor/app": "1.1.1",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#63833e978ca25f9846544d32b9c67810af0daf19",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#00aabcce2276c671c0f4a371d459c93eb39c936a",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "13.0.0",
|
||||
"@testing-library/user-event": "13.2.1",
|
||||
|
||||
+25
-16
@@ -1,33 +1,39 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||
import { isHomeView } from './lib/utils/view-utils';
|
||||
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './app.module.css';
|
||||
import { Subplebbit as SubplebbitType, 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 Home from './views/home';
|
||||
import Settings from './views/settings';
|
||||
import Subplebbit from './views/subplebbit';
|
||||
import BoardNav from './components/board-nav';
|
||||
import BoardBanner from './components/board-banner';
|
||||
import { DesktopBoardButtons } from './components/board-buttons';
|
||||
import { MobileBoardButtons } from './components/board-buttons';
|
||||
import BoardStats from './components/board-stats';
|
||||
import PostForm from './components/post-form';
|
||||
import { MobileBoardButtons } from './components/board-buttons';
|
||||
import { DesktopBoardButtons } from './components/board-buttons';
|
||||
|
||||
const BoardLayout = () => {
|
||||
interface BoardLayoutProps {
|
||||
subplebbits: (SubplebbitType | undefined)[];
|
||||
}
|
||||
|
||||
const BoardLayout = ({ subplebbits }: BoardLayoutProps) => {
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const subplebbit = subplebbits.find((s) => s?.address === subplebbitAddress);
|
||||
const { address, createdAt, title } = subplebbit || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
{subplebbitAddress && (
|
||||
{address && (
|
||||
<>
|
||||
<BoardNav address={subplebbitAddress} />
|
||||
<BoardBanner title={subplebbit.title} address={subplebbitAddress} />
|
||||
<MobileBoardButtons address={subplebbitAddress} />
|
||||
<PostForm address={subplebbitAddress} />
|
||||
<BoardStats address={subplebbitAddress} createdAt={subplebbit.createdAt} />
|
||||
<DesktopBoardButtons address={subplebbitAddress} />
|
||||
<BoardNav address={address} subplebbits={subplebbits} />
|
||||
<BoardBanner title={title} address={address} />
|
||||
<MobileBoardButtons address={address} />
|
||||
<PostForm address={address} />
|
||||
<BoardStats address={address} createdAt={createdAt} />
|
||||
<DesktopBoardButtons address={address} />
|
||||
</>
|
||||
)}
|
||||
<Outlet />
|
||||
@@ -40,6 +46,9 @@ const App = () => {
|
||||
const location = useLocation();
|
||||
const isInHomeView = isHomeView(location.pathname);
|
||||
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.forEach((className) => document.body.classList.remove(className));
|
||||
document.body.classList.add(theme);
|
||||
@@ -55,9 +64,9 @@ const App = () => {
|
||||
return (
|
||||
<div className={`${styles.app} ${isInHomeView ? 'yotsuba' : theme}`}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route element={<BoardLayout />}>
|
||||
<Route path='/p/:subplebbitAddress' element={<Subplebbit />} />
|
||||
<Route path='/' element={<Home subplebbits={subplebbits} />} />
|
||||
<Route element={<BoardLayout subplebbits={subplebbits} />}>
|
||||
<Route path='/p/:subplebbitAddress' element={<Subplebbit subplebbits={subplebbits} />} />
|
||||
</Route>
|
||||
<Route path='/settings' element={<Settings />} />
|
||||
</Routes>
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
font-size: var(--boardnav-mobile-font-size);
|
||||
}
|
||||
|
||||
.boardNavMobile .pageJump a {
|
||||
color: var(--button-text-color-mobile);
|
||||
text-decoration: var(--button-text-decoration-mobile);
|
||||
}
|
||||
|
||||
.boardNavMobile .pageJump a:hover {
|
||||
color: var(--button-text-color-hover-mobile);
|
||||
text-decoration: var(--button-text-decoration-hover-mobile);
|
||||
}
|
||||
|
||||
.boardNavMobile strong {
|
||||
text-transform: capitalize;
|
||||
padding-right: 5px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useDefaultSubplebbits from '../../hooks/use-default-subplebbits';
|
||||
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './board-nav.module.css';
|
||||
|
||||
interface BoardNavProps {
|
||||
address?: string | undefined;
|
||||
subplebbits?: any;
|
||||
subplebbits: (Subplebbit | undefined)[];
|
||||
currentSubplebbit?: string | undefined;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,19 @@ const BoardNavDesktop = ({ subplebbits }: BoardNavProps) => {
|
||||
<div className={styles.boardNavDesktop}>
|
||||
<span className={styles.boardList}>
|
||||
[
|
||||
{subplebbits.map((subplebbit: any, index: number) => (
|
||||
<span key={subplebbit.address}>
|
||||
{index === 0 ? null : ' '}
|
||||
<Link to={`/p/${subplebbit.address}`} title={subplebbit.title || ''}>
|
||||
{subplebbit.address.includes('.') ? subplebbit.address : subplebbit.title || subplebbit.address.slice(0, 10).concat('...')}
|
||||
</Link>
|
||||
{index !== subplebbits.length - 1 ? ' /' : null}
|
||||
</span>
|
||||
))}
|
||||
{subplebbits.map((subplebbit: any, index: number) => {
|
||||
const address = subplebbit?.address || '';
|
||||
const title = subplebbit?.title || '';
|
||||
return (
|
||||
<span key={index}>
|
||||
{index === 0 ? null : ' '}
|
||||
<Link to={`/p/${address}`} title={title || ''}>
|
||||
{address.includes('.') ? address : title || address.slice(0, 10).concat('...')}
|
||||
</Link>
|
||||
{index !== subplebbits.length - 1 ? ' /' : null}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
]
|
||||
</span>
|
||||
<span className={styles.navTopRight}>
|
||||
@@ -39,7 +43,7 @@ const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
const navigate = useNavigate();
|
||||
const displaySubplebbitAddress = currentSubplebbit && currentSubplebbit.length > 30 ? currentSubplebbit.slice(0, 30).concat('...') : currentSubplebbit;
|
||||
|
||||
const currentSubplebbitIsInList = subplebbits.some((subplebbit: any) => subplebbit.address === currentSubplebbit);
|
||||
const currentSubplebbitIsInList = subplebbits.some((subplebbit: any) => subplebbit?.address === currentSubplebbit);
|
||||
|
||||
const boardSelect = (
|
||||
<select value={currentSubplebbit || 'all'} onChange={(e) => navigate(`/p/${e.target.value}`)}>
|
||||
@@ -47,10 +51,11 @@ const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
<option value='all'>{t('all')}</option>
|
||||
<option value='subscriptions'>{t('subscriptions')}</option>
|
||||
{subplebbits.map((subplebbit: any, index: number) => {
|
||||
const { address, title } = subplebbit;
|
||||
const subplebbitAddress = address.includes('.') ? address : title || address.slice(0, 10).concat('...');
|
||||
const address = subplebbit?.address || '';
|
||||
const title = subplebbit?.title || '';
|
||||
const subplebbitAddress = address?.includes('.') ? address : title || address?.slice(0, 10).concat('...');
|
||||
return (
|
||||
<option key={index} value={subplebbit.address}>
|
||||
<option key={index} value={address}>
|
||||
{subplebbitAddress}
|
||||
</option>
|
||||
);
|
||||
@@ -72,13 +77,11 @@ const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BoardNav = ({ address }: BoardNavProps) => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
|
||||
const BoardNav = ({ address, subplebbits }: BoardNavProps) => {
|
||||
return (
|
||||
<>
|
||||
<BoardNavDesktop subplebbits={defaultSubplebbits} />
|
||||
<BoardNavMobile subplebbits={defaultSubplebbits} currentSubplebbit={address} />
|
||||
<BoardNavDesktop subplebbits={subplebbits} />
|
||||
<BoardNavMobile subplebbits={subplebbits} currentSubplebbit={address} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
.content hr {
|
||||
min-width: 468px;
|
||||
margin-left: -4px;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.blotter {
|
||||
min-width: 468px;
|
||||
margin: auto;
|
||||
line-height: 1;
|
||||
margin-top: -2px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.blotter tr {
|
||||
|
||||
@@ -24,7 +24,13 @@ const BoardStats = ({ address, createdAt }: BoardStatsProps) => {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<table className={styles.blotter}>
|
||||
<hr />
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
{showStats && (
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.markdown a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: none;
|
||||
text-decoration: var(--post-content-link-text-decoration);
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: none;
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.markdown ol {
|
||||
|
||||
@@ -4,7 +4,6 @@ import ReactMarkdown from 'react-markdown';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import supersub from 'remark-supersub';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
@@ -38,7 +37,7 @@ const blockquoteToGreentext = () => (tree: any) => {
|
||||
const Markdown = ({ content }: MarkdownProps) => {
|
||||
const remarkPlugins: any[] = [[supersub]];
|
||||
|
||||
if (content.length <= MAX_LENGTH_FOR_GFM) {
|
||||
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
|
||||
remarkPlugins.push([remarkGfm, { singleTilde: false }]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { Thumbnail, Media } from './media';
|
||||
@@ -0,0 +1,29 @@
|
||||
.thumbnailBig {
|
||||
background-color: var(--post-thumbnail-background-color);
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
margin: 3px 20px 5px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbnailSmall {
|
||||
background-color: var(--post-thumbnail-background-color);
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
margin: 3px 10px 5px 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbnailBig img, .thumbnailBig video {
|
||||
max-width: 250px;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.thumbnailSmall img, .thumbnailSmall video {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import styles from './media.module.css';
|
||||
import { CommentMediaInfo } from '../../lib/utils/media-utils';
|
||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||
|
||||
interface ThumbnailProps {
|
||||
commentMediaInfo?: CommentMediaInfo;
|
||||
isMobile: boolean;
|
||||
isReply: boolean;
|
||||
linkHeight?: number;
|
||||
linkWidth?: number;
|
||||
toggleExpanded?: () => void;
|
||||
}
|
||||
|
||||
const ThumbnailBig = ({ style, children }: { style: React.CSSProperties; children: React.ReactNode }) => (
|
||||
<span className={styles.thumbnailBig} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
const ThumbnailSmall = ({ style, children }: { style: React.CSSProperties; children: React.ReactNode }) => (
|
||||
<span className={styles.thumbnailSmall} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const Thumbnail = ({ commentMediaInfo, isMobile, isReply, linkHeight, linkWidth }: ThumbnailProps) => {
|
||||
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`;
|
||||
displayHeight = `${linkHeight * scale}px`;
|
||||
} else {
|
||||
displayWidth = `${maxThumbnailSize}px`;
|
||||
displayHeight = `${maxThumbnailSize}px`;
|
||||
}
|
||||
|
||||
let mediaComponent: React.ReactNode = null;
|
||||
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(commentMediaInfo?.type === 'gif' ? commentMediaInfo.url : undefined);
|
||||
if (commentMediaInfo?.type === 'image') {
|
||||
mediaComponent = <img src={commentMediaInfo.url} alt='' />;
|
||||
} else if (commentMediaInfo?.type === 'video') {
|
||||
mediaComponent = commentMediaInfo.thumbnail ? <img src={commentMediaInfo.thumbnail} alt='' /> : <video src={`${commentMediaInfo.url}#t=0.001`} />;
|
||||
} else if (commentMediaInfo?.type === 'webpage') {
|
||||
mediaComponent = <img src={commentMediaInfo.thumbnail} alt='' />;
|
||||
} else if (commentMediaInfo?.type === 'iframe') {
|
||||
mediaComponent = iframeThumbnail ? <img src={iframeThumbnail} alt='' /> : null;
|
||||
} else if (commentMediaInfo?.type === 'gif' && gifFrameUrl) {
|
||||
mediaComponent = <img src={gifFrameUrl} alt='' />;
|
||||
}
|
||||
|
||||
const thumbnailStyle = { '--width': displayWidth, '--height': displayHeight } as React.CSSProperties;
|
||||
|
||||
return isMobile || isReply ? (
|
||||
<ThumbnailSmall style={thumbnailStyle}>{mediaComponent}</ThumbnailSmall>
|
||||
) : (
|
||||
<ThumbnailBig style={thumbnailStyle}>{mediaComponent}</ThumbnailBig>
|
||||
);
|
||||
};
|
||||
|
||||
export const Media = () => {
|
||||
return <></>;
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
.postFormButtonMobile {
|
||||
text-align: center;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -6,17 +6,50 @@
|
||||
margin: 0; /* margin bugs Virtuoso scrolling */
|
||||
}
|
||||
|
||||
.hrWrapper {
|
||||
padding-top: 0.5em; /* instead of using margin, wrap with padding */
|
||||
padding-bottom: 0.5em;
|
||||
.postDesktop .hrWrapper {
|
||||
padding: 0.5em 0; /* instead of using margin, wrap with padding */
|
||||
}
|
||||
|
||||
.postDesktop .link a {
|
||||
.threadHideButtonWrapper {
|
||||
padding-right: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.postDesktop .threadHideButton {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
image-rendering: pixelated;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* clearfix to the container of the floatied elements to contain them */
|
||||
.postDesktop::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.postDesktop .hideThread {
|
||||
background-image: var(--post-hide-button-background-image);
|
||||
}
|
||||
|
||||
.postDesktop .unhideThread {
|
||||
background-image: var(--post-unhide-button-background-image);
|
||||
}
|
||||
|
||||
.postDesktop .fileThumb {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.postDesktop .fileText a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-link-text-decoration);
|
||||
}
|
||||
|
||||
.postDesktop .link a:hover {
|
||||
.postDesktop .fileText a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-link-text-decoration-hover);
|
||||
}
|
||||
@@ -75,12 +108,12 @@
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
}
|
||||
|
||||
.postMenuBtn {
|
||||
.postDesktop .postMenuBtn {
|
||||
padding-left: 5px;
|
||||
color: var(--post-menu-button-color);
|
||||
}
|
||||
|
||||
.postMenuBtn:hover {
|
||||
.postDesktop .postMenuBtn:hover {
|
||||
cursor: pointer;
|
||||
color: var(--post-menu-button-color-hover);
|
||||
}
|
||||
@@ -89,8 +122,141 @@
|
||||
padding: 1em 40px;
|
||||
}
|
||||
|
||||
.postDesktop .abbr {
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
font-size: var(--post-mobile-abbr-font-size);
|
||||
}
|
||||
|
||||
.postDesktop .abbr a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-content-link-text-decoration);
|
||||
}
|
||||
|
||||
.postDesktop .abbr a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.postMobile hr {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.postMobile .thread {
|
||||
border-top: none;
|
||||
margin: 0;
|
||||
clear: both;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.postMobile .postContainer {
|
||||
background-color: var(--post-mobile-background-color);
|
||||
}
|
||||
|
||||
.postMobile .postOp {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.postMobile .postInfo {
|
||||
overflow: hidden;
|
||||
border-bottom: var(--post-mobile-info-border-bottom);
|
||||
background-color: var(--post-mobile-info-background-color);
|
||||
padding: 5px;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.postMobile .postMenuBtn {
|
||||
color: var(--post-mobile-menu-button-color);
|
||||
line-height: 1em;
|
||||
width: 10px;
|
||||
text-align: center;
|
||||
transition: transform 0.1s;
|
||||
transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
float: left;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.postMobile .nameBlock {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
.postMobile .nameBlock .name {
|
||||
color: var(--post-mobile-name-text-color);
|
||||
font-weight: var(--post-mobile-name-font-weight);
|
||||
}
|
||||
|
||||
.postMobile .nameBlock .subject {
|
||||
color: var(--post-mobile-subject-text-color);
|
||||
font-weight: var(--post-mobile-subject-font-weight);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.postMobile .dateTimePostNum {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.postMobile .fileThumb {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.postMobile .fileThumb .fileInfo {
|
||||
padding-bottom: 5px;
|
||||
text-align: center;
|
||||
color: var(--post-mobile-file-info-text-color);
|
||||
font-size: 9pt;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.postMobile .postMessage {
|
||||
padding: 10px;
|
||||
font-size: var(--post-mobile-content-font-size);
|
||||
}
|
||||
|
||||
.postMobile .postLink {
|
||||
clear: both;
|
||||
background-color: var(--post-mobile-link-background-color);
|
||||
border-top: var(--post-mobile-link-border-top);
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.postMobile .postLink .info {
|
||||
color: var(--post-mobile-link-info-text-color);
|
||||
}
|
||||
|
||||
.postMobile .abbr {
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
font-size: var(--post-mobile-abbr-font-size);
|
||||
}
|
||||
|
||||
.postMobile .abbr a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-content-link-text-decoration);
|
||||
|
||||
}
|
||||
|
||||
.postMobile .abbr a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.postDesktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.postMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
+170
-56
@@ -1,78 +1,192 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './post.module.css';
|
||||
import useReplies from '../../hooks/use-replies';
|
||||
import { getLinkMediaInfoMemoized } from '../../lib/utils/media-utils';
|
||||
import { getCommentMediaInfoMemoized, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getFormattedDate } from '../../lib/utils/time-utils';
|
||||
import useReplies from '../../hooks/use-replies';
|
||||
import styles from './post.module.css';
|
||||
import Markdown from '../markdown';
|
||||
import { Thumbnail } from '../media';
|
||||
|
||||
const Post = ({ post }: Comment) => {
|
||||
import { countLinksInCommentReplies } from '../../lib/utils/comment-utils';
|
||||
|
||||
const ReplyDesktop = ({ reply }: Comment) => {
|
||||
const { content, link, linkHeight, linkWidth, shortCid, subplebbitAddress, timestamp } = reply || {};
|
||||
return (
|
||||
<div className={styles.replyDesktop}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostDesktop = ({ post }: Comment) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, link, locked, pinned, postCid, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
||||
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, postCid, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
||||
const { displayName, shortAddress } = author || {};
|
||||
const replies = useReplies(post);
|
||||
const linkMediaInfo = getLinkMediaInfoMemoized(link);
|
||||
const displayTitle = title && title.length > 75 ? title.slice(0, 75) + '...' : title;
|
||||
const displayContent = content && content.length > 1000 ? content.slice(0, 1000) + '(...)' : content;
|
||||
|
||||
const commentMediaInfo = getCommentMediaInfoMemoized(post);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
<div className={styles.postDesktop}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
<div className={styles.postDesktop}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
<span className={styles.threadHideButtonWrapper}>
|
||||
<span className={`${styles.threadHideButton} ${styles.hideThread}`} />
|
||||
</span>
|
||||
{commentMediaInfo?.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>{' '}
|
||||
({commentMediaInfo?.type})
|
||||
</div>
|
||||
{linkMediaInfo?.url && (
|
||||
<div className={styles.link}>
|
||||
{t('link')}:{' '}
|
||||
<a href={linkMediaInfo?.url} target='_blank' rel='noopener noreferrer'>
|
||||
{linkMediaInfo.url.length > 30 ? linkMediaInfo?.url.slice(0, 30) + '...' : linkMediaInfo?.url}
|
||||
</a>{' '}
|
||||
({linkMediaInfo?.type})
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
<span className={styles.fileThumb}>
|
||||
<Thumbnail commentMediaInfo={commentMediaInfo} isMobile={false} isReply={false} linkHeight={linkHeight} linkWidth={linkWidth} />
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.postInfo}>
|
||||
{title && <span className={styles.subject}>{title.length > 75 ? title.slice(0, 75) + '...' : title}</span>}{' '}
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={styles.name}>{displayName || 'Anonymous'} </span>
|
||||
<span className={styles.userAddress}>(u/{shortAddress}) </span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.postInfo}>
|
||||
{title && <span className={styles.subject}>{displayTitle}</span>}{' '}
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={styles.name}>{displayName || 'Anonymous'} </span>
|
||||
<span className={styles.userAddress}>(u/{shortAddress}) </span>
|
||||
</span>
|
||||
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
|
||||
<span className={styles.postNum}>
|
||||
<span className={styles.postNumLink}>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className={styles.linkToPost} title={t('link_to_post')}>
|
||||
c/
|
||||
</Link>
|
||||
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
||||
{shortCid}
|
||||
</span>
|
||||
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
|
||||
<span className={styles.postNum}>
|
||||
<span className={styles.postNumLink}>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className={styles.linkToPost} title={t('link_to_post')}>
|
||||
c/
|
||||
</Link>
|
||||
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
||||
{shortCid}
|
||||
</span>
|
||||
</span>
|
||||
{pinned && (
|
||||
<span className={styles.stickyIconWrapper}>
|
||||
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
|
||||
</span>
|
||||
)}
|
||||
{locked && (
|
||||
<span className={styles.closedIconWrapper}>
|
||||
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.replyButton}>
|
||||
[<Link to={`/p/${subplebbitAddress}/c/${postCid}`}>{t('reply')}</Link>]
|
||||
</span>
|
||||
</span>
|
||||
{pinned && (
|
||||
<span className={styles.stickyIconWrapper}>
|
||||
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
|
||||
</span>
|
||||
<span className={styles.postMenuBtn}>▶</span>
|
||||
</div>
|
||||
{content && (
|
||||
<div className={styles.postMessage}>
|
||||
<blockquote>
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{locked && (
|
||||
<span className={styles.closedIconWrapper}>
|
||||
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.replyButton}>
|
||||
[<Link to={`/p/${subplebbitAddress}/c/${postCid}`}>{t('reply')}</Link>]
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.postMenuBtn}>▶</span>
|
||||
</div>
|
||||
{content && (
|
||||
<blockquote className={styles.postMessage}>
|
||||
<Markdown content={displayContent} />
|
||||
{content.length > 1000 && (
|
||||
<span className={styles.abbr}>
|
||||
<br />
|
||||
Comment too long. <Link to={`/p/${subplebbitAddress}/c/${cid}`}>Click here</Link> to view the full text.
|
||||
</span>
|
||||
)}
|
||||
</blockquote>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostMobile = ({ post }: Comment) => {
|
||||
const { author, cid, content, link, linkHeight, linkWidth, replyCount, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
||||
const { address, displayName, shortAddress } = author || {};
|
||||
const linkCount = countLinksInCommentReplies(post);
|
||||
const displayTitle = title && title.length > 30 ? title.slice(0, 30) + '(...)' : title;
|
||||
const displayContent = content && content.length > 1000 ? content.slice(0, 1000) : content;
|
||||
|
||||
const commentMediaInfo = getCommentMediaInfoMemoized(post);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
<div className={styles.postMobile}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
<div className={styles.postOp}>
|
||||
<div className={styles.postInfo}>
|
||||
<span className={styles.postMenuBtn} title='Post menu'>
|
||||
...
|
||||
</span>
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={styles.name}>{displayName || 'Anonymous'} </span>
|
||||
<span className={styles.address}>(u/{shortAddress || address.slice(0, 12) + '...'})</span>
|
||||
{title && (
|
||||
<>
|
||||
<br />
|
||||
<span className={styles.subject}>{displayTitle}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.dateTimePostNum}>
|
||||
{getFormattedDate(timestamp)} <span className={styles.linkToPost}>c/</span>
|
||||
<span className={styles.replyToPost}>{shortCid}</span>
|
||||
</span>
|
||||
</div>
|
||||
{hasThumbnail && (
|
||||
<span className={styles.fileThumb}>
|
||||
<Thumbnail commentMediaInfo={commentMediaInfo} isMobile={true} isReply={false} linkHeight={linkHeight} linkWidth={linkWidth} />
|
||||
{commentMediaInfo?.type && <div className={styles.fileInfo}>{commentMediaInfo.type}</div>}
|
||||
</span>
|
||||
)}
|
||||
{content && (
|
||||
<blockquote className={`${styles.postMessage} ${styles.clampLines}`}>
|
||||
<Markdown content={displayContent} />
|
||||
{content.length > 1000 && (
|
||||
<span className={styles.abbr}>
|
||||
<br />
|
||||
Comment too long. <Link to={`/p/${subplebbitAddress}/c/${cid}`}>Click here</Link> to view the full text.
|
||||
</span>
|
||||
)}
|
||||
</blockquote>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.postLink}>
|
||||
<span className={styles.info}>
|
||||
{replyCount} Replies
|
||||
{linkCount > 0 && ` / ${linkCount} Links`}
|
||||
</span>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className='button'>
|
||||
View Thread
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Post = ({ post }: Comment) => {
|
||||
const replies = useReplies(post);
|
||||
|
||||
return (
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
<PostDesktop post={post} />
|
||||
<PostMobile post={post} />
|
||||
</div>
|
||||
{replies.map((reply) => (
|
||||
<div key={reply.cid} className={styles.replyContainer}>
|
||||
<ReplyDesktop reply={reply} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const fetchImage = (url: string): Promise<ArrayBuffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', url, true);
|
||||
request.responseType = 'arraybuffer';
|
||||
request.onloadend = () => {
|
||||
if (request.response !== undefined && (request.status === 200 || request.status === 304)) {
|
||||
resolve(request.response);
|
||||
} else {
|
||||
reject(new Error(`XMLHttpRequest, ${request.statusText}`));
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
};
|
||||
|
||||
export const readImage = (file: File): Promise<ArrayBuffer> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as ArrayBuffer);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
const parseGif = async (buf: ArrayBuffer): Promise<Blob> => {
|
||||
const image = new Image();
|
||||
await new Promise((resolve) => {
|
||||
image.src = URL.createObjectURL(new Blob([buf]));
|
||||
image.onload = resolve;
|
||||
});
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx === null) throw new Error('Canvas Context null');
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
return await new Promise((resolve, reject) =>
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob === null) {
|
||||
reject('Canvas Blob null');
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const useFetchGifFirstFrame = (url: string | undefined) => {
|
||||
const [frameUrl, setFrameUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) {
|
||||
setFrameUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
|
||||
const fetchFrame = async () => {
|
||||
try {
|
||||
const blob = typeof url === 'string' ? await parseGif(await fetchImage(url)) : await parseGif(await readImage(url as File));
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
if (isActive) setFrameUrl(objectUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to load GIF frame:', error);
|
||||
if (isActive) setFrameUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFrame();
|
||||
|
||||
// Cleanup function to avoid setting state on unmounted component
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return frameUrl;
|
||||
};
|
||||
|
||||
export default useFetchGifFirstFrame;
|
||||
@@ -39,6 +39,11 @@ hr {
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--button-text-color-hover-mobile);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
export const countLinksInCommentReplies = (comment: Comment) => {
|
||||
let linkCount = 0;
|
||||
|
||||
if (comment.replyCount > 0) {
|
||||
for (let reply of comment.replies.pages.topAll.comments) {
|
||||
if (reply.link) {
|
||||
linkCount++;
|
||||
}
|
||||
|
||||
if (reply.replyCount > 0) {
|
||||
linkCount += countLinksInCommentReplies(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linkCount;
|
||||
};
|
||||
+80
-7
@@ -65,8 +65,12 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: red;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: none;
|
||||
--post-content-link-text-decoration-hover: none;
|
||||
|
||||
/* post */
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/post-expand-minus-red.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/post-expand-plus-red.png");
|
||||
--post-subject-text-color: #cc1105;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -75,6 +79,25 @@
|
||||
--post-menu-button-color-hover: red;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post mobile */
|
||||
--post-mobile-background-color: #f5e9e1;
|
||||
--post-mobile-info-border-bottom: 1px solid #d9bfb7;
|
||||
--post-mobile-info-background-color: #ead6ca;
|
||||
--post-mobile-menu-button-color: #00e;
|
||||
--post-mobile-name-text-color: #117743;
|
||||
--post-mobile-name-font-weight: 700;
|
||||
--post-mobile-subject-text-color: #cc1105;
|
||||
--post-mobile-subject-font-weight: 700;
|
||||
--post-mobile-link-background-color: #ead6ca;
|
||||
--post-mobile-link-border-top: 1px solid #d9bfb7;
|
||||
--post-mobile-link-info-text-color: #800;
|
||||
--post-mobile-abbr-text-color: #707070;
|
||||
--post-mobile-abbr-font-size: 10pt;
|
||||
--post-mobile-file-info-text-color: #707070;
|
||||
--post-mobile-content-font-size: 11pt;
|
||||
|
||||
--post-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
--post-form-toggle-font-weight: 700;
|
||||
@@ -148,8 +171,12 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #d00;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: none;
|
||||
--post-content-link-text-decoration-hover: none;
|
||||
|
||||
/* post */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/post-expand-minus-blue.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/post-expand-plus-blue.png");
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -157,6 +184,21 @@
|
||||
--post-menu-button-color: #34345c;
|
||||
--post-menu-button-color-hover: #d00;
|
||||
--post-greentext-color: #789922;
|
||||
--post-mobile-background-color: #d6daf0;
|
||||
--post-mobile-info-border-bottom: 1px solid #b7c5d9;
|
||||
--post-mobile-info-background-color: #c9cde8;
|
||||
--post-mobile-menu-button-color: #34345c;
|
||||
--post-mobile-name-text-color: purple;
|
||||
--post-mobile-name-font-weight: 700;
|
||||
--post-mobile-subject-text-color: #0f0c5d;
|
||||
--post-mobile-subject-font-weight: 700;
|
||||
--post-mobile-link-background-color: #c9cde8;
|
||||
--post-mobile-link-border-top: 1px solid #b7c5d9;
|
||||
--post-mobile-link-info-text-color: #34345c;
|
||||
--post-mobile-abbr-text-color: #707070;
|
||||
--post-mobile-abbr-font-size: 10pt;
|
||||
--post-mobile-content-font-size: 11pt;
|
||||
--post-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -193,12 +235,15 @@
|
||||
--button-text-decoration: underline;
|
||||
|
||||
/* mobile buttons */
|
||||
--button-font-size-mobile: 11pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #800;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade.png");
|
||||
--button-background-color-mobile: #f0e0d6;
|
||||
--button-border-mobile: 1px solid #c0a69d;
|
||||
--button-font-size-mobile: 12pt;
|
||||
--button-font-weight-mobile: unset;
|
||||
--button-text-color-mobile: #00e;
|
||||
--button-background-image-mobile: none;
|
||||
--button-background-color-mobile: none;
|
||||
--button-border-mobile: none;
|
||||
--button-text-decoration-mobile: underline;
|
||||
--button-text-color-hover-mobile: red;
|
||||
--button-text-decoration-hover-mobile: underline;
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #fed;
|
||||
@@ -208,6 +253,8 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: red;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: underline;
|
||||
--post-content-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #cc1105;
|
||||
@@ -217,6 +264,21 @@
|
||||
--post-menu-button-color: #000080;
|
||||
--post-menu-button-color-hover: red;
|
||||
--post-greentext-color: #789922;
|
||||
--post-mobile-background-color: #f5e9e1;
|
||||
--post-mobile-info-border-bottom: 1px solid #d9bfb7;
|
||||
--post-mobile-info-background-color: #ead6ca;
|
||||
--post-mobile-menu-button-color: #00e;
|
||||
--post-mobile-name-text-color: #117743;
|
||||
--post-mobile-name-font-weight: 700;
|
||||
--post-mobile-subject-text-color: #cc1105;
|
||||
--post-mobile-subject-font-weight: 700;
|
||||
--post-mobile-link-background-color: #ead6ca;
|
||||
--post-mobile-link-border-top: 1px solid #d9bfb7;
|
||||
--post-mobile-link-info-text-color: #800;
|
||||
--post-mobile-abbr-text-color: #707070;
|
||||
--post-mobile-abbr-font-size: 12pt;
|
||||
--post-mobile-content-font-size: 13pt;
|
||||
--post-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -273,6 +335,8 @@
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade-blue.png");
|
||||
--button-background-color-mobile: #d6daf0;
|
||||
--button-border-mobile: 1px solid #b7c5d9;
|
||||
--button-text-color-hover-mobile: red;
|
||||
--button-text-decoration-hover-mobile: underline;
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
@@ -282,6 +346,8 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #d00;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: none;
|
||||
--post-content-link-text-decoration-hover: none;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
@@ -291,6 +357,7 @@
|
||||
--post-menu-button-color: #34345c;
|
||||
--post-menu-button-color-hover: #d00;
|
||||
--post-greentext-color: #789922;
|
||||
--post-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -354,6 +421,8 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #5f89ac;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: none;
|
||||
--post-content-link-text-decoration-hover: none;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #b294bb;
|
||||
@@ -363,6 +432,7 @@
|
||||
--post-menu-button-color: #5F89AC;
|
||||
--post-menu-button-color-hover: #81a2be;
|
||||
--post-greentext-color: #b5bd68;
|
||||
--post-thumbnail-background-color: rgba(255, 255, 255, 0.01);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -421,6 +491,8 @@
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #f30;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
--post-content-link-text-decoration: none;
|
||||
--post-content-link-text-decoration-hover: none;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #111;
|
||||
@@ -430,6 +502,7 @@
|
||||
--post-menu-button-color: #FF6600;
|
||||
--post-menu-button-color-hover: #FF3300;
|
||||
--post-greentext-color: #789922;
|
||||
--post-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
|
||||
+18
-10
@@ -2,9 +2,13 @@ import { useRef } from 'react';
|
||||
import styles from './home.module.css';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
interface HomeProps {
|
||||
subplebbits: (Subplebbit | undefined)[];
|
||||
}
|
||||
|
||||
const isValidAddress = (address: string): boolean => {
|
||||
if (address.includes('/') || address.includes('\\') || address.includes(' ')) {
|
||||
return false;
|
||||
@@ -65,9 +69,8 @@ const InfoBox = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Boards = () => {
|
||||
const Boards = ({ subplebbits }: HomeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
@@ -79,11 +82,16 @@ const Boards = () => {
|
||||
<div className={styles.column}>
|
||||
<h3>Default SFW</h3>
|
||||
<div className={styles.list}>
|
||||
{defaultSubplebbitAddresses.map((address) => (
|
||||
<div className={styles.subplebbit} key={address}>
|
||||
<Link to={`/p/${address}`}>{address}</Link>
|
||||
</div>
|
||||
))}
|
||||
{subplebbits
|
||||
.filter((subplebbit): subplebbit is Subplebbit => subplebbit !== undefined)
|
||||
.map((subplebbit) => {
|
||||
const address = subplebbit.address;
|
||||
return (
|
||||
<div className={styles.subplebbit} key={address}>
|
||||
<Link to={`/p/${address}`}>{address}</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
@@ -193,7 +201,7 @@ const Footer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
const Home = ({ subplebbits }: HomeProps) => {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Link to='/'>
|
||||
@@ -203,7 +211,7 @@ const Home = () => {
|
||||
</Link>
|
||||
<SearchBar />
|
||||
<InfoBox />
|
||||
<Boards />
|
||||
<Boards subplebbits={subplebbits} />
|
||||
<PopularThreads />
|
||||
<Stats />
|
||||
<Footer />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Subplebbit as SubplebbitType, useFeed } from '@plebbit/plebbit-react-hooks';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './subplebbit.module.css';
|
||||
@@ -9,12 +9,18 @@ import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import Post from '../../components/post';
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const Subplebbit = () => {
|
||||
interface SubplebbitProps {
|
||||
subplebbits: (SubplebbitType | undefined)[];
|
||||
}
|
||||
|
||||
const Subplebbit = ({ subplebbits }: SubplebbitProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const subplebbit = subplebbits.find((s) => s?.address === subplebbitAddress);
|
||||
const subplebbitAddresses = useMemo(() => [subplebbitAddress], [subplebbitAddress]) as string[];
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { createdAt, description, roles, rules, shortAddress, state, title, updatedAt, settings } = subplebbit || {};
|
||||
const { shortAddress, state, title } = subplebbit || {};
|
||||
|
||||
const sortType = 'active';
|
||||
const { feed, hasMore, loadMore } = useFeed({ subplebbitAddresses, sortType });
|
||||
const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading');
|
||||
@@ -54,7 +60,7 @@ const Subplebbit = () => {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 600 }}
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={feed?.length || 0}
|
||||
data={feed}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
|
||||
@@ -3413,6 +3413,22 @@
|
||||
dependencies:
|
||||
debug "4.3.4"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#00aabcce2276c671c0f4a371d459c93eb39c936a":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#00aabcce2276c671c0f4a371d459c93eb39c936a"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#073d5f501ccf9521942d93f6522d1125058c196a"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
localforage "1.10.0"
|
||||
lodash.isequal "4.5.0"
|
||||
memoizee "0.4.15"
|
||||
quick-lru "5.1.1"
|
||||
uint8arrays "3.1.1"
|
||||
uuid "8.3.2"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#63833e978ca25f9846544d32b9c67810af0daf19":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#63833e978ca25f9846544d32b9c67810af0daf19"
|
||||
|
||||
Reference in New Issue
Block a user