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
+125 -27
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
@@ -43,6 +43,7 @@ import useRegisterFreshReplies from '../../hooks/use-register-fresh-replies';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import useQuotedByMap from '../../hooks/use-quoted-by-map';
import useProgressiveRender from '../../hooks/use-progressive-render';
import useReplyHeightEstimates from '../../hooks/use-reply-height-estimates';
import useFreshReplies from '../../hooks/use-fresh-replies';
import { BOARD_REPLIES_PREVIEW_FETCH_SIZE, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT, REPLIES_PER_PAGE } from '../../lib/constants';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
@@ -52,6 +53,7 @@ import { getThreadTopNavigationState, scrollThreadContainerToTop } from '../../l
import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
const { addChallenge } = useChallengesStore.getState();
@@ -496,7 +498,8 @@ const Reply = ({
quotedByMap,
directRepliesByParentCid,
postsByAuthorInThread,
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number> }) => {
disableDeferredLayout,
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]>; postsByAuthorInThread?: Map<string, number>; disableDeferredLayout?: boolean }) => {
const accountReply = useSafeAccountComment({
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
});
@@ -520,7 +523,7 @@ const Reply = ({
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
return (
<div className={styles.replyMobile}>
<div className={`${styles.replyMobile} ${disableDeferredLayout ? styles.pretextVirtualizedReply : ''}`}>
<div className={styles.reply}>
<div
className={`${styles.replyContainer} ${isRouteLinkToReply && styles.highlight}`}
@@ -540,8 +543,11 @@ const Reply = ({
};
const PostMobile = ({
feedVirtualizationModeOverride,
post,
roles,
replyPaginationOverride,
replyVirtualizationModeOverride,
showAllReplies,
showReplies = true,
targetReplyCid,
@@ -565,7 +571,8 @@ const PostMobile = ({
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
const linksCount = useCountLinksInReplies(resolvedPost);
const shouldFetchReplies = showReplies && !isModQueue;
const hasReplyPaginationOverride = !!replyPaginationOverride;
const shouldFetchReplies = showReplies && !isModQueue && !hasReplyPaginationOverride;
const shouldUsePreview = shouldFetchReplies && !showAllReplies;
const cachedPreviewRepliesResult = useReplies({
comment: shouldUsePreview ? resolvedPost : undefined,
@@ -601,8 +608,18 @@ const PostMobile = ({
const livePreviewReplies = (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies?.length
? (previewRepliesResult as { updatedReplies?: Comment[] }).updatedReplies!
: previewRepliesResult.replies || [];
const previewReplies = hasEnoughCachedPreview ? cachedPreviewReplies : livePreviewReplies;
const repliesResult = showAllReplies ? fullRepliesResult : { ...previewRepliesResult, replies: previewReplies, updatedReplies: previewReplies };
const previewReplies = hasReplyPaginationOverride ? replyPaginationOverride.replies : hasEnoughCachedPreview ? cachedPreviewReplies : livePreviewReplies;
const repliesResult = hasReplyPaginationOverride
? {
hasMore: replyPaginationOverride.hasMore ?? false,
loadMore: replyPaginationOverride.loadMore ?? (() => {}),
replies: replyPaginationOverride.replies,
reset: replyPaginationOverride.reset,
updatedReplies: replyPaginationOverride.replies,
}
: showAllReplies
? fullRepliesResult
: { ...previewRepliesResult, replies: previewReplies, updatedReplies: previewReplies };
const { replies, hasMore, loadMore } = repliesResult;
const updatedReplies = (repliesResult as { updatedReplies?: Comment[] }).updatedReplies;
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
@@ -635,11 +652,11 @@ const PostMobile = ({
const failedPublishNotice = canDeleteFailedPost ? <FailedPublishNotice isDeleting={isDeletingFailedPost} onDelete={onDeleteFailedPost} /> : undefined;
// Author-deleted replies are hidden from thread replies; moderator removals still render their placeholder.
const filteredReplies = filterRepliesForDisplay(freshRepliesForRender);
const postsByAuthorInThread = getThreadPostCountsByAuthor(resolvedPost, filteredReplies);
const previewDisplayReplies = getPreviewDisplayReplies(filteredReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT);
const filteredReplies = useMemo(() => filterRepliesForDisplay(freshRepliesForRender), [freshRepliesForRender]);
const postsByAuthorInThread = useMemo(() => getThreadPostCountsByAuthor(resolvedPost, filteredReplies), [resolvedPost, filteredReplies]);
const previewDisplayReplies = useMemo(() => getPreviewDisplayReplies(filteredReplies, BOARD_REPLIES_PREVIEW_VISIBLE_COUNT), [filteredReplies]);
const directRepliesByParentCid = (() => {
const directRepliesByParentCid = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const directParentCid = reply?.parentCid;
@@ -652,9 +669,62 @@ const PostMobile = ({
}
}
return map;
})();
}, [filteredReplies]);
const quotedByMap = useQuotedByMap(filteredReplies, communityAddress);
const {
defaultItemHeight: defaultReplyItemHeight,
heightEstimates: replyHeightEstimates,
itemSize: replyItemSize,
metrics,
windowWidth,
} = useReplyHeightEstimates({
directRepliesByParentCid,
enabled: showAllReplies,
isMobile: true,
maxContentChars: showAllReplies ? 2000 : 1000,
mode: replyVirtualizationModeOverride,
quotedByMap,
replies: filteredReplies,
});
const replyVirtualizationProps = replyItemSize ? { itemSize: replyItemSize } : {};
const shouldUseFeedHeightEstimate = !showAllReplies && feedVirtualizationModeOverride !== 'off';
const previewReplyHeightEstimates = useMemo(
() =>
!shouldUseFeedHeightEstimate || previewDisplayReplies.length === 0
? []
: getReplyHeightEstimates({
context: 'preview',
directRepliesByParentCid,
isMobile: true,
maxContentChars: 1000,
metrics,
quotedByMap,
replies: previewDisplayReplies,
windowWidth,
}),
[directRepliesByParentCid, metrics, previewDisplayReplies, quotedByMap, shouldUseFeedHeightEstimate, windowWidth],
);
const getPreviewReplyDebugProps = useCallback(
(index: number) => (import.meta.env.DEV && shouldUseFeedHeightEstimate ? { 'data-pretext-reply-estimate': previewReplyHeightEstimates[index] } : {}),
[previewReplyHeightEstimates, shouldUseFeedHeightEstimate],
);
const feedHeightEstimate = useMemo(
() =>
!shouldUseFeedHeightEstimate
? undefined
: getFeedPostHeightEstimate({
directRepliesByParentCid,
isMobile: true,
metrics,
post: resolvedPost,
previewReplies: previewDisplayReplies,
previewReplyEstimates: previewReplyHeightEstimates,
quotedByMap,
windowWidth,
}),
[directRepliesByParentCid, metrics, previewDisplayReplies, previewReplyHeightEstimates, quotedByMap, resolvedPost, shouldUseFeedHeightEstimate, windowWidth],
);
const visibleReplies = useProgressiveRender(filteredReplies, {
batchSize: 50,
@@ -719,7 +789,7 @@ const PostMobile = ({
</span>
</>
) : (
<div className={styles.postMobile}>
<div className={styles.postMobile} data-pretext-height={shouldUseFeedHeightEstimate ? feedHeightEstimate : undefined}>
{(showReplies || isModQueue) && (
<div className={styles.hrWrapper}>
<hr />
@@ -796,22 +866,49 @@ const PostMobile = ({
{/* Virtuoso infinite scroll for post page view when there's more content to paginate */}
{showAllReplies && !isInPendingPostView && showReplies && hasMore && !!resolvedPost?.replyCount && (
<Virtuoso
defaultItemHeight={defaultReplyItemHeight}
heightEstimates={replyHeightEstimates}
{...replyVirtualizationProps}
increaseViewportBy={{ bottom: 1200, top: 1200 }}
totalCount={filteredReplies.length}
data={filteredReplies}
itemContent={(index, reply) => (
<div className={styles.replyContainer}>
<Reply
postReplyCount={replyCount}
reply={reply}
postsByAuthorInThread={postsByAuthorInThread}
roles={roles}
threadNumber={resolvedPost?.number}
quotedByMap={quotedByMap}
directRepliesByParentCid={directRepliesByParentCid}
/>
</div>
)}
itemContent={(index, reply) => {
const renderableBacklinks = import.meta.env.DEV
? getRenderableMobileBacklinks({
cid: reply.cid,
directRepliesByParentCid,
parentCid: reply.parentCid,
quotedByMap,
})
: undefined;
const backlinkCount = renderableBacklinks
? renderableBacklinks.directReplyBacklinks.length + renderableBacklinks.opBacklinks.length + renderableBacklinks.quotedReplyBacklinks.length
: undefined;
return (
<div
className={styles.replyContainer}
data-pretext-height={replyHeightEstimates?.[index]}
data-pretext-reply-backlink-count={backlinkCount}
data-pretext-reply-content-length={import.meta.env.DEV ? reply.content?.length || 0 : undefined}
data-pretext-reply-has-media={import.meta.env.DEV ? (reply.link ? '1' : '0') : undefined}
data-pretext-reply-number={import.meta.env.DEV ? reply.number : undefined}
data-pretext-reply-title-length={import.meta.env.DEV ? reply.title?.trim().length || 0 : undefined}
ref={(element) => reportReplyHeightAuditSample(element, replyHeightEstimates?.[index], reply.cid)}
>
<Reply
disableDeferredLayout={Boolean(replyItemSize)}
postReplyCount={replyCount}
reply={reply}
postsByAuthorInThread={postsByAuthorInThread}
roles={roles}
threadNumber={resolvedPost?.number}
quotedByMap={quotedByMap}
directRepliesByParentCid={directRepliesByParentCid}
/>
</div>
);
}}
useWindowScroll={true}
components={{ Footer: virtuosoFooter }}
endReached={loadMore}
@@ -843,9 +940,10 @@ const PostMobile = ({
!isInPendingPostView &&
freshRepliesForRender &&
showReplies &&
previewDisplayReplies.map((reply) => (
<div key={reply.cid} className={styles.replyContainer}>
previewDisplayReplies.map((reply, index) => (
<div key={reply.cid} className={styles.replyContainer} {...getPreviewReplyDebugProps(index)}>
<Reply
disableDeferredLayout={feedVirtualizationModeOverride === 'item-size'}
postReplyCount={replyCount}
reply={reply}
postsByAuthorInThread={postsByAuthorInThread}