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 } = {};
|
||||
|
||||
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 = () => ({
|
||||
filter: (comment: Comment) => {
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
@@ -195,12 +338,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
const weeklyFeedLength = weeklyFeed.length;
|
||||
const monthlyFeedLength = monthlyFeed.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);
|
||||
useEffect(() => {
|
||||
@@ -213,100 +350,64 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
|
||||
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
|
||||
|
||||
const Footer = () => {
|
||||
let footerContent;
|
||||
if (combinedFeed.length === 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={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>
|
||||
);
|
||||
const handleUnblock = () => {
|
||||
unblock();
|
||||
reset();
|
||||
};
|
||||
|
||||
// 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 virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
|
||||
const navigationType = useNavigationType();
|
||||
@@ -389,7 +490,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
data={combinedFeed}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
components={footerComponents}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
@@ -400,7 +501,28 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
{combinedFeed.map((post, index) => (
|
||||
<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>
|
||||
|
||||
+232
-92
@@ -24,6 +24,168 @@ import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
|
||||
|
||||
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 = (
|
||||
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
|
||||
subplebbitAddress: string,
|
||||
@@ -325,98 +487,49 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const weeklyFeedLength = weeklyFeed.length;
|
||||
const monthlyFeedLength = monthlyFeed.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 Footer = () => {
|
||||
let footerContent;
|
||||
if (feed.length === 0) {
|
||||
if (blocked) {
|
||||
footerContent = t('you_have_blocked_this_board');
|
||||
} else if (combinedFeed.length === 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={handleNewerPostsButtonClick} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
(isInAllView || isInSubscriptionsView) &&
|
||||
showMorePostsSuggestion &&
|
||||
(monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
|
||||
(weeklyFeed.length > feed.length ? (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
i18nKey='more_threads_last_week'
|
||||
values={{ currentTimeFilterName, count: feed.length }}
|
||||
components={{
|
||||
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1w'} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : monthlyFeed.length > feed.length ? (
|
||||
<div className={styles.stateString}>
|
||||
<Trans
|
||||
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>;
|
||||
};
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
|
||||
const footerComponents = useMemo(
|
||||
() => ({
|
||||
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}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[
|
||||
subplebbitAddresses,
|
||||
hasMore,
|
||||
feedLength,
|
||||
combinedFeed.length,
|
||||
subplebbitAddressesWithNewerPosts,
|
||||
handleNewerPostsButtonClick,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
showMorePostsSuggestion,
|
||||
weeklyFeedLength,
|
||||
monthlyFeedLength,
|
||||
yearlyFeedLength,
|
||||
boardPath,
|
||||
currentTimeFilterName,
|
||||
],
|
||||
);
|
||||
|
||||
const isFeedLoaded = feed.length > 0 || state === 'failed';
|
||||
|
||||
@@ -548,7 +661,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
data={rows}
|
||||
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
components={footerComponents}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
@@ -559,13 +672,40 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
{rows.map((row, index) => (
|
||||
<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}>
|
||||
{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 && (
|
||||
<>
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user