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
@@ -54,7 +54,6 @@ const testState = vi.hoisted(() => ({
hiddenCids: new Set<string>(),
imageSize: 'Small' as 'Large' | 'Small',
linkCount: 0,
matchedFilters: new Map<string, string>(),
mediaInfoByLink: {} as Record<string, { patternThumbnailUrl?: string; thumbnail?: string; type: string; url: string }>,
lastRepliesComment: undefined as TestComment | undefined,
replies: [] as TestComment[],
@@ -63,12 +62,6 @@ const testState = vi.hoisted(() => ({
showSnow: false,
}));
function getCatalogFiltersState() {
return {
matchedFilters: testState.matchedFilters,
};
}
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
@@ -156,13 +149,6 @@ vi.mock('../../../hooks/use-directories', () => ({
useDirectories: () => testState.directories,
}));
vi.mock('../../../stores/use-catalog-filters-store', () => ({
default: <T,>(selector?: (state: ReturnType<typeof getCatalogFiltersState>) => T) => {
const state = getCatalogFiltersState();
return selector ? selector(state) : (state as T);
},
}));
vi.mock('../../../stores/use-catalog-style-store', () => ({
default: () => ({
imageSize: testState.imageSize,
@@ -232,7 +218,6 @@ describe('CatalogRow', () => {
testState.hiddenCids = new Set<string>();
testState.imageSize = 'Small';
testState.linkCount = 0;
testState.matchedFilters = new Map<string, string>();
testState.mediaInfoByLink = {};
testState.lastRepliesComment = undefined;
testState.replies = [];
@@ -253,7 +238,6 @@ describe('CatalogRow', () => {
it('renders gif frames with matched filter borders and falls back to deleted media on load errors', async () => {
testState.gifFrameStatus = 'ready';
testState.gifFrameUrl = 'https://cdn.example/frame.png';
testState.matchedFilters = new Map([['post-1', 'red']]);
await act(async () => {
root.render(
@@ -262,6 +246,7 @@ describe('CatalogRow', () => {
commentMediaInfo: { type: 'gif', url: 'https://example.com/source.gif' },
linkHeight: 200,
linkWidth: 400,
matchedFilterColor: 'red',
}),
);
});
@@ -416,7 +401,7 @@ describe('CatalogRow', () => {
expect(container.textContent).toContain('/ I: 3');
expect(container.textContent).not.toContain('/ L: 3');
expect(document.body.querySelector('a[href="/mu/thread/post-alias"]')).toBeTruthy();
expect(container.querySelector('[title=\"(R)eplies / (I)mage Replies\"]')).toBeTruthy();
expect(container.querySelector('[title="(R)eplies / (I)mage Replies"]')).toBeTruthy();
});
it('normalizes legacy board addresses before fetching hover preview replies', async () => {
@@ -495,4 +480,16 @@ describe('CatalogRow', () => {
expect(container.textContent).toContain('(hidden)');
expect(container.textContent).toContain('Text title: Plain thread body');
});
it('applies the estimated row height to the virtualization wrapper', async () => {
const post: TestComment = {
cid: 'estimated-post',
content: 'Estimated row body',
communityAddress: 'music-posting.eth',
};
await renderWithRouter(createElement(CatalogRow, { estimatedHeight: 246, row: [post] }), '/mu/catalog');
expect(container.querySelector('[data-pretext-height="246"]')).toBeTruthy();
});
});
+18 -10
View File
@@ -11,7 +11,6 @@ import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { findDirectoryByAddress, useDirectories } from '../../hooks/use-directories';
import { getBoardPath } from '../../lib/utils/route-utils';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useEditCommentPrivileges from '../../hooks/use-author-privileges';
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
@@ -32,9 +31,11 @@ interface CatalogPostMediaProps {
isOutOfFeed?: boolean;
linkWidth?: number;
linkHeight?: number;
matchedFilterColor?: string;
}
export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight }: CatalogPostMediaProps) => {
export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight, matchedFilterColor }: CatalogPostMediaProps) => {
void cid;
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
const iframeThumbnail = patternThumbnailUrl || thumbnail;
const { frameUrl: gifFrameUrl, status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
@@ -100,8 +101,6 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight
thumbnailComponent = <audio src={url} controls />;
}
const matchedFilterColor = useCatalogFiltersStore((state) => state.matchedFilters.get(cid || ''));
return (
<div
className={hasError ? '' : styles.mediaWrapper}
@@ -118,7 +117,7 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight
// Memoize CatalogPost to prevent rerenders when parent rerenders due to updatingState
const CatalogPost = memo(
({ post }: { post: Comment }) => {
({ matchedFilterColor, post }: { matchedFilterColor?: string; post: Comment }) => {
const { t } = useTranslation();
const resolvedPost = useMemo(() => withResolvedCommentCommunityAddress(post), [post]);
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, spoiler, communityAddress, timestamp, title, thumbnailUrl } =
@@ -260,7 +259,13 @@ const CatalogPost = memo(
{spoiler ? (
<img src='assets/spoiler.png' alt='' />
) : (
<CatalogPostMedia cid={cid} commentMediaInfo={commentMediaInfo} linkWidth={linkWidth} linkHeight={linkHeight} />
<CatalogPostMedia
cid={cid}
commentMediaInfo={commentMediaInfo}
linkWidth={linkWidth}
linkHeight={linkHeight}
matchedFilterColor={matchedFilterColor}
/>
)}
</div>
</Link>
@@ -339,21 +344,24 @@ const CatalogPost = memo(
prev?.thumbnailUrl === next?.thumbnailUrl &&
prev?.linkWidth === next?.linkWidth &&
prev?.linkHeight === next?.linkHeight &&
prevCommunityAddress === nextCommunityAddress
prevCommunityAddress === nextCommunityAddress &&
prevProps.matchedFilterColor === nextProps.matchedFilterColor
);
},
);
interface CatalogRowProps {
estimatedHeight?: number;
index?: number;
matchedFilterColors?: Map<string, string>;
row: Comment[];
}
const CatalogRow = memo(({ row }: CatalogRowProps) => {
const CatalogRow = memo(({ estimatedHeight, matchedFilterColors, row }: CatalogRowProps) => {
return (
<div className={styles.row}>
<div className={styles.row} data-pretext-height={estimatedHeight}>
{row.map((post, index) => (
<CatalogPost key={post?.cid || index} post={post} />
<CatalogPost key={post?.cid || index} matchedFilterColor={matchedFilterColors?.get(post?.cid || '')} post={post} />
))}
</div>
);