mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf: isolate IPFS state updates in board and catalog views to prevent excessive re-renders
This commit is contained in:
+222
-100
@@ -22,6 +22,149 @@ import { Post } from '../post';
|
|||||||
|
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
|
interface BoardFooterProps {
|
||||||
|
subplebbitAddresses: string[];
|
||||||
|
hasMore: boolean;
|
||||||
|
combinedFeedLength: number;
|
||||||
|
subplebbitAddressesWithNewerPosts: string[];
|
||||||
|
onNewerPostsClick: () => void;
|
||||||
|
isInAllView: boolean;
|
||||||
|
isInSubscriptionsView: boolean;
|
||||||
|
isInModView: boolean;
|
||||||
|
showMorePostsSuggestion: boolean;
|
||||||
|
feedLength: number;
|
||||||
|
weeklyFeedLength: number;
|
||||||
|
monthlyFeedLength: number;
|
||||||
|
yearlyFeedLength: number;
|
||||||
|
boardPath: string | undefined;
|
||||||
|
currentTimeFilterName: string | undefined;
|
||||||
|
subplebbitState: string | undefined;
|
||||||
|
subscriptionsLength: number;
|
||||||
|
accountSubplebbitAddressesLength: number;
|
||||||
|
blocked: boolean;
|
||||||
|
onUnblock: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined outside Board to preserve component identity across renders (Virtuoso optimization)
|
||||||
|
// The useFeedStateString hook is called here instead of in Board to isolate re-renders
|
||||||
|
// caused by backend IPFS state changes to just this footer component
|
||||||
|
const BoardFooter = ({
|
||||||
|
subplebbitAddresses,
|
||||||
|
hasMore,
|
||||||
|
combinedFeedLength,
|
||||||
|
subplebbitAddressesWithNewerPosts,
|
||||||
|
onNewerPostsClick,
|
||||||
|
isInAllView,
|
||||||
|
isInSubscriptionsView,
|
||||||
|
isInModView,
|
||||||
|
showMorePostsSuggestion,
|
||||||
|
feedLength,
|
||||||
|
weeklyFeedLength,
|
||||||
|
monthlyFeedLength,
|
||||||
|
yearlyFeedLength,
|
||||||
|
boardPath,
|
||||||
|
currentTimeFilterName,
|
||||||
|
subplebbitState,
|
||||||
|
subscriptionsLength,
|
||||||
|
accountSubplebbitAddressesLength,
|
||||||
|
blocked,
|
||||||
|
onUnblock,
|
||||||
|
}: BoardFooterProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const loadingStateString =
|
||||||
|
useFeedStateString(subplebbitAddresses) ||
|
||||||
|
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength)
|
||||||
|
? t('loading_feed')
|
||||||
|
: t('looking_for_more_posts'));
|
||||||
|
|
||||||
|
let footerContent;
|
||||||
|
if (combinedFeedLength === 0) {
|
||||||
|
footerContent = t('no_threads');
|
||||||
|
}
|
||||||
|
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
||||||
|
footerContent = (
|
||||||
|
<>
|
||||||
|
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
||||||
|
<div className={styles.morePostsSuggestion}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='newer_threads_available'
|
||||||
|
components={{
|
||||||
|
1: <span className={styles.newerPostsButton} onClick={onNewerPostsClick} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(isInAllView || isInSubscriptionsView || isInModView) &&
|
||||||
|
showMorePostsSuggestion &&
|
||||||
|
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
|
||||||
|
(() => {
|
||||||
|
const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
|
||||||
|
return weeklyFeedLength > feedLength ? (
|
||||||
|
<div className={styles.morePostsSuggestion}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_week'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={`${basePath}/1w`} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : monthlyFeedLength > feedLength ? (
|
||||||
|
<div className={styles.morePostsSuggestion}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_month'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={`${basePath}/1m`} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.morePostsSuggestion}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_year'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={`${basePath}/1y`} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
{footerContent}
|
||||||
|
<div>
|
||||||
|
{subplebbitState === 'failed' ? (
|
||||||
|
<span className='red'>{subplebbitState}</span>
|
||||||
|
) : isInSubscriptionsView && subscriptionsLength === 0 ? (
|
||||||
|
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
||||||
|
) : isInModView && accountSubplebbitAddressesLength === 0 ? (
|
||||||
|
<span className='red'>{t('not_mod_of_any_board')}</span>
|
||||||
|
) : blocked ? (
|
||||||
|
<span className='red'>{t('you_have_blocked_this_board')}</span>
|
||||||
|
) : (
|
||||||
|
hasMore && <LoadingEllipsis string={loadingStateString} />
|
||||||
|
)}
|
||||||
|
{blocked && (
|
||||||
|
<>
|
||||||
|
[
|
||||||
|
<span className={styles.button} onClick={onUnblock}>
|
||||||
|
{t('unblock')}
|
||||||
|
</span>
|
||||||
|
]
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const createThreadsWithoutImagesFilter = () => ({
|
const createThreadsWithoutImagesFilter = () => ({
|
||||||
filter: (comment: Comment) => {
|
filter: (comment: Comment) => {
|
||||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||||
@@ -195,12 +338,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
const weeklyFeedLength = weeklyFeed.length;
|
const weeklyFeedLength = weeklyFeed.length;
|
||||||
const monthlyFeedLength = monthlyFeed.length;
|
const monthlyFeedLength = monthlyFeed.length;
|
||||||
const yearlyFeedLength = yearlyFeed.length;
|
const yearlyFeedLength = yearlyFeed.length;
|
||||||
const hasFeedLoaded = !!feed;
|
|
||||||
const loadingStateString =
|
|
||||||
useFeedStateString(subplebbitAddresses) ||
|
|
||||||
(!hasFeedLoaded || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
|
||||||
? t('loading_feed')
|
|
||||||
: t('looking_for_more_posts'));
|
|
||||||
|
|
||||||
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
|
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -213,100 +350,64 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
|
|
||||||
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
||||||
|
|
||||||
const Footer = () => {
|
const handleUnblock = () => {
|
||||||
let footerContent;
|
unblock();
|
||||||
if (combinedFeed.length === 0) {
|
reset();
|
||||||
footerContent = t('no_threads');
|
|
||||||
}
|
|
||||||
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
|
||||||
footerContent = (
|
|
||||||
<>
|
|
||||||
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
|
||||||
<div className={styles.morePostsSuggestion}>
|
|
||||||
<Trans
|
|
||||||
i18nKey='newer_threads_available'
|
|
||||||
components={{
|
|
||||||
1: <span className={styles.newerPostsButton} onClick={handleNewerPostsButtonClick} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
(isInAllView || isInSubscriptionsView || isInModView) &&
|
|
||||||
showMorePostsSuggestion &&
|
|
||||||
(monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
|
|
||||||
(() => {
|
|
||||||
const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
|
|
||||||
return weeklyFeed.length > feed.length ? (
|
|
||||||
<div className={styles.morePostsSuggestion}>
|
|
||||||
<Trans
|
|
||||||
i18nKey='more_threads_last_week'
|
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
|
||||||
components={{
|
|
||||||
1: <Link to={`${basePath}/1w`} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : monthlyFeed.length > feed.length ? (
|
|
||||||
<div className={styles.morePostsSuggestion}>
|
|
||||||
<Trans
|
|
||||||
i18nKey='more_threads_last_month'
|
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
|
||||||
components={{
|
|
||||||
1: <Link to={`${basePath}/1m`} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.morePostsSuggestion}>
|
|
||||||
<Trans
|
|
||||||
i18nKey='more_threads_last_year'
|
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
|
||||||
components={{
|
|
||||||
1: <Link to={`${basePath}/1y`} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={styles.footer}>
|
|
||||||
{footerContent}
|
|
||||||
<div>
|
|
||||||
{subplebbitState === 'failed' ? (
|
|
||||||
<span className='red'>{subplebbitState}</span>
|
|
||||||
) : isInSubscriptionsView && subscriptions?.length === 0 ? (
|
|
||||||
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
|
||||||
) : isInModView && accountSubplebbitAddresses?.length === 0 ? (
|
|
||||||
<span className='red'>{t('not_mod_of_any_board')}</span>
|
|
||||||
) : blocked ? (
|
|
||||||
<span className='red'>{t('you_have_blocked_this_board')}</span>
|
|
||||||
) : (
|
|
||||||
hasMore && <LoadingEllipsis string={loadingStateString} />
|
|
||||||
)}
|
|
||||||
{blocked && (
|
|
||||||
<>
|
|
||||||
[
|
|
||||||
<span
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => {
|
|
||||||
unblock();
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('unblock')}
|
|
||||||
</span>
|
|
||||||
]
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||||
|
// Note: useFeedStateString is called inside BoardFooter to isolate re-renders from backend state changes
|
||||||
|
const footerComponents = useMemo(
|
||||||
|
() => ({
|
||||||
|
Footer: () => (
|
||||||
|
<BoardFooter
|
||||||
|
subplebbitAddresses={subplebbitAddresses}
|
||||||
|
hasMore={hasMore}
|
||||||
|
combinedFeedLength={combinedFeed.length}
|
||||||
|
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||||
|
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||||
|
isInAllView={isInAllView}
|
||||||
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
|
isInModView={isInModView}
|
||||||
|
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||||
|
feedLength={feedLength}
|
||||||
|
weeklyFeedLength={weeklyFeedLength}
|
||||||
|
monthlyFeedLength={monthlyFeedLength}
|
||||||
|
yearlyFeedLength={yearlyFeedLength}
|
||||||
|
boardPath={boardPath}
|
||||||
|
currentTimeFilterName={currentTimeFilterName}
|
||||||
|
subplebbitState={subplebbitState}
|
||||||
|
subscriptionsLength={subscriptions?.length || 0}
|
||||||
|
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
|
||||||
|
blocked={blocked || false}
|
||||||
|
onUnblock={handleUnblock}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
subplebbitAddresses,
|
||||||
|
hasMore,
|
||||||
|
combinedFeed.length,
|
||||||
|
subplebbitAddressesWithNewerPosts,
|
||||||
|
handleNewerPostsButtonClick,
|
||||||
|
isInAllView,
|
||||||
|
isInSubscriptionsView,
|
||||||
|
isInModView,
|
||||||
|
showMorePostsSuggestion,
|
||||||
|
feedLength,
|
||||||
|
weeklyFeedLength,
|
||||||
|
monthlyFeedLength,
|
||||||
|
yearlyFeedLength,
|
||||||
|
boardPath,
|
||||||
|
currentTimeFilterName,
|
||||||
|
subplebbitState,
|
||||||
|
subscriptions?.length,
|
||||||
|
accountSubplebbitAddresses?.length,
|
||||||
|
blocked,
|
||||||
|
handleUnblock,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
|
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
|
||||||
const navigationType = useNavigationType();
|
const navigationType = useNavigationType();
|
||||||
@@ -389,7 +490,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
data={combinedFeed}
|
data={combinedFeed}
|
||||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||||
useWindowScroll={true}
|
useWindowScroll={true}
|
||||||
components={{ Footer }}
|
components={footerComponents}
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
restoreStateFrom={lastVirtuosoState}
|
restoreStateFrom={lastVirtuosoState}
|
||||||
@@ -400,7 +501,28 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
|||||||
{combinedFeed.map((post, index) => (
|
{combinedFeed.map((post, index) => (
|
||||||
<Post key={post.cid} index={index} post={post} />
|
<Post key={post.cid} index={index} post={post} />
|
||||||
))}
|
))}
|
||||||
<Footer />
|
<BoardFooter
|
||||||
|
subplebbitAddresses={subplebbitAddresses}
|
||||||
|
hasMore={hasMore}
|
||||||
|
combinedFeedLength={combinedFeed.length}
|
||||||
|
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||||
|
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||||
|
isInAllView={isInAllView}
|
||||||
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
|
isInModView={isInModView}
|
||||||
|
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||||
|
feedLength={feedLength}
|
||||||
|
weeklyFeedLength={weeklyFeedLength}
|
||||||
|
monthlyFeedLength={monthlyFeedLength}
|
||||||
|
yearlyFeedLength={yearlyFeedLength}
|
||||||
|
boardPath={boardPath}
|
||||||
|
currentTimeFilterName={currentTimeFilterName}
|
||||||
|
subplebbitState={subplebbitState}
|
||||||
|
subscriptionsLength={subscriptions?.length || 0}
|
||||||
|
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
|
||||||
|
blocked={blocked || false}
|
||||||
|
onUnblock={handleUnblock}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+232
-92
@@ -24,6 +24,168 @@ import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
|
|||||||
|
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
|
interface CatalogFooterProps {
|
||||||
|
subplebbitAddresses: string[];
|
||||||
|
hasMore: boolean;
|
||||||
|
feedLength: number;
|
||||||
|
combinedFeedLength: number;
|
||||||
|
subplebbitAddressesWithNewerPosts: string[];
|
||||||
|
onNewerPostsClick: () => void;
|
||||||
|
isInAllView: boolean;
|
||||||
|
isInSubscriptionsView: boolean;
|
||||||
|
showMorePostsSuggestion: boolean;
|
||||||
|
weeklyFeedLength: number;
|
||||||
|
monthlyFeedLength: number;
|
||||||
|
yearlyFeedLength: number;
|
||||||
|
boardPath: string | undefined;
|
||||||
|
currentTimeFilterName: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined outside Catalog to preserve component identity across renders (Virtuoso optimization)
|
||||||
|
// The useFeedStateString hook is called here instead of in Catalog to isolate re-renders
|
||||||
|
// caused by backend IPFS state changes to just this footer component
|
||||||
|
const CatalogFooter = ({
|
||||||
|
subplebbitAddresses,
|
||||||
|
hasMore,
|
||||||
|
feedLength,
|
||||||
|
combinedFeedLength,
|
||||||
|
subplebbitAddressesWithNewerPosts,
|
||||||
|
onNewerPostsClick,
|
||||||
|
isInAllView,
|
||||||
|
isInSubscriptionsView,
|
||||||
|
showMorePostsSuggestion,
|
||||||
|
weeklyFeedLength,
|
||||||
|
monthlyFeedLength,
|
||||||
|
yearlyFeedLength,
|
||||||
|
boardPath,
|
||||||
|
currentTimeFilterName,
|
||||||
|
}: CatalogFooterProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const loadingStateString =
|
||||||
|
useFeedStateString(subplebbitAddresses) ||
|
||||||
|
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
||||||
|
? t('loading_feed')
|
||||||
|
: t('looking_for_more_posts');
|
||||||
|
|
||||||
|
let footerContent;
|
||||||
|
if (feedLength === 0) {
|
||||||
|
if (combinedFeedLength === 0) {
|
||||||
|
footerContent = t('no_threads');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
||||||
|
footerContent = (
|
||||||
|
<>
|
||||||
|
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='newer_threads_available'
|
||||||
|
components={{
|
||||||
|
1: <span className={styles.newerPostsButton} onClick={onNewerPostsClick} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(isInAllView || isInSubscriptionsView) &&
|
||||||
|
showMorePostsSuggestion &&
|
||||||
|
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
|
||||||
|
(weeklyFeedLength > feedLength ? (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_week'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1w'} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : monthlyFeedLength > feedLength ? (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_month'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1m'} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<Trans
|
||||||
|
i18nKey='more_threads_last_year'
|
||||||
|
values={{ currentTimeFilterName, count: feedLength }}
|
||||||
|
components={{
|
||||||
|
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1y'} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
<LoadingEllipsis string={loadingStateString} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className={styles.footer}>{footerContent}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Separate component for the loading state when there's no feed
|
||||||
|
// This also calls useFeedStateString internally to isolate re-renders
|
||||||
|
interface CatalogLoadingProps {
|
||||||
|
subplebbitAddresses: string[];
|
||||||
|
hasMore: boolean;
|
||||||
|
feedLength: number;
|
||||||
|
weeklyFeedLength: number;
|
||||||
|
monthlyFeedLength: number;
|
||||||
|
yearlyFeedLength: number;
|
||||||
|
state: string | undefined;
|
||||||
|
subscriptionsLength: number;
|
||||||
|
blocked: boolean;
|
||||||
|
combinedFeedLength: number;
|
||||||
|
error: Error | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CatalogLoading = ({
|
||||||
|
subplebbitAddresses,
|
||||||
|
hasMore,
|
||||||
|
feedLength,
|
||||||
|
weeklyFeedLength,
|
||||||
|
monthlyFeedLength,
|
||||||
|
yearlyFeedLength,
|
||||||
|
state,
|
||||||
|
subscriptionsLength,
|
||||||
|
blocked,
|
||||||
|
combinedFeedLength,
|
||||||
|
error,
|
||||||
|
}: CatalogLoadingProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const rawFeedStateString = useFeedStateString(subplebbitAddresses);
|
||||||
|
const loadingStateString =
|
||||||
|
rawFeedStateString || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
||||||
|
? t('loading_feed')
|
||||||
|
: t('looking_for_more_posts');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.stateString}>
|
||||||
|
{state === 'failed' ? (
|
||||||
|
<span className='red'>{state}</span>
|
||||||
|
) : subscriptionsLength === 0 ? (
|
||||||
|
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
||||||
|
) : blocked ? (
|
||||||
|
t('you_have_blocked_this_board')
|
||||||
|
) : !hasMore && combinedFeedLength === 0 ? (
|
||||||
|
t('no_threads')
|
||||||
|
) : (
|
||||||
|
hasMore && <LoadingEllipsis string={loadingStateString} />
|
||||||
|
)}
|
||||||
|
<ErrorDisplay error={error} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const createContentFilter = (
|
const createContentFilter = (
|
||||||
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
|
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
|
||||||
subplebbitAddress: string,
|
subplebbitAddress: string,
|
||||||
@@ -325,98 +487,49 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
const weeklyFeedLength = weeklyFeed.length;
|
const weeklyFeedLength = weeklyFeed.length;
|
||||||
const monthlyFeedLength = monthlyFeed.length;
|
const monthlyFeedLength = monthlyFeed.length;
|
||||||
const yearlyFeedLength = yearlyFeed.length;
|
const yearlyFeedLength = yearlyFeed.length;
|
||||||
const hasFeedLoaded = !!feed;
|
|
||||||
const loadingStateString =
|
|
||||||
useFeedStateString(subplebbitAddresses) ||
|
|
||||||
!hasFeedLoaded ||
|
|
||||||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
|
|
||||||
? t('loading_feed')
|
|
||||||
: t('looking_for_more_posts');
|
|
||||||
|
|
||||||
const loadingString = (
|
|
||||||
<div className={styles.stateString}>
|
|
||||||
{state === 'failed' ? (
|
|
||||||
<span className='red'>{state}</span>
|
|
||||||
) : isInSubscriptionsView && subscriptions?.length === 0 ? (
|
|
||||||
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
|
||||||
) : blocked ? (
|
|
||||||
t('you_have_blocked_this_board')
|
|
||||||
) : !hasMore && combinedFeed.length === 0 ? (
|
|
||||||
t('no_threads')
|
|
||||||
) : (
|
|
||||||
hasMore && <LoadingEllipsis string={loadingStateString} />
|
|
||||||
)}
|
|
||||||
<ErrorDisplay error={error} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
||||||
|
|
||||||
const Footer = () => {
|
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||||
let footerContent;
|
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
|
||||||
if (feed.length === 0) {
|
const footerComponents = useMemo(
|
||||||
if (blocked) {
|
() => ({
|
||||||
footerContent = t('you_have_blocked_this_board');
|
Footer: () => (
|
||||||
} else if (combinedFeed.length === 0) {
|
<CatalogFooter
|
||||||
footerContent = t('no_threads');
|
subplebbitAddresses={subplebbitAddresses}
|
||||||
}
|
hasMore={hasMore}
|
||||||
}
|
feedLength={feedLength}
|
||||||
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
|
combinedFeedLength={combinedFeed.length}
|
||||||
footerContent = (
|
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||||
<>
|
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||||
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
isInAllView={isInAllView}
|
||||||
<div className={styles.stateString}>
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
<Trans
|
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||||
i18nKey='newer_threads_available'
|
weeklyFeedLength={weeklyFeedLength}
|
||||||
components={{
|
monthlyFeedLength={monthlyFeedLength}
|
||||||
1: <span className={styles.newerPostsButton} onClick={handleNewerPostsButtonClick} />,
|
yearlyFeedLength={yearlyFeedLength}
|
||||||
}}
|
boardPath={boardPath}
|
||||||
/>
|
currentTimeFilterName={currentTimeFilterName}
|
||||||
</div>
|
/>
|
||||||
) : (
|
),
|
||||||
(isInAllView || isInSubscriptionsView) &&
|
}),
|
||||||
showMorePostsSuggestion &&
|
[
|
||||||
(monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
|
subplebbitAddresses,
|
||||||
(weeklyFeed.length > feed.length ? (
|
hasMore,
|
||||||
<div className={styles.stateString}>
|
feedLength,
|
||||||
<Trans
|
combinedFeed.length,
|
||||||
i18nKey='more_threads_last_week'
|
subplebbitAddressesWithNewerPosts,
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
handleNewerPostsButtonClick,
|
||||||
components={{
|
isInAllView,
|
||||||
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1w'} />,
|
isInSubscriptionsView,
|
||||||
}}
|
showMorePostsSuggestion,
|
||||||
/>
|
weeklyFeedLength,
|
||||||
</div>
|
monthlyFeedLength,
|
||||||
) : monthlyFeed.length > feed.length ? (
|
yearlyFeedLength,
|
||||||
<div className={styles.stateString}>
|
boardPath,
|
||||||
<Trans
|
currentTimeFilterName,
|
||||||
i18nKey='more_threads_last_month'
|
],
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
);
|
||||||
components={{
|
|
||||||
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1m'} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.stateString}>
|
|
||||||
<Trans
|
|
||||||
i18nKey='more_threads_last_year'
|
|
||||||
values={{ currentTimeFilterName, count: feed.length }}
|
|
||||||
components={{
|
|
||||||
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1y'} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<div className={styles.stateString}>
|
|
||||||
<LoadingEllipsis string={loadingStateString} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <div className={styles.footer}>{footerContent}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFeedLoaded = feed.length > 0 || state === 'failed';
|
const isFeedLoaded = feed.length > 0 || state === 'failed';
|
||||||
|
|
||||||
@@ -548,7 +661,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
data={rows}
|
data={rows}
|
||||||
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
|
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
|
||||||
useWindowScroll={true}
|
useWindowScroll={true}
|
||||||
components={{ Footer }}
|
components={footerComponents}
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
restoreStateFrom={lastVirtuosoState}
|
restoreStateFrom={lastVirtuosoState}
|
||||||
@@ -559,13 +672,40 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
{rows.map((row, index) => (
|
{rows.map((row, index) => (
|
||||||
<CatalogRow key={index} index={index} row={row} />
|
<CatalogRow key={index} index={index} row={row} />
|
||||||
))}
|
))}
|
||||||
<Footer />
|
<CatalogFooter
|
||||||
|
subplebbitAddresses={subplebbitAddresses}
|
||||||
|
hasMore={hasMore}
|
||||||
|
feedLength={feedLength}
|
||||||
|
combinedFeedLength={combinedFeed.length}
|
||||||
|
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
|
||||||
|
onNewerPostsClick={handleNewerPostsButtonClick}
|
||||||
|
isInAllView={isInAllView}
|
||||||
|
isInSubscriptionsView={isInSubscriptionsView}
|
||||||
|
showMorePostsSuggestion={showMorePostsSuggestion}
|
||||||
|
weeklyFeedLength={weeklyFeedLength}
|
||||||
|
monthlyFeedLength={monthlyFeedLength}
|
||||||
|
yearlyFeedLength={yearlyFeedLength}
|
||||||
|
boardPath={boardPath}
|
||||||
|
currentTimeFilterName={currentTimeFilterName}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{loadingString}
|
<CatalogLoading
|
||||||
|
subplebbitAddresses={subplebbitAddresses}
|
||||||
|
hasMore={hasMore}
|
||||||
|
feedLength={feedLength}
|
||||||
|
weeklyFeedLength={weeklyFeedLength}
|
||||||
|
monthlyFeedLength={monthlyFeedLength}
|
||||||
|
yearlyFeedLength={yearlyFeedLength}
|
||||||
|
state={state}
|
||||||
|
subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1}
|
||||||
|
blocked={blocked || false}
|
||||||
|
combinedFeedLength={combinedFeed.length}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
{blocked && (
|
{blocked && (
|
||||||
<>
|
<>
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user