mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'master' of github.com:bitsocialnet/5chan
This commit is contained in:
@@ -61,6 +61,46 @@ describe('getRawBoardThreadState', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('treats explicit empty page CIDs as a fully loaded empty board', () => {
|
||||
const community = {
|
||||
posts: {
|
||||
pageCids: {},
|
||||
pages: {},
|
||||
},
|
||||
updatedAt: 1781773422,
|
||||
} as Community;
|
||||
|
||||
expect(
|
||||
getRawBoardThreadState({
|
||||
accountId: undefined,
|
||||
communitiesPages: {} as CommunitiesPages,
|
||||
community,
|
||||
sortType: 'active',
|
||||
}),
|
||||
).toMatchObject({
|
||||
isFullyLoaded: true,
|
||||
rootThreadCids: new Set<string>(),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not treat placeholder empty page CIDs as fully loaded', () => {
|
||||
const community = {
|
||||
posts: {
|
||||
pageCids: {},
|
||||
pages: {},
|
||||
},
|
||||
} as Community;
|
||||
|
||||
expect(
|
||||
getRawBoardThreadState({
|
||||
accountId: undefined,
|
||||
communitiesPages: {} as CommunitiesPages,
|
||||
community,
|
||||
sortType: 'active',
|
||||
}).isFullyLoaded,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('walks stored board pages without importing side-effectful stores', () => {
|
||||
const community = {
|
||||
posts: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Comment, CommunitiesPages, Community, CommunityPage } from '@bitsocial/bitsocial-react-hooks';
|
||||
|
||||
export type RawBoardThreadState = {
|
||||
hasExplicitEmptyPageCids: boolean;
|
||||
isFullyLoaded: boolean;
|
||||
rootThreadCids: Set<string>;
|
||||
};
|
||||
|
||||
const EMPTY_RAW_BOARD_THREAD_STATE: RawBoardThreadState = {
|
||||
hasExplicitEmptyPageCids: false,
|
||||
isFullyLoaded: false,
|
||||
rootThreadCids: new Set<string>(),
|
||||
};
|
||||
@@ -80,6 +82,7 @@ export const getRawBoardThreadState = ({
|
||||
|
||||
if (pages.length > 0) {
|
||||
return {
|
||||
hasExplicitEmptyPageCids: false,
|
||||
isFullyLoaded: !pages[pages.length - 1]?.nextCid,
|
||||
rootThreadCids,
|
||||
};
|
||||
@@ -88,6 +91,8 @@ export const getRawBoardThreadState = ({
|
||||
const hasPageCid = Boolean(community.posts?.pageCids?.[sortType]);
|
||||
const preloadedPages = (preloadedSortPage ? [preloadedSortPage] : []) as Array<{ comments?: Comment[]; nextCid?: string }>;
|
||||
const hasCompletePreloadedPage = !hasPageCid && preloadedPages.some((page) => Array.isArray(page?.comments)) && preloadedPages.every((page) => !page?.nextCid);
|
||||
const hasFetchedCommunityUpdate = typeof community.updatedAt === 'number' || typeof community.updateCid === 'string';
|
||||
const hasExplicitEmptyPageCids = hasFetchedCommunityUpdate && Boolean(community.posts?.pageCids && !hasPageCid);
|
||||
|
||||
if (hasCompletePreloadedPage) {
|
||||
for (const page of preloadedPages) {
|
||||
@@ -96,7 +101,8 @@ export const getRawBoardThreadState = ({
|
||||
}
|
||||
|
||||
return {
|
||||
isFullyLoaded: hasCompletePreloadedPage,
|
||||
hasExplicitEmptyPageCids,
|
||||
isFullyLoaded: hasCompletePreloadedPage || hasExplicitEmptyPageCids,
|
||||
rootThreadCids,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ type TestComment = {
|
||||
type TestCommunity = {
|
||||
error?: Error;
|
||||
nameResolved?: boolean;
|
||||
updatedAt?: number;
|
||||
posts?: {
|
||||
pageCids?: Record<string, string>;
|
||||
pages?: Record<string, { comments?: TestComment[]; nextCid?: string }>;
|
||||
@@ -687,6 +688,43 @@ describe('Board', () => {
|
||||
expect(container.querySelectorAll('[data-testid="loading-ellipsis"]').length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders an empty flash table when a loaded board reports explicit empty page cids', async () => {
|
||||
testState.directories = [{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash' }];
|
||||
testState.directoryByAddress = {
|
||||
'flash-posting.bso': {
|
||||
address: 'flash-posting.bso',
|
||||
directoryCode: 'f',
|
||||
features: { postsPerPage: 50 },
|
||||
title: '/f/ - Flash',
|
||||
},
|
||||
};
|
||||
testState.resolvedCommunityAddress = 'flash-posting.bso';
|
||||
testState.feedState = 'fetching-ipns';
|
||||
testState.hasMore = true;
|
||||
testState.community = {
|
||||
error: undefined,
|
||||
posts: {
|
||||
pageCids: {},
|
||||
pages: {},
|
||||
},
|
||||
shortAddress: 'flash-posting.bso',
|
||||
state: 'succeeded',
|
||||
title: '/f/ - Flash',
|
||||
updatedAt: 1781773422,
|
||||
};
|
||||
testState.communitySnapshot = {
|
||||
shortAddress: 'flash-posting.bso',
|
||||
title: '/f/ - Flash',
|
||||
};
|
||||
|
||||
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
|
||||
|
||||
const table = container.querySelector('#flash-list');
|
||||
expect(table).toBeTruthy();
|
||||
expect(table?.textContent).toContain('no posts');
|
||||
expect(table?.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('inserts a nonoko pending account comment after pinned posts on the redirected board index', async () => {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
testState.feed = [
|
||||
@@ -1043,6 +1081,71 @@ describe('Board', () => {
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps loading when an empty preloaded board page finishes before the feed', async () => {
|
||||
testState.feedStateString = undefined;
|
||||
testState.feedState = 'fetching-ipns';
|
||||
testState.hasMore = true;
|
||||
testState.community = {
|
||||
error: undefined,
|
||||
shortAddress: 'music-posting.eth',
|
||||
state: 'succeeded',
|
||||
title: '/mu/ - Music',
|
||||
};
|
||||
markRawBoardThreadsFullyLoaded();
|
||||
|
||||
await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' });
|
||||
|
||||
expect(container.textContent).not.toContain('no_threads');
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('downloading_board');
|
||||
expect(container.textContent).toContain('load_more');
|
||||
});
|
||||
|
||||
it('shows no threads when a loaded board reports explicit empty page cids', async () => {
|
||||
testState.feedStateString = 'Downloading board from peers';
|
||||
testState.feedState = 'fetching-ipns';
|
||||
testState.hasMore = true;
|
||||
testState.community = {
|
||||
error: undefined,
|
||||
posts: {
|
||||
pageCids: {},
|
||||
pages: {},
|
||||
},
|
||||
shortAddress: 'blog.bitsocial.bso',
|
||||
state: 'succeeded',
|
||||
title: 'Bitsocial Updates',
|
||||
updatedAt: 1781773422,
|
||||
};
|
||||
testState.communitySnapshot = {
|
||||
shortAddress: 'blog.bitsocial.bso',
|
||||
title: 'Bitsocial Updates',
|
||||
};
|
||||
|
||||
await renderBoard({ initialEntry: '/blog.bitsocial.bso', routePath: '/:boardIdentifier/*' });
|
||||
|
||||
expect(container.textContent).toContain('no_threads');
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
||||
expect(container.textContent).not.toContain('load_more');
|
||||
});
|
||||
|
||||
it('keeps loading when raw board pages contain threads but the feed has not caught up', async () => {
|
||||
testState.feedStateString = undefined;
|
||||
testState.feedState = 'fetching-ipns';
|
||||
testState.hasMore = true;
|
||||
testState.community = {
|
||||
error: undefined,
|
||||
shortAddress: 'music-posting.eth',
|
||||
state: 'succeeded',
|
||||
title: '/mu/ - Music',
|
||||
};
|
||||
markRawBoardThreadsFullyLoaded([{ cid: 'post-1' }]);
|
||||
|
||||
await renderBoard({ initialEntry: '/mu', routePath: '/:boardIdentifier/*' });
|
||||
|
||||
expect(container.textContent).not.toContain('no_threads');
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('downloading_board');
|
||||
expect(container.textContent).toContain('load_more');
|
||||
});
|
||||
|
||||
it('does not show no threads after board metadata loads but raw thread pages are still missing', async () => {
|
||||
testState.feedStateString = undefined;
|
||||
testState.feedState = 'succeeded';
|
||||
|
||||
@@ -60,6 +60,7 @@ interface BoardFooterProps {
|
||||
combinedFeedLength: number;
|
||||
isSingleCommunityBoard: boolean;
|
||||
isRawBoardThreadStateFullyLoaded: boolean;
|
||||
isKnownEmptySingleCommunityBoard: boolean;
|
||||
isInSubscriptionsView: boolean;
|
||||
isInModView: boolean;
|
||||
currentTimeFilterName: string;
|
||||
@@ -84,6 +85,7 @@ const BoardFooter = ({
|
||||
combinedFeedLength,
|
||||
isSingleCommunityBoard,
|
||||
isRawBoardThreadStateFullyLoaded,
|
||||
isKnownEmptySingleCommunityBoard,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
currentTimeFilterName,
|
||||
@@ -102,7 +104,9 @@ const BoardFooter = ({
|
||||
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
||||
const isFeedSucceeded = feedState === 'succeeded';
|
||||
const isFeedFailed = feedState === 'failed';
|
||||
const canShowNoThreads = isSingleCommunityBoard ? isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded : isFeedSucceeded && !hasMore;
|
||||
const canShowNoThreads =
|
||||
isKnownEmptySingleCommunityBoard ||
|
||||
(isSingleCommunityBoard ? isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded : isFeedSucceeded && !hasMore);
|
||||
const isEmptyFeedLoading = combinedFeedLength === 0 && !canShowNoThreads && (isSingleCommunityBoard ? communityState !== 'failed' : !isFeedFailed);
|
||||
const showFooterLoading = showLoadingEllipsis && (hasMore || isEmptyFeedLoading);
|
||||
|
||||
@@ -472,6 +476,14 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
[account?.id, communitiesPages, communityData, isMultiboardView],
|
||||
);
|
||||
const isRawBoardThreadStateFullyLoaded = rawBoardThreadState?.isFullyLoaded ?? false;
|
||||
const hasExplicitEmptyPageCids = rawBoardThreadState?.hasExplicitEmptyPageCids ?? false;
|
||||
const isRawBoardThreadStateEmpty = isRawBoardThreadStateFullyLoaded && (rawBoardThreadState?.rootThreadCids.size ?? 0) === 0;
|
||||
const isSingleCommunityBoard = !isInAllView && !isInSubscriptionsView && !isInModView;
|
||||
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
||||
const isFeedSucceeded = feedState === 'succeeded';
|
||||
const isKnownEmptySingleCommunityBoard =
|
||||
isSingleCommunityBoard && combinedFeed.length === 0 && isLoadedCommunityState && isRawBoardThreadStateEmpty && (hasExplicitEmptyPageCids || isFeedSucceeded);
|
||||
const effectiveHasMore = isKnownEmptySingleCommunityBoard ? false : hasMore;
|
||||
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : communityTitle;
|
||||
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
@@ -483,11 +495,12 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
{shouldUseFlashTable ? null : (
|
||||
<BoardFooter
|
||||
communityAddresses={communityAddresses}
|
||||
hasMore={hasMore}
|
||||
hasMore={effectiveHasMore}
|
||||
feedState={feedState}
|
||||
combinedFeedLength={combinedFeed.length}
|
||||
isSingleCommunityBoard={!isInAllView && !isInSubscriptionsView && !isInModView}
|
||||
isSingleCommunityBoard={isSingleCommunityBoard}
|
||||
isRawBoardThreadStateFullyLoaded={isRawBoardThreadStateFullyLoaded}
|
||||
isKnownEmptySingleCommunityBoard={isKnownEmptySingleCommunityBoard}
|
||||
isInSubscriptionsView={isInSubscriptionsView}
|
||||
isInModView={isInModView}
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
@@ -552,7 +565,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasMore && !effectiveInfiniteScroll && !shouldUseFlashTable && (
|
||||
{effectiveHasMore && !effectiveInfiniteScroll && !shouldUseFlashTable && (
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<button type='button' className='button' onClick={() => setEnableInfiniteScroll(true)}>
|
||||
{t('load_more')}
|
||||
@@ -566,9 +579,11 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
}),
|
||||
[
|
||||
communityAddresses,
|
||||
hasMore,
|
||||
effectiveHasMore,
|
||||
combinedFeed.length,
|
||||
isRawBoardThreadStateFullyLoaded,
|
||||
isKnownEmptySingleCommunityBoard,
|
||||
isSingleCommunityBoard,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
@@ -665,9 +680,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
communityIdentifier.publicKey.length > 0 &&
|
||||
communityData?.nameResolved === false;
|
||||
const displayFeed = effectiveInfiniteScroll ? combinedFeed : currentPageFeed;
|
||||
const isLoadedCommunityState = communityState === 'succeeded' || communityState === 'ready';
|
||||
const isFeedSucceeded = feedState === 'succeeded';
|
||||
const canShowEmptyFlashTable = isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded;
|
||||
const canShowEmptyFlashTable = hasExplicitEmptyPageCids || (isLoadedCommunityState && isFeedSucceeded && isRawBoardThreadStateFullyLoaded);
|
||||
const shouldShowFlashTableLoading = shouldUseFlashTable && displayFeed.length === 0 && !canShowEmptyFlashTable && communityState !== 'failed' && feedState !== 'failed';
|
||||
|
||||
return (
|
||||
@@ -697,7 +710,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
itemContent={boardItemContent}
|
||||
useWindowScroll={true}
|
||||
components={footerComponents}
|
||||
endReached={hasMore ? loadMore : undefined}
|
||||
endReached={effectiveHasMore ? loadMore : undefined}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
|
||||
Reference in New Issue
Block a user