feat(flash board): add SWF posting support (#1145)

This commit is contained in:
Tommaso Casaburi
2026-05-30 16:07:41 +07:00
committed by GitHub
parent 56894700c1
commit 9b3a95dd95
69 changed files with 1372 additions and 63 deletions
+124 -1
View File
@@ -10,17 +10,26 @@ import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../.
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
type TestComment = {
author?: {
displayName?: string;
};
cid?: string;
content?: string;
flairs?: Array<{ text?: string }>;
index?: number;
link?: string;
number?: number | string;
parentCid?: string;
pinned?: boolean;
postNumber?: number | string;
communityAddress?: string;
deleted?: boolean;
postCid?: string;
replyCount?: number;
removed?: boolean;
state?: string;
timestamp?: number;
title?: string;
};
type TestCommunity = {
@@ -42,7 +51,7 @@ const testState = vi.hoisted(() => ({
address: 'music-posting.eth',
features: { postsPerPage: 2 },
},
} as Record<string, { address: string; features?: Record<string, unknown> }>,
} as Record<string, { address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
feed: [] as TestComment[],
feedOptionsCalls: [] as Array<{ communities?: unknown[]; communitiesLength?: number; newerThan?: number; postsPerPage?: number; sortType?: string }>,
feedState: undefined as string | undefined,
@@ -539,6 +548,120 @@ describe('Board', () => {
expect(testState.setEnableInfiniteScrollMock).toHaveBeenCalledWith(true);
});
it('renders flash board posts as table rows instead of the normal feed', 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.community = {
error: undefined,
shortAddress: 'flash-posting.bso',
state: 'ready',
title: '/f/ - Flash',
};
testState.communitySnapshot = {
shortAddress: 'flash-posting.bso',
title: '/f/ - Flash',
};
testState.hasMore = true;
testState.feed = [
{
author: { displayName: 'FlashAnon' },
cid: 'flash-cid',
communityAddress: 'flash-posting.bso',
flairs: [{ text: 'flash:game' }],
link: 'https://files.catbox.moe/movie.swf',
number: 3524333,
postCid: 'flash-cid',
replyCount: 4,
timestamp: 1_704_067_200,
title: 'Flash game',
},
];
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
const table = container.querySelector('#flash-list');
expect(table).toBeTruthy();
expect(container.querySelector('[data-testid="post"]')).toBeNull();
expect(table?.querySelectorAll('tbody tr').length).toBe(1);
expect(table?.textContent).toContain('3524333');
expect(table?.textContent).toContain('FlashAnon');
expect(table?.textContent).toContain('movie.swf');
expect(table?.textContent).toContain('[G]');
expect(table?.textContent).toContain('Flash game');
expect(table?.textContent).toContain('4');
expect(table?.querySelector('a[href="/f/thread/flash-cid"]')?.textContent).toBe('3524333');
expect(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'load_more')).toBeUndefined();
});
it('renders an empty flash table when the board has no posts', 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.community = {
error: undefined,
shortAddress: 'flash-posting.bso',
state: 'succeeded',
title: '/f/ - Flash',
};
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(container.querySelector('[data-testid="post"]')).toBeNull();
expect(table?.textContent).toContain('no posts');
});
it('keeps the flash table in loading state until the empty board feed finishes syncing', 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.community = {
error: undefined,
shortAddress: 'flash-posting.bso',
state: 'ready',
title: '/f/ - Flash',
};
testState.communitySnapshot = {
shortAddress: 'flash-posting.bso',
title: '/f/ - Flash',
};
testState.hasMore = true;
await renderBoard({ initialEntry: '/f', routePath: '/:boardIdentifier/*' });
const table = container.querySelector('#flash-list');
expect(table).toBeTruthy();
expect(table?.textContent).not.toContain('no posts');
expect(table?.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('downloading_board');
});
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 = [
+18 -4
View File
@@ -28,7 +28,9 @@ import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { getNonokoPendingAccountCommentIndex } from '../../lib/utils/post-options-utils';
import { getSearchWithTimeFilter, getTimeFilterSuggestion, type TimeFilterSuggestion } from '../../lib/utils/time-filter-utils';
import { getPretextItemSizeFromElement, resolveFeedVirtualizationMode } from '../../lib/utils/pretext-height-estimates';
import { isFlashDirectory, isFlashDirectoryCode } from '../../lib/flash-tags';
import ErrorDisplay from '../../components/error-display/error-display';
import FlashBoardTable from '../../components/flash-board-table/flash-board-table';
import LoadingEllipsis from '../../components/loading-ellipsis';
import BoardPagination from '../../components/board-pagination';
import { CatalogButton } from '../../components/board-buttons/board-buttons';
@@ -193,12 +195,14 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
const communities = useCommunityIdentifiers(communityAddresses);
const communityIdentifier = useCommunityIdentifier(communityAddress);
const communityDirectory = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : communityAddress);
const requestedBoardIdentifier = boardIdentifierProp || params.boardIdentifier;
const shouldUseFlashTable = !isMultiboardView && (isFlashDirectoryCode(requestedBoardIdentifier) || isFlashDirectory(communityDirectory));
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll);
const isMobile = useIsMobile();
const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView;
const effectiveInfiniteScroll = enableInfiniteScroll || isForcedInfiniteScroll;
const communityDirectory = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : communityAddress);
const effectiveInfiniteScroll = !shouldUseFlashTable && (enableInfiniteScroll || isForcedInfiniteScroll);
const { guiPostsPerPage, maxGuiPages, paginationFeedPostsPerPage, infiniteFeedPostsPerPage } = useBoardFeedPageSize(communityDirectory);
const excludeArchivedFilter = useMemo(
@@ -524,7 +528,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
</div>
</>
)}
{hasMore && !effectiveInfiniteScroll && (
{hasMore && !effectiveInfiniteScroll && !shouldUseFlashTable && (
<div className={mobileFooterStyles.mobileFooterButtons}>
<button type='button' className='button' onClick={() => setEnableInfiniteScroll(true)}>
{t('load_more')}
@@ -553,6 +557,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
subscriptions?.length,
accountCommunityAddresses?.length,
effectiveInfiniteScroll,
shouldUseFlashTable,
isForcedInfiniteScroll,
paginationBasePath,
currentPage,
@@ -635,6 +640,10 @@ 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 shouldShowFlashTableLoading =
shouldUseFlashTable && displayFeed.length === 0 && !(isLoadedCommunityState && isFeedSucceeded) && communityState !== 'failed' && feedState !== 'failed';
return (
<>
@@ -646,7 +655,12 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
</div>
)}
{shouldShowUnverifiedAddressWarning && <output className={styles.addressWarning}>{t('board_address_unverified_warning')}</output>}
{effectiveInfiniteScroll ? (
{shouldUseFlashTable ? (
<>
<FlashBoardTable boardBasePath={paginationBasePath} isLoading={shouldShowFlashTableLoading} posts={displayFeed} />
<footerComponents.Footer />
</>
) : effectiveInfiniteScroll ? (
<Virtuoso
defaultItemHeight={defaultBoardItemHeight}
{...boardSizingProps}
+4
View File
@@ -148,6 +148,10 @@
vertical-align: -1px;
}
.flashTag {
font-weight: bold;
}
.authorFlag {
display: inline-block;
flex: 0 0 auto;