diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx
index 2f0b1047..eb57989c 100644
--- a/src/views/board/board.tsx
+++ b/src/views/board/board.tsx
@@ -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 ? (
+
+ ,
+ }}
+ />
+
+ ) : (
+ (isInAllView || isInSubscriptionsView || isInModView) &&
+ showMorePostsSuggestion &&
+ (monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
+ (() => {
+ const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
+ return weeklyFeedLength > feedLength ? (
+
+ ,
+ }}
+ />
+
+ ) : monthlyFeedLength > feedLength ? (
+
+ ,
+ }}
+ />
+
+ ) : (
+
+ ,
+ }}
+ />
+
+ );
+ })()
+ )}
+ >
+ );
+ }
+ return (
+
+ {footerContent}
+
+ {subplebbitState === 'failed' ? (
+ {subplebbitState}
+ ) : isInSubscriptionsView && subscriptionsLength === 0 ? (
+ {t('not_subscribed_to_any_board')}
+ ) : isInModView && accountSubplebbitAddressesLength === 0 ? (
+ {t('not_mod_of_any_board')}
+ ) : blocked ? (
+ {t('you_have_blocked_this_board')}
+ ) : (
+ hasMore &&
+ )}
+ {blocked && (
+ <>
+ [
+
+ {t('unblock')}
+
+ ]
+ >
+ )}
+
+
+ );
+};
+
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 ? (
-
- ,
- }}
- />
-
- ) : (
- (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 ? (
-
- ,
- }}
- />
-
- ) : monthlyFeed.length > feed.length ? (
-
- ,
- }}
- />
-
- ) : (
-
- ,
- }}
- />
-
- );
- })()
- )}
- >
- );
- }
- return (
-
- {footerContent}
-
- {subplebbitState === 'failed' ? (
- {subplebbitState}
- ) : isInSubscriptionsView && subscriptions?.length === 0 ? (
- {t('not_subscribed_to_any_board')}
- ) : isInModView && accountSubplebbitAddresses?.length === 0 ? (
- {t('not_mod_of_any_board')}
- ) : blocked ? (
- {t('you_have_blocked_this_board')}
- ) : (
- hasMore &&
- )}
- {blocked && (
- <>
- [
- {
- unblock();
- reset();
- }}
- >
- {t('unblock')}
-
- ]
- >
- )}
-
-
- );
+ 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: () => (
+
+ ),
+ }),
+ [
+ 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(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) => }
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) => (
))}
-
+
>
)}
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx
index 8b823b45..6b65d5a9 100644
--- a/src/views/catalog/catalog.tsx
+++ b/src/views/catalog/catalog.tsx
@@ -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 ? (
+
+ ,
+ }}
+ />
+
+ ) : (
+ (isInAllView || isInSubscriptionsView) &&
+ showMorePostsSuggestion &&
+ (monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
+ (weeklyFeedLength > feedLength ? (
+
+ ,
+ }}
+ />
+
+ ) : monthlyFeedLength > feedLength ? (
+
+ ,
+ }}
+ />
+
+ ) : (
+
+ ,
+ }}
+ />
+
+ ))
+ )}
+
+
+
+ >
+ );
+ }
+ return {footerContent}
;
+};
+
+// 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 (
+
+ {state === 'failed' ? (
+ {state}
+ ) : subscriptionsLength === 0 ? (
+ {t('not_subscribed_to_any_board')}
+ ) : blocked ? (
+ t('you_have_blocked_this_board')
+ ) : !hasMore && combinedFeedLength === 0 ? (
+ t('no_threads')
+ ) : (
+ hasMore &&
+ )}
+
+
+ );
+};
+
const createContentFilter = (
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; 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 = (
-
- {state === 'failed' ? (
- {state}
- ) : isInSubscriptionsView && subscriptions?.length === 0 ? (
- {t('not_subscribed_to_any_board')}
- ) : blocked ? (
- t('you_have_blocked_this_board')
- ) : !hasMore && combinedFeed.length === 0 ? (
- t('no_threads')
- ) : (
- hasMore &&
- )}
-
-
- );
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 ? (
-
- ,
- }}
- />
-
- ) : (
- (isInAllView || isInSubscriptionsView) &&
- showMorePostsSuggestion &&
- (monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
- (weeklyFeed.length > feed.length ? (
-
- ,
- }}
- />
-
- ) : monthlyFeed.length > feed.length ? (
-
- ,
- }}
- />
-
- ) : (
-
- ,
- }}
- />
-
- ))
- )}
-
-
-
- >
- );
- }
- return {footerContent}
;
- };
+ // 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: () => (
+
+ ),
+ }),
+ [
+ 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) => }
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) => (
))}
-
+
>
)}
>
) : (
- {loadingString}
+
{blocked && (
<>
[