mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(catalog): address missed review findings
This commit is contained in:
@@ -3,7 +3,7 @@ import { createElement } from 'react';
|
|||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import Catalog, { type CatalogProps } from '../catalog';
|
import Catalog, { getCatalogRenderFeed, type CatalogProps } from '../catalog';
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||||
@@ -62,6 +62,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
setResetFunctionMock: vi.fn(),
|
setResetFunctionMock: vi.fn(),
|
||||||
showOPComment: true,
|
showOPComment: true,
|
||||||
sortType: 'new' as 'active' | 'new',
|
sortType: 'new' as 'active' | 'new',
|
||||||
|
virtuosoInitialScrollTops: [] as Array<number | undefined>,
|
||||||
windowWidth: 900,
|
windowWidth: 900,
|
||||||
community: {
|
community: {
|
||||||
error: undefined as Error | undefined,
|
error: undefined as Error | undefined,
|
||||||
@@ -142,11 +143,13 @@ vi.mock('react-virtuoso', () => ({
|
|||||||
components,
|
components,
|
||||||
data = [],
|
data = [],
|
||||||
endReached,
|
endReached,
|
||||||
|
initialScrollTop,
|
||||||
itemContent,
|
itemContent,
|
||||||
}: {
|
}: {
|
||||||
components?: { Footer?: React.ComponentType };
|
components?: { Footer?: React.ComponentType };
|
||||||
data?: Array<TestComment[]>;
|
data?: Array<TestComment[]>;
|
||||||
endReached?: ((index: number) => void) | undefined;
|
endReached?: ((index: number) => void) | undefined;
|
||||||
|
initialScrollTop?: number;
|
||||||
itemContent: (index: number, item: TestComment[]) => React.ReactNode;
|
itemContent: (index: number, item: TestComment[]) => React.ReactNode;
|
||||||
},
|
},
|
||||||
ref: React.ForwardedRef<{ getState: (cb: (snapshot: { ranges: number[]; scrollTop: number }) => void) => void }>,
|
ref: React.ForwardedRef<{ getState: (cb: (snapshot: { ranges: number[]; scrollTop: number }) => void) => void }>,
|
||||||
@@ -154,6 +157,7 @@ vi.mock('react-virtuoso', () => ({
|
|||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
getState: (cb) => cb({ ranges: [0], scrollTop: 24 }),
|
getState: (cb) => cb({ ranges: [0], scrollTop: 24 }),
|
||||||
}));
|
}));
|
||||||
|
testState.virtuosoInitialScrollTops.push(initialScrollTop);
|
||||||
|
|
||||||
return createElement(
|
return createElement(
|
||||||
'div',
|
'div',
|
||||||
@@ -323,6 +327,7 @@ describe('Catalog', () => {
|
|||||||
testState.searchText = '';
|
testState.searchText = '';
|
||||||
testState.showOPComment = true;
|
testState.showOPComment = true;
|
||||||
testState.sortType = 'new';
|
testState.sortType = 'new';
|
||||||
|
testState.virtuosoInitialScrollTops = [];
|
||||||
testState.windowWidth = 900;
|
testState.windowWidth = 900;
|
||||||
testState.community = {
|
testState.community = {
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@@ -385,6 +390,13 @@ describe('Catalog', () => {
|
|||||||
expect(Array.from(container.querySelectorAll('[data-testid="catalog-row"]')).map((element) => element.textContent)).toEqual(['row:board-post-1,board-post-2']);
|
expect(Array.from(container.querySelectorAll('[data-testid="catalog-row"]')).map((element) => element.textContent)).toEqual(['row:board-post-1,board-post-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers the immediate feed until deferred catalog rows exist', () => {
|
||||||
|
const immediateFeed = [{ cid: 'all-post', title: 'one', communityAddress: 'music-posting.eth' }];
|
||||||
|
|
||||||
|
expect(getCatalogRenderFeed(immediateFeed, [])).toBe(immediateFeed);
|
||||||
|
expect(getCatalogRenderFeed(immediateFeed, immediateFeed)).toBe(immediateFeed);
|
||||||
|
});
|
||||||
|
|
||||||
it('canonicalizes multiboard catalog paths and keeps load-more wired for infinite scrolling', async () => {
|
it('canonicalizes multiboard catalog paths and keeps load-more wired for infinite scrolling', async () => {
|
||||||
testState.feed = [{ cid: 'all-post', title: 'one', communityAddress: 'music-posting.eth' }];
|
testState.feed = [{ cid: 'all-post', title: 'one', communityAddress: 'music-posting.eth' }];
|
||||||
testState.hasMore = true;
|
testState.hasMore = true;
|
||||||
@@ -428,6 +440,30 @@ describe('Catalog', () => {
|
|||||||
root = createRoot(container);
|
root = createRoot(container);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores multiboard catalog state after remounting', async () => {
|
||||||
|
testState.feed = [{ cid: 'all-post', title: 'one', communityAddress: 'music-posting.eth' }];
|
||||||
|
const catalogProps = { feedCacheKey: 'restore-check', viewType: 'all' as const };
|
||||||
|
|
||||||
|
await renderCatalog({
|
||||||
|
catalogProps,
|
||||||
|
initialEntry: '/all/catalog',
|
||||||
|
routePath: '/all/*',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testState.virtuosoInitialScrollTops.at(-1)).toBeUndefined();
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
root = createRoot(container);
|
||||||
|
|
||||||
|
await renderCatalog({
|
||||||
|
catalogProps,
|
||||||
|
initialEntry: '/all/catalog',
|
||||||
|
routePath: '/all/*',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testState.virtuosoInitialScrollTops.at(-1)).toBe(24);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows the empty subscriptions state when there are no subscribed boards to browse', async () => {
|
it('shows the empty subscriptions state when there are no subscribed boards to browse', async () => {
|
||||||
testState.account = { subscriptions: [] };
|
testState.account = { subscriptions: [] };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDeferredValue, useEffect, useMemo, useRef, useCallback } from 'react';
|
import { useDeferredValue, useEffect, useMemo, useRef, useCallback, useLayoutEffect } from 'react';
|
||||||
import { useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Comment, useAccount, useCommunity, useFeed, useAccountComments } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useAccount, useCommunity, useFeed, useAccountComments } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
@@ -37,6 +37,8 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
|||||||
const RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS = 60 * 60;
|
const RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS = 60 * 60;
|
||||||
// Keep the hook on its indexed fast path when this view should not inject local posts.
|
// Keep the hook on its indexed fast path when this view should not inject local posts.
|
||||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||||
|
export const getCatalogRenderFeed = <T,>(processedFeed: readonly T[], deferredProcessedFeed: readonly T[]): readonly T[] =>
|
||||||
|
deferredProcessedFeed.length === 0 && processedFeed.length > 0 ? processedFeed : deferredProcessedFeed;
|
||||||
|
|
||||||
interface CatalogFooterProps {
|
interface CatalogFooterProps {
|
||||||
communityAddresses: string[];
|
communityAddresses: string[];
|
||||||
@@ -464,6 +466,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
}, [sortedFeed, filterItems]);
|
}, [sortedFeed, filterItems]);
|
||||||
|
|
||||||
const deferredProcessedFeed = useDeferredValue(processedFeed);
|
const deferredProcessedFeed = useDeferredValue(processedFeed);
|
||||||
|
const catalogRenderFeed = getCatalogRenderFeed(processedFeed, deferredProcessedFeed);
|
||||||
|
|
||||||
const matchedFilterColors = useMemo(() => {
|
const matchedFilterColors = useMemo(() => {
|
||||||
const nextMatchedFilterColors = new Map<string, string>();
|
const nextMatchedFilterColors = new Map<string, string>();
|
||||||
@@ -473,7 +476,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
return nextMatchedFilterColors;
|
return nextMatchedFilterColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const comment of deferredProcessedFeed) {
|
for (const comment of catalogRenderFeed) {
|
||||||
const cid = comment?.cid;
|
const cid = comment?.cid;
|
||||||
if (!cid) {
|
if (!cid) {
|
||||||
continue;
|
continue;
|
||||||
@@ -486,7 +489,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nextMatchedFilterColors;
|
return nextMatchedFilterColors;
|
||||||
}, [deferredProcessedFeed, filterItems]);
|
}, [catalogRenderFeed, filterItems]);
|
||||||
|
|
||||||
const rowCacheRef = useRef(new Map<string, Comment[]>());
|
const rowCacheRef = useRef(new Map<string, Comment[]>());
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
@@ -498,8 +501,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
const effectiveColumnCount = Math.max(columnCount, 1);
|
const effectiveColumnCount = Math.max(columnCount, 1);
|
||||||
const nextRows: Comment[][] = [];
|
const nextRows: Comment[][] = [];
|
||||||
const nextRowCache = new Map<string, Comment[]>();
|
const nextRowCache = new Map<string, Comment[]>();
|
||||||
for (let i = 0; i < deferredProcessedFeed.length; i += effectiveColumnCount) {
|
for (let i = 0; i < catalogRenderFeed.length; i += effectiveColumnCount) {
|
||||||
const nextRow = deferredProcessedFeed.slice(i, i + effectiveColumnCount);
|
const nextRow = catalogRenderFeed.slice(i, i + effectiveColumnCount);
|
||||||
const rowKey = nextRow.map((post) => post?.cid || '').join('\u0000');
|
const rowKey = nextRow.map((post) => post?.cid || '').join('\u0000');
|
||||||
const cachedRow = rowCacheRef.current.get(rowKey);
|
const cachedRow = rowCacheRef.current.get(rowKey);
|
||||||
const stableRow = cachedRow && cachedRow.length === nextRow.length && cachedRow.every((post, index) => post === nextRow[index]) ? cachedRow : nextRow;
|
const stableRow = cachedRow && cachedRow.length === nextRow.length && cachedRow.every((post, index) => post === nextRow[index]) ? cachedRow : nextRow;
|
||||||
@@ -508,7 +511,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
}
|
}
|
||||||
rowCacheRef.current = nextRowCache;
|
rowCacheRef.current = nextRowCache;
|
||||||
return nextRows;
|
return nextRows;
|
||||||
}, [columnCount, deferredProcessedFeed, isFeedLoaded]);
|
}, [catalogRenderFeed, columnCount, isFeedLoaded]);
|
||||||
|
|
||||||
const catalogMetrics = useMemo(() => readReplyTypographyMetrics(), [themeKey, windowWidth]);
|
const catalogMetrics = useMemo(() => readReplyTypographyMetrics(), [themeKey, windowWidth]);
|
||||||
const rowHeightEstimates = useMemo(
|
const rowHeightEstimates = useMemo(
|
||||||
@@ -545,7 +548,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
}
|
}
|
||||||
}, [isVisible, navigationType]);
|
}, [isVisible, navigationType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isVisible || !shouldVirtualizeCatalog) return;
|
if (!isVisible || !shouldVirtualizeCatalog) return;
|
||||||
|
|
||||||
const currentKey = virtuosoStateKey;
|
const currentKey = virtuosoStateKey;
|
||||||
@@ -592,7 +595,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<hr />
|
<hr />
|
||||||
<div className={styles.catalog}>
|
<div className={styles.catalog}>
|
||||||
{processedFeed?.length !== 0 ? (
|
{catalogRenderFeed.length !== 0 ? (
|
||||||
<>
|
<>
|
||||||
{shouldVirtualizeCatalog ? (
|
{shouldVirtualizeCatalog ? (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
|
|||||||
Reference in New Issue
Block a user