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) => ( ))} -