feat: finalize pretext feed sizing rollout (#1120)

* feat(feeds): add pretext-backed item sizing across board, catalog, and thread replies

* feat: finalize pretext feed sizing rollout

* fix(catalog): raise multiboard viewport buffer

* fix(board): preserve pretext query overrides
This commit is contained in:
Tommaso Casaburi
2026-04-02 19:45:55 +07:00
committed by GitHub
parent 4fb7739991
commit 251e103db3
21 changed files with 2733 additions and 179 deletions
+32 -12
View File
@@ -16,15 +16,16 @@ import useFeedResetStore from '../../stores/use-feed-reset-store';
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
import usePostNumberStore from '../../stores/use-post-number-store';
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
import useIsMobile from '../../hooks/use-is-mobile';
import { getPageSlice } from '../../lib/utils/board-feed-pagination';
import { getPageFromFeedPath, getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath, stripPageFromFeedPath } from '../../lib/utils/route-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { getPretextItemSizeFromElement, resolveFeedVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
import ErrorDisplay from '../../components/error-display/error-display';
import LoadingEllipsis from '../../components/loading-ellipsis';
import BoardPagination from '../../components/board-pagination';
import { CatalogButton } from '../../components/board-buttons/board-buttons';
import { PageFooterDesktop, PageFooterMobile } from '../../components/footer';
import useIsMobile from '../../hooks/use-is-mobile';
import { Post } from '../post';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -234,24 +235,34 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
);
const navigate = useNavigate();
const isMultiboardView = isInAllView || isInSubscriptionsView || isInModView;
const defaultFeedVirtualizationMode = isMobile && isMultiboardView ? 'off' : 'item-size';
const feedVirtualizationMode = useMemo(
() => resolveFeedVirtualizationMode(location.search, defaultFeedVirtualizationMode),
[defaultFeedVirtualizationMode, location.search],
);
const defaultBoardItemHeight = feedVirtualizationMode === 'item-size' ? (isMobile ? 420 : 480) : isMobile ? 420 : 300;
// Omit the prop entirely in fallback mode. Passing `itemSize={undefined}` overrides
// Virtuoso's internal DOM measurer and leaves multiboard items stuck on the default height.
const boardSizingProps = useMemo(() => (feedVirtualizationMode === 'item-size' ? { itemSize: getPretextItemSizeFromElement } : {}), [feedVirtualizationMode]);
// Redirect multiboard paths with page-number segments to normalized path (infinite-scroll only)
useEffect(() => {
if (!isVisible || !isForcedInfiniteScroll) return;
const normalized = normalizeMultiboardFeedPath(location.pathname);
if (normalized !== location.pathname) {
navigate(normalized, { replace: true });
navigate({ pathname: normalized, search: location.search }, { replace: true });
}
}, [isVisible, isForcedInfiniteScroll, location.pathname, navigate]);
}, [isVisible, isForcedInfiniteScroll, location.pathname, location.search, navigate]);
useEffect(() => {
if (!isVisible) return;
if (!effectiveInfiniteScroll && currentPage > totalPages && totalPages > 0) {
const targetPage = totalPages;
const targetPath = targetPage === 1 ? paginationBasePath : `${paginationBasePath}/${targetPage}`;
navigate(targetPath, { replace: true });
navigate({ pathname: targetPath, search: location.search }, { replace: true });
}
}, [isVisible, effectiveInfiniteScroll, currentPage, totalPages, paginationBasePath, navigate]);
}, [isVisible, effectiveInfiniteScroll, currentPage, totalPages, paginationBasePath, location.search, navigate]);
// Scroll to top instantly when page changes in pagination mode
useEffect(() => {
@@ -301,7 +312,14 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
/>
<PageFooterDesktop
firstRow={
<BoardPagination basePath={paginationBasePath} currentPage={currentPage} totalPages={totalPages} footerStyle isMultiboard={isForcedInfiniteScroll} />
<BoardPagination
basePath={paginationBasePath}
currentPage={currentPage}
search={location.search}
totalPages={totalPages}
footerStyle
isMultiboard={isForcedInfiniteScroll}
/>
}
/>
<PageFooterMobile>
@@ -329,7 +347,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
<span key={page}>
[
<Link
to={page === 1 ? paginationBasePath : `${paginationBasePath}/${page}`}
to={{ pathname: page === 1 ? paginationBasePath : `${paginationBasePath}/${page}`, search: location.search }}
className={page === currentPage ? mobileFooterStyles.mobileFooterPaginationCurrent : undefined}
>
{page}
@@ -373,6 +391,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
totalPages,
setEnableInfiniteScroll,
reset,
location.search,
t,
],
);
@@ -380,12 +399,13 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${BOARD_SORT_TYPE}` : `${location.pathname}-${BOARD_SORT_TYPE}`;
const navigationType = useNavigationType();
const isMultiboardView = isInAllView || isInSubscriptionsView || isInModView;
const defaultBoardItemHeight = isMobile ? 420 : 300;
const boardViewportBuffer = isMultiboardView ? (isMobile ? { bottom: 1400, top: 2400 } : { bottom: 600, top: 600 }) : { bottom: 1200, top: 1200 };
const boardMinOverscanItemCount = isMultiboardView && isMobile ? { bottom: 4, top: 8 } : undefined;
const boardItemContent = useCallback((index: number, post: Comment | undefined) => <Post index={index} post={post} />, []);
const boardItemContent = useCallback(
(index: number, post: Comment | undefined) => <Post feedVirtualizationModeOverride={feedVirtualizationMode} index={index} post={post} />,
[feedVirtualizationMode],
);
const hasBeenVisibleRef = useRef(false);
useEffect(() => {
@@ -401,8 +421,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
if (!isVisible) return;
const currentKey = virtuosoStateKey;
// Saving the snapshot on every scroll tick makes board scrolling do extra work
// in the hottest path. Capturing on teardown still preserves POP restores.
// Avoid state snapshot work on every scroll tick in the hottest board path.
const saveVirtuosoState = () => {
virtuosoRef.current?.getState((snapshot: StateSnapshot) => {
if (snapshot?.ranges?.length) {
@@ -454,6 +473,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
{effectiveInfiniteScroll ? (
<Virtuoso
defaultItemHeight={defaultBoardItemHeight}
{...boardSizingProps}
increaseViewportBy={boardViewportBuffer}
minOverscanItemCount={boardMinOverscanItemCount}
totalCount={displayFeed.length}
+9 -12
View File
@@ -35,7 +35,6 @@ const testState = vi.hoisted(() => ({
account: { subscriptions: [] as string[] },
accountComments: [] as TestComment[],
accountCommentsCalls: [] as Array<{ commentIndices?: number[]; communityAddress?: string; newerThan?: number; sortType?: 'new' | 'old' } | undefined>,
clearMatchedFiltersMock: vi.fn(),
directoryByAddress: {
'music-posting.eth': {
address: 'music-posting.eth',
@@ -47,6 +46,7 @@ const testState = vi.hoisted(() => ({
filterItems: [] as FilterItem[],
filteredDirectoryAddresses: ['music-posting.eth'] as string[],
hasMore: false,
imageSize: 'Small' as 'Large' | 'Small',
incrementFilterCountMock: vi.fn(),
loadMoreMock: vi.fn(),
pageSizes: {
@@ -58,8 +58,8 @@ const testState = vi.hoisted(() => ({
resolvedCommunityAddress: 'music-posting.eth' as string | undefined,
searchText: '',
setCurrentCommunityAddressMock: vi.fn(),
setMatchedFilterMock: vi.fn(),
setResetFunctionMock: vi.fn(),
showOPComment: true,
sortType: 'new' as 'active' | 'new',
windowWidth: 900,
community: {
@@ -72,12 +72,10 @@ const testState = vi.hoisted(() => ({
function getCatalogFiltersState() {
return {
clearMatchedFilters: testState.clearMatchedFiltersMock,
filterItems: testState.filterItems,
incrementFilterCount: testState.incrementFilterCountMock,
searchText: testState.searchText,
setCurrentSubplebbitAddress: testState.setCurrentCommunityAddressMock,
setMatchedFilter: testState.setMatchedFilterMock,
};
}
@@ -191,7 +189,8 @@ vi.mock('../../../hooks/use-window-width', () => ({
vi.mock('../../../stores/use-catalog-style-store', () => ({
default: () => ({
imageSize: 'Small',
imageSize: testState.imageSize,
showOPComment: testState.showOPComment,
}),
}));
@@ -213,7 +212,8 @@ vi.mock('../../../stores/use-catalog-filters-store', () => ({
}));
vi.mock('../../../components/catalog-row', () => ({
default: ({ row }: { row: TestComment[] }) => createElement('div', { 'data-testid': 'catalog-row' }, `row:${row.map((comment) => comment.cid).join(',')}`),
default: ({ estimatedHeight, row }: { estimatedHeight?: number; row: TestComment[] }) =>
createElement('div', { 'data-pretext-height': estimatedHeight, 'data-testid': 'catalog-row' }, `row:${row.map((comment) => comment.cid).join(',')}`),
}));
vi.mock('../../../components/footer', () => ({
@@ -308,6 +308,7 @@ describe('Catalog', () => {
testState.filterItems = [];
testState.filteredDirectoryAddresses = ['music-posting.eth'];
testState.hasMore = false;
testState.imageSize = 'Small';
testState.pageSizes = {
guiPostsPerPage: 2,
maxGuiPages: 3,
@@ -315,6 +316,7 @@ describe('Catalog', () => {
};
testState.resolvedCommunityAddress = 'music-posting.eth';
testState.searchText = '';
testState.showOPComment = true;
testState.sortType = 'new';
testState.windowWidth = 900;
testState.community = {
@@ -323,12 +325,10 @@ describe('Catalog', () => {
state: 'ready',
title: '/mu/ - Music',
};
testState.clearMatchedFiltersMock.mockReset();
testState.incrementFilterCountMock.mockReset();
testState.loadMoreMock.mockReset();
testState.resetMock.mockReset();
testState.setCurrentCommunityAddressMock.mockReset();
testState.setMatchedFilterMock.mockReset();
testState.setResetFunctionMock.mockReset();
document.title = 'before';
@@ -342,7 +342,7 @@ describe('Catalog', () => {
container.remove();
});
it('applies catalog filters, promotes top matches, and clears board filter state on unmount', async () => {
it('applies catalog filters and promotes top matches', async () => {
testState.feed = [
{ cid: 'boring-post', title: 'plain talk', content: 'nothing special', communityAddress: 'music-posting.eth' },
{ cid: 'hidden-post', title: 'cats and spoilers', content: 'spoiler content', communityAddress: 'music-posting.eth' },
@@ -357,16 +357,13 @@ describe('Catalog', () => {
expect(document.title).toBe('/mu/ - catalog - 5chan');
expect(testState.setCurrentCommunityAddressMock).toHaveBeenCalledWith('music-posting.eth');
expect(testState.clearMatchedFiltersMock).toHaveBeenCalled();
expect(Array.from(container.querySelectorAll('[data-testid="catalog-row"]')).map((element) => element.textContent)).toEqual(['row:top-post,boring-post']);
expect(testState.incrementFilterCountMock).toHaveBeenCalledWith(0, 'hidden-post', 'music-posting.eth');
expect(testState.incrementFilterCountMock).toHaveBeenCalledWith(1, 'top-post', 'music-posting.eth');
expect(testState.setMatchedFilterMock).toHaveBeenCalledWith('top-post', 'red');
act(() => root.unmount());
expect(testState.setCurrentCommunityAddressMock).toHaveBeenLastCalledWith(null);
expect(testState.clearMatchedFiltersMock).toHaveBeenCalledTimes(3);
root = createRoot(container);
});
+65 -65
View File
@@ -8,6 +8,7 @@ import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useFeedStateString } from '../../hooks/use-state-string';
import useIsMobile from '../../hooks/use-is-mobile';
import useWindowWidth from '../../hooks/use-window-width';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
@@ -24,6 +25,13 @@ import styles from './catalog.module.css';
import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { sortCatalogFeedForDisplay } from '../../lib/utils/catalog-sort';
import {
getCatalogRowHeightEstimates,
getPretextItemSizeFromElement,
getTypicalCatalogRowHeight,
readReplyTypographyMetrics,
resolveCatalogVirtualizationMode,
} from '../../lib/utils/pretext-height-estimates';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS = 60 * 60;
@@ -34,9 +42,6 @@ interface CatalogFooterProps {
communityAddresses: string[];
hasMore: boolean;
combinedFeedLength: number;
isInAllView: boolean;
isInSubscriptionsView: boolean;
isInModView: boolean;
/** When false, suppress the loading ellipsis (e.g. non-infinite mode) */
showLoadingEllipsis?: boolean;
}
@@ -44,15 +49,7 @@ interface CatalogFooterProps {
// 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 = ({
communityAddresses,
hasMore,
combinedFeedLength,
isInAllView,
isInSubscriptionsView,
isInModView,
showLoadingEllipsis = true,
}: CatalogFooterProps) => {
const CatalogFooter = ({ communityAddresses, hasMore, combinedFeedLength, showLoadingEllipsis = true }: CatalogFooterProps) => {
const { t } = useTranslation();
const loadingStateString = useFeedStateString(communityAddresses) || (combinedFeedLength === 0 ? t('loading_feed') : t('looking_for_more_posts'));
@@ -141,11 +138,6 @@ const createContentFilter = (
// Fallback to the store method if no callback provided
useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, communityAddress);
}
// If the filter has a color, track it in the matchedFilters map
if (item.color) {
useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color);
}
}
// If this filter is set to hide, filter out the comment
@@ -222,7 +214,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
return resolvedAddressFromUrl;
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
const { filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
const { filterItems, searchText } = useCatalogFiltersStore();
const account = useAccount();
const subscriptions = account?.subscriptions;
@@ -239,11 +231,11 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
return communityAddress ? [communityAddress] : [];
}, [isInAllView, isInSubscriptionsView, communityAddress, filteredDirectoryAddresses, subscriptions]);
const { imageSize } = useCatalogStyleStore();
const { imageSize, showOPComment } = useCatalogStyleStore();
const columnWidth = imageSize === 'Large' ? 270 : 180;
const columnCount = Math.floor(useWindowWidth() / columnWidth);
const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25;
const windowWidth = useWindowWidth();
const isMobile = useIsMobile();
const columnCount = Math.floor(windowWidth / columnWidth);
const communityDirectory = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : communityAddress);
const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage } = useBoardFeedPageSize(communityDirectory);
@@ -259,6 +251,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const { sortType } = useSortingStore();
const feedSortType = sortType === 'new' ? 'new' : 'active';
const catalogVirtualizationMode = useMemo(() => resolveCatalogVirtualizationMode(location.search, 'item-size'), [location.search]);
const themeKey = typeof document !== 'undefined' ? document.body.className : '';
// Create a stable callback for filter matching
const handleFilterMatch = useCallback((filterIndex: number, cid: string, communityAddress: string) => {
@@ -372,15 +366,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
() => ({
Footer: () => (
<>
<CatalogFooter
communityAddresses={communityAddresses}
hasMore={hasMore}
combinedFeedLength={cappedFeed.length}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
isInModView={isInModView}
showLoadingEllipsis={effectiveInfiniteScroll}
/>
<CatalogFooter communityAddresses={communityAddresses} hasMore={hasMore} combinedFeedLength={cappedFeed.length} showLoadingEllipsis={effectiveInfiniteScroll} />
<PageFooterDesktop
firstRow={
<CatalogFooterFirstRow
@@ -440,6 +426,29 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
return [...topPosts, ...regularPosts];
}, [sortedFeed, filterItems]);
const matchedFilterColors = useMemo(() => {
const nextMatchedFilterColors = new Map<string, string>();
const activeColoredFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '' && item.color);
if (activeColoredFilters.length === 0) {
return nextMatchedFilterColors;
}
for (const comment of processedFeed) {
const cid = comment?.cid;
if (!cid) {
continue;
}
const firstMatch = activeColoredFilters.find((item) => commentMatchesPattern(comment, item.text));
if (firstMatch?.color) {
nextMatchedFilterColors.set(cid, firstMatch.color);
}
}
return nextMatchedFilterColors;
}, [filterItems, processedFeed]);
const rows = useMemo(() => {
if (!isFeedLoaded) {
return [];
@@ -453,6 +462,26 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
return nextRows;
}, [columnCount, isFeedLoaded, processedFeed]);
const catalogMetrics = useMemo(() => readReplyTypographyMetrics(), [themeKey, windowWidth]);
const rowHeightEstimates = useMemo(
() =>
catalogVirtualizationMode === 'off'
? []
: getCatalogRowHeightEstimates({
imageSize,
metrics: catalogMetrics,
rows,
showOPComment,
}),
[catalogMetrics, catalogVirtualizationMode, imageSize, rows, showOPComment],
);
const defaultCatalogRowHeight = useMemo(() => getTypicalCatalogRowHeight(rowHeightEstimates, imageSize), [imageSize, rowHeightEstimates]);
// Omit the prop entirely in fallback mode. Passing `itemSize={undefined}` overrides
// Virtuoso's default DOM measurement path and leaves rows on the fallback height.
const catalogSizingProps = useMemo(() => (catalogVirtualizationMode === 'item-size' ? { itemSize: getPretextItemSizeFromElement } : {}), [catalogVirtualizationMode]);
const isMultiboardView = isInAllView || isInSubscriptionsView || isInModView;
const catalogViewportBuffer = isMultiboardView ? (isMobile ? { bottom: 2400, top: 1200 } : { bottom: 1200, top: 900 }) : { bottom: 1200, top: 1200 };
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}` : `${location.pathname}-${sortType}-catalog`;
const navigationType = useNavigationType();
@@ -501,37 +530,6 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
document.title = documentTitle + ` - ${t('catalog')} - 5chan`;
}, [title, shortAddress, communityAddress, isInAllView, isInSubscriptionsView, t, isVisible, params.boardIdentifier, boardIdentifierProp, directories]);
// Clear matched filters when component mounts or when community changes
useEffect(() => {
clearMatchedFilters();
return () => {
clearMatchedFilters();
};
}, [clearMatchedFilters, communityAddress]);
// Memoize filter color application to avoid redundant iterations
useMemo(() => {
if (cappedFeed.length > 0 && filterItems.length > 0) {
// Clear existing matched filters
clearMatchedFilters();
// Apply colors to posts that match filters
cappedFeed.forEach((comment) => {
if (!comment?.cid) return;
// Check each filter
for (const item of filterItems) {
if (item.enabled && item.text.trim() !== '' && item.color) {
if (commentMatchesPattern(comment, item.text)) {
useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color);
break; // Use the first matching filter's color
}
}
}
});
}
}, [cappedFeed, filterItems, clearMatchedFilters]);
return (
<div className={styles.content}>
<hr />
@@ -539,11 +537,13 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
{processedFeed?.length !== 0 ? (
<>
<Virtuoso
defaultItemHeight={imageSize === 'Large' ? 320 : 200}
increaseViewportBy={isInAllView || isInSubscriptionsView || isInModView ? { bottom: 600, top: 600 } : { bottom: 1200, top: 1200 }}
defaultItemHeight={defaultCatalogRowHeight}
heightEstimates={catalogVirtualizationMode === 'off' ? undefined : rowHeightEstimates}
increaseViewportBy={catalogViewportBuffer}
{...catalogSizingProps}
totalCount={rows?.length || 0}
data={rows}
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
itemContent={(index, row) => <CatalogRow estimatedHeight={rowHeightEstimates[index]} index={index} matchedFilterColors={matchedFilterColors} row={row} />}
useWindowScroll={true}
components={footerComponents}
endReached={effectiveInfiniteScroll && hasMore ? loadMore : undefined}
+6
View File
@@ -507,6 +507,12 @@
contain-intrinsic-size: auto 120px;
}
.replyDesktop.pretextVirtualizedReply,
.replyMobile.pretextVirtualizedReply {
content-visibility: visible;
contain-intrinsic-size: auto;
}
.replyMobile .replyContainer {
background-color: var(--post-mobile-background-color);
border-bottom: var(--post-mobile-border-bottom);
+35 -1
View File
@@ -17,6 +17,7 @@ import PostDesktop from '../../components/post-desktop';
import PostMobile from '../../components/post-mobile';
import { getRequestedThreadTopCid, scrollThreadContainerToTop } from '../../lib/utils/thread-scroll-utils';
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
import type { ReplyVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
import styles from './post.module.css';
type CommentWithRefresh = Comment & {
@@ -26,6 +27,13 @@ type CommentWithRefresh = Comment & {
errors?: Error[];
};
export interface ReplyPaginationOverride {
hasMore?: boolean;
loadMore?: () => void;
replies: Comment[];
reset?: () => Promise<void>;
}
// useComment may not return cached feed data immediately due to its updatedAt comparison logic.
// This hook falls back to the communities pages store (populated by useFeed) so content
// from the catalog appears instantly instead of going through a loading phase.
@@ -46,12 +54,15 @@ const useCommentWithFeedCache = (options: { commentCid: string | undefined; auto
};
export interface PostProps {
feedVirtualizationModeOverride?: ReplyVirtualizationMode;
index?: number;
isHidden?: boolean;
hasThumbnail?: boolean;
post?: any;
postReplyCount?: number;
reply?: any;
replyPaginationOverride?: ReplyPaginationOverride;
replyVirtualizationModeOverride?: ReplyVirtualizationMode;
roles?: Role[];
showAllReplies?: boolean;
showReplies?: boolean;
@@ -67,7 +78,21 @@ export interface PostProps {
}
export const Post = memo(
({ post, showAllReplies = false, showReplies = true, targetReplyCid, isModQueue, modQueueStatus, modQueueError, isPublishing, onApprove, onReject }: PostProps) => {
({
post,
showAllReplies = false,
showReplies = true,
targetReplyCid,
isModQueue,
modQueueStatus,
modQueueError,
isPublishing,
onApprove,
onReject,
feedVirtualizationModeOverride,
replyPaginationOverride,
replyVirtualizationModeOverride,
}: PostProps) => {
// Only subscribe to roles field to avoid rerenders from updatingState changes
const communityAddress = post?.communityAddress || post?.subplebbitAddress;
const roles = useCommunityField(communityAddress, (community) => community?.roles);
@@ -86,7 +111,10 @@ export const Post = memo(
<div className={styles.postContainer}>
{isMobile ? (
<PostMobile
feedVirtualizationModeOverride={feedVirtualizationModeOverride}
post={comment}
replyPaginationOverride={replyPaginationOverride}
replyVirtualizationModeOverride={replyVirtualizationModeOverride}
roles={roles}
showAllReplies={showAllReplies}
showReplies={showReplies}
@@ -100,7 +128,10 @@ export const Post = memo(
/>
) : (
<PostDesktop
feedVirtualizationModeOverride={feedVirtualizationModeOverride}
post={comment}
replyPaginationOverride={replyPaginationOverride}
replyVirtualizationModeOverride={replyVirtualizationModeOverride}
roles={roles}
showAllReplies={showAllReplies}
showReplies={showReplies}
@@ -133,6 +164,9 @@ export const Post = memo(
prevProps.showAllReplies === nextProps.showAllReplies &&
prevProps.showReplies === nextProps.showReplies &&
prevProps.targetReplyCid === nextProps.targetReplyCid &&
prevProps.feedVirtualizationModeOverride === nextProps.feedVirtualizationModeOverride &&
prevProps.replyPaginationOverride === nextProps.replyPaginationOverride &&
prevProps.replyVirtualizationModeOverride === nextProps.replyVirtualizationModeOverride &&
prevProps.isModQueue === nextProps.isModQueue &&
prevProps.modQueueStatus === nextProps.modQueueStatus &&
prevProps.modQueueError === nextProps.modQueueError &&