fix(board-buttons): hide catalog controls on flash upload boards

Flash boards do not use catalog view, so hide catalog links and OP search
without leaving empty bracket placeholders in desktop board controls.
This commit is contained in:
Tommaso Casaburi
2026-06-01 12:42:21 +07:00
parent d3b5c40321
commit b686def1a1
12 changed files with 181 additions and 35 deletions
+12
View File
@@ -416,6 +416,18 @@ describe('App', () => {
expect(container.querySelector('[data-testid="not-found-view"]')).toBeTruthy(); expect(container.querySelector('[data-testid="not-found-view"]')).toBeTruthy();
}); });
it('redirects flash board catalog routes to not-found', async () => {
testState.directories = [
{ address: 'music-posting.eth', title: '/mu/ - Music', nsfw: false },
{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash', nsfw: true },
];
await renderApp('/f/catalog');
expect(latestLocation).toBe('/not-found');
expect(container.querySelector('[data-testid="not-found-view"]')).toBeTruthy();
});
it('renders the pass route as a global static page', async () => { it('renders the pass route as a global static page', async () => {
await renderApp('/pass'); await renderApp('/pass');
+7 -2
View File
@@ -29,6 +29,7 @@ import {
isModQueueRoute, isModQueueRoute,
isValidBoardModRoute, isValidBoardModRoute,
isValidModRoute, isValidModRoute,
isFlashBoardRoute,
} from './lib/utils/route-utils'; } from './lib/utils/route-utils';
import styles from './app.module.css'; import styles from './app.module.css';
import { DesktopBoardButtons, MobileAllFeedFilter, MobileBoardButtons } from './components/board-buttons'; import { DesktopBoardButtons, MobileAllFeedFilter, MobileBoardButtons } from './components/board-buttons';
@@ -112,6 +113,10 @@ const BoardLayout = () => {
return <Navigate to='/not-found' replace />; return <Navigate to='/not-found' replace />;
} }
if (isCatalogView(pathname, params) && isFlashBoardRoute(boardIdentifier, directories)) {
return <Navigate to='/not-found' replace />;
}
// Invalid /mod/ paths (e.g. /mod/modqueue, /mod/asdoijasd) -> not-found // Invalid /mod/ paths (e.g. /mod/modqueue, /mod/asdoijasd) -> not-found
if (pathname.startsWith('/mod/') && !isValidModRoute(pathname)) { if (pathname.startsWith('/mod/') && !isValidModRoute(pathname)) {
return <Navigate to='/not-found' replace />; return <Navigate to='/not-found' replace />;
@@ -335,12 +340,12 @@ const App = () => {
<Route path='/subs/*' element={<Navigate to='/not-found' replace />} /> <Route path='/subs/*' element={<Navigate to='/not-found' replace />} />
<Route path='/mod/*' element={<Navigate to='/not-found' replace />} /> <Route path='/mod/*' element={<Navigate to='/not-found' replace />} />
<Route path='/:boardIdentifier/catalog' element={catalogFeedElement} />
<Route path='/:boardIdentifier/catalog/settings' element={catalogFeedElement} />
<Route path='/:boardIdentifier/:pageNumber' element={boardFeedElement} /> <Route path='/:boardIdentifier/:pageNumber' element={boardFeedElement} />
<Route path='/:boardIdentifier/:pageNumber/settings' element={boardFeedElement} /> <Route path='/:boardIdentifier/:pageNumber/settings' element={boardFeedElement} />
<Route path='/:boardIdentifier' element={boardFeedElement} /> <Route path='/:boardIdentifier' element={boardFeedElement} />
<Route path='/:boardIdentifier/settings' element={boardFeedElement} /> <Route path='/:boardIdentifier/settings' element={boardFeedElement} />
<Route path='/:boardIdentifier/catalog' element={catalogFeedElement} />
<Route path='/:boardIdentifier/catalog/settings' element={catalogFeedElement} />
<Route path='/:boardIdentifier/archive' element={<Archive />} /> <Route path='/:boardIdentifier/archive' element={<Archive />} />
<Route path='/:boardIdentifier/archive/settings' element={<Archive />} /> <Route path='/:boardIdentifier/archive/settings' element={<Archive />} />
<Route path='/:boardIdentifier/directory' element={<Directory />} /> <Route path='/:boardIdentifier/directory' element={<Directory />} />
@@ -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 } from 'react-router-dom'; import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DesktopBoardButtons, MobileBoardButtons } from '../board-buttons'; import { DesktopBoardButtons, BracketedCatalogButton, MobileBoardButtons } from '../board-buttons';
import useThreadLiveUpdatesStore from '../../../stores/use-thread-live-updates-store'; import useThreadLiveUpdatesStore from '../../../stores/use-thread-live-updates-store';
import useHiddenCatalogThreadsStore from '../../../stores/use-hidden-catalog-threads-store'; import useHiddenCatalogThreadsStore from '../../../stores/use-hidden-catalog-threads-store';
import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../../../lib/utils/time-filter-utils'; import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../../../lib/utils/time-filter-utils';
@@ -13,6 +13,7 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
type DirectoryEntry = { type DirectoryEntry = {
address: string; address: string;
directoryCode?: string;
features?: { requirePostLinkIsMedia?: boolean }; features?: { requirePostLinkIsMedia?: boolean };
name?: string; name?: string;
publicKey?: string; publicKey?: string;
@@ -120,7 +121,7 @@ vi.mock('../../../hooks/use-post-page-number', () => ({
vi.mock('../../../hooks/use-directories', () => ({ vi.mock('../../../hooks/use-directories', () => ({
findDirectoryByAddress: (directories: DirectoryEntry[], address?: string) => findDirectoryByAddress: (directories: DirectoryEntry[], address?: string) =>
directories.find((entry) => address && [entry.address, entry.name, entry.publicKey].includes(address)), directories.find((entry) => address && [entry.address, entry.name, entry.publicKey, entry.directoryCode].includes(address)),
useDirectories: () => testState.directories, useDirectories: () => testState.directories,
useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address), useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address),
})); }));
@@ -360,6 +361,28 @@ describe('BoardButtons', () => {
expect(container.textContent).not.toContain('directory'); expect(container.textContent).not.toContain('directory');
}); });
it('does not render catalog or OP search controls on flash upload boards', async () => {
testState.directories = [
{ address: 'music-posting.eth', features: {}, name: 'music-posting.eth', publicKey: 'music-public-key', title: '/mu/ - Music' },
{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash' },
];
testState.resolvedCommunityAddress = 'flash-posting.bso';
await renderWithRoute(createElement(DesktopBoardButtons), '/f');
expect(findButtonLink('catalog')).toBeUndefined();
expect(container.querySelector('input[type="text"]')).toBeNull();
expect(container.textContent).not.toMatch(/\[\s*\]/);
});
it('does not render bracketed catalog controls on flash upload boards', async () => {
testState.directories = [{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash' }];
await renderWithRoute(createElement(BracketedCatalogButton, { address: 'flash-posting.bso' }), '/f');
expect(container.textContent?.trim()).toBe('');
});
it('renders desktop catalog controls and wires sort, style, filter, and refresh updates', async () => { it('renders desktop catalog controls and wires sort, style, filter, and refresh updates', async () => {
testState.filteredCount = 4; testState.filteredCount = 4;
+56 -6
View File
@@ -7,7 +7,7 @@ import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories'; import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses'; import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses'; import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils'; import { getBoardPath, isDirectoryRoute, isFlashBoardRoute } from '../../lib/utils/route-utils';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address'; import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers'; import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import useSafeAccountComment from '../../hooks/use-safe-account-comment';
@@ -45,6 +45,17 @@ interface BoardButtonsProps {
const EMPTY_COMMUNITY_ADDRESSES: string[] = []; const EMPTY_COMMUNITY_ADDRESSES: string[] = [];
export const shouldShowCatalogButton = (
boardIdentifier: string | undefined,
directories: ReturnType<typeof useDirectories>,
{ isInAllView, isInSubscriptionsView, isInModView }: Pick<BoardButtonsProps, 'isInAllView' | 'isInSubscriptionsView' | 'isInModView'>,
): boolean => {
if (isInAllView || isInSubscriptionsView || isInModView) {
return true;
}
return !isFlashBoardRoute(boardIdentifier, directories);
};
const getMultiboardPath = ({ const getMultiboardPath = ({
isInAllView, isInAllView,
isInCatalogView, isInCatalogView,
@@ -67,9 +78,14 @@ const getMultiboardPath = ({
export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const params = useParams();
const directories = useDirectories(); const directories = useDirectories();
const { timeFilterValue } = useTimeFilter(); const { timeFilterValue } = useTimeFilter();
if (!shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView })) {
return null;
}
const createCatalogLink = () => { const createCatalogLink = () => {
const multiboardPath = getMultiboardPath({ isInAllView, isInCatalogView: true, isInSubscriptionsView, isInModView }); const multiboardPath = getMultiboardPath({ isInAllView, isInCatalogView: true, isInSubscriptionsView, isInModView });
if (multiboardPath) { if (multiboardPath) {
@@ -94,6 +110,20 @@ export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isI
); );
}; };
export const BracketedCatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const params = useParams();
const directories = useDirectories();
if (!shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView })) {
return null;
}
return (
<span>
[<CatalogButton address={address} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</span>
);
};
export const ArchiveButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { export const ArchiveButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -526,13 +556,16 @@ export const MobileBoardButtons = () => {
const directories = useDirectories(); const directories = useDirectories();
const boardIdentifier = params.boardIdentifier; const boardIdentifier = params.boardIdentifier;
const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories); const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
const showCatalogButton = shouldShowCatalogButton(boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView });
return ( return (
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}> <div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
{isInPostView || isInPendingPostPage ? ( {isInPostView || isInPendingPostPage ? (
<> <>
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} /> <ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} /> {showCatalogButton && (
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
{showBottomButton && <BottomButton />} {showBottomButton && <BottomButton />}
<div className={styles.secondRow}> <div className={styles.secondRow}>
<UpdateButton /> <UpdateButton />
@@ -592,7 +625,9 @@ export const MobileBoardButtons = () => {
) : ( ) : (
<> <>
{showBottomButton && <BottomButton />} {showBottomButton && <BottomButton />}
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} /> {showCatalogButton && (
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
<RefreshButton /> <RefreshButton />
<div className={styles.secondRow}> <div className={styles.secondRow}>
{showDirectoryButton && <DirectoryButton />} {showDirectoryButton && <DirectoryButton />}
@@ -711,6 +746,7 @@ export const DesktopBoardButtons = () => {
const directories = useDirectories(); const directories = useDirectories();
const boardIdentifier = params.boardIdentifier; const boardIdentifier = params.boardIdentifier;
const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories); const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
const showCatalogButton = shouldShowCatalogButton(boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView });
return ( return (
<> <>
@@ -718,8 +754,13 @@ export const DesktopBoardButtons = () => {
<div className={styles.desktopBoardButtons}> <div className={styles.desktopBoardButtons}>
{isInPostView || isInPendingPostPage ? ( {isInPostView || isInPendingPostPage ? (
<> <>
[<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [ [<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] {showCatalogButton && (
<>
{' '}
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</>
)}
{showBottomButton && ( {showBottomButton && (
<> <>
{' '} {' '}
@@ -762,7 +803,12 @@ export const DesktopBoardButtons = () => {
) : ( ) : (
<> <>
<SearchOPsBar /> <SearchOPsBar />
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] {showCatalogButton && (
<>
{' '}
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</>
)}
{!(isInAllView || isInSubscriptionsView || isInModView) && ( {!(isInAllView || isInSubscriptionsView || isInModView) && (
<> <>
{' '} {' '}
@@ -845,6 +891,10 @@ const SearchOPsBar = () => {
const resolvedAddress = useResolvedCommunityAddress(); const resolvedAddress = useResolvedCommunityAddress();
const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, directories) : params?.boardIdentifier || params?.communityAddress; const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, directories) : params?.boardIdentifier || params?.communityAddress;
if (!shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView })) {
return null;
}
const handleSearch = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleSearch = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
const searchQuery = (event.target as HTMLInputElement).value.trim(); const searchQuery = (event.target as HTMLInputElement).value.trim();
@@ -9,6 +9,7 @@ import BoardPagination from '../board-pagination';
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>;
const testState = vi.hoisted(() => ({ const testState = vi.hoisted(() => ({
directories: [] as Array<{ address: string; directoryCode?: string; title?: string }>,
enableInfiniteScroll: false, enableInfiniteScroll: false,
navigateMock: vi.fn(), navigateMock: vi.fn(),
setEnableInfiniteScrollMock: vi.fn(), setEnableInfiniteScrollMock: vi.fn(),
@@ -36,6 +37,12 @@ vi.mock('../../../stores/use-feed-view-settings-store', () => ({
}), }),
})); }));
vi.mock('../../../hooks/use-directories', () => ({
findDirectoryByAddress: (directories: Array<{ address: string; directoryCode?: string; title?: string }>, address?: string) =>
directories.find((entry) => address && (entry.address === address || entry.directoryCode === address || (entry.title && entry.title.includes(`/${address}/`)))),
useDirectories: () => testState.directories,
}));
vi.mock('../../style-selector/style-selector', () => ({ vi.mock('../../style-selector/style-selector', () => ({
default: () => createElement('div', { 'data-testid': 'style-selector' }, 'style-selector'), default: () => createElement('div', { 'data-testid': 'style-selector' }, 'style-selector'),
})); }));
@@ -53,6 +60,7 @@ describe('BoardPagination', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
testState.enableInfiniteScroll = false; testState.enableInfiniteScroll = false;
testState.directories = [{ address: 'music-posting.eth', directoryCode: 'mu', title: '/mu/ - Music' }];
container = document.createElement('div'); container = document.createElement('div');
document.body.appendChild(container); document.body.appendChild(container);
@@ -116,6 +124,14 @@ describe('BoardPagination', () => {
expect(container.textContent).not.toContain('archive'); expect(container.textContent).not.toContain('archive');
}); });
it('hides the catalog link on flash upload boards', () => {
testState.directories = [{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash' }];
renderPagination(createElement(BoardPagination, { basePath: '/f', currentPage: 1, footerStyle: true, totalPages: 3 }));
expect(container.textContent).not.toContain('catalog');
expect(container.textContent).toContain('archive');
});
it('returns nothing for single-page non-footer pagination', () => { it('returns nothing for single-page non-footer pagination', () => {
renderPagination(createElement(BoardPagination, { basePath: '/mu', currentPage: 1, totalPages: 1 })); renderPagination(createElement(BoardPagination, { basePath: '/mu', currentPage: 1, totalPages: 1 }));
@@ -1,6 +1,8 @@
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store'; import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
import { useDirectories } from '../../hooks/use-directories';
import { isFlashBoardRoute } from '../../lib/utils/route-utils';
import StyleSelector from '../style-selector/style-selector'; import StyleSelector from '../style-selector/style-selector';
import footerStyles from '../footer/footer.module.css'; import footerStyles from '../footer/footer.module.css';
import styles from './board-pagination.module.css'; import styles from './board-pagination.module.css';
@@ -19,10 +21,13 @@ interface BoardPaginationProps {
const BoardPagination = ({ basePath, currentPage, search = '', totalPages, footerStyle = false, isMultiboard = false }: BoardPaginationProps) => { const BoardPagination = ({ basePath, currentPage, search = '', totalPages, footerStyle = false, isMultiboard = false }: BoardPaginationProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const directories = useDirectories();
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll); const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll);
const pageHref = (page: number) => ({ pathname: page === 1 ? basePath : `${basePath}/${page}`, search }); const pageHref = (page: number) => ({ pathname: page === 1 ? basePath : `${basePath}/${page}`, search });
const boardIdentifier = basePath.replace(/^\//, '').split('/')[0];
const showCatalogLink = !isFlashBoardRoute(boardIdentifier, directories);
const catalogHref = { pathname: `${basePath}/catalog`, search }; const catalogHref = { pathname: `${basePath}/catalog`, search };
if (totalPages <= 1 && !footerStyle) { if (totalPages <= 1 && !footerStyle) {
@@ -78,9 +83,11 @@ const BoardPagination = ({ basePath, currentPage, search = '', totalPages, foote
) : ( ) : (
<span className={styles.footerNavPlainDisabled}>{t('next')}</span> <span className={styles.footerNavPlainDisabled}>{t('next')}</span>
)} )}
<Link to={catalogHref} className={styles.pagelistSeparatorLink}> {showCatalogLink && (
{t('catalog')} <Link to={catalogHref} className={styles.pagelistSeparatorLink}>
</Link> {t('catalog')}
</Link>
)}
<Link to={archiveHref} className={styles.pagelistSeparatorLink}> <Link to={archiveHref} className={styles.pagelistSeparatorLink}>
{t('archive')} {t('archive')}
</Link> </Link>
@@ -18,6 +18,11 @@ import {
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>;
const testState = vi.hoisted(() => ({ const testState = vi.hoisted(() => ({
directories: [{ address: 'music-posting.eth', directoryCode: 'mu', title: '/mu/ - Music' }] as Array<{
address: string;
directoryCode?: string;
title?: string;
}>,
directoryEntry: { features: {} } as { features?: Record<string, unknown> } | undefined, directoryEntry: { features: {} } as { features?: Record<string, unknown> } | undefined,
linkCount: 2, linkCount: 2,
openReplyModalEmptyMock: vi.fn(), openReplyModalEmptyMock: vi.fn(),
@@ -65,6 +70,7 @@ vi.mock('../../board-buttons/board-buttons', () => ({
isInModView?: boolean; isInModView?: boolean;
isInSubscriptionsView?: boolean; isInSubscriptionsView?: boolean;
}) => createElement('button', { 'data-testid': 'catalog-button', type: 'button' }, `${address}|${isInAllView}|${isInSubscriptionsView}|${isInModView}`), }) => createElement('button', { 'data-testid': 'catalog-button', type: 'button' }, `${address}|${isInAllView}|${isInSubscriptionsView}|${isInModView}`),
shouldShowCatalogButton: () => true,
PostPageStats: () => createElement('div', { 'data-testid': 'post-page-stats' }, 'post-page-stats'), PostPageStats: () => createElement('div', { 'data-testid': 'post-page-stats' }, 'post-page-stats'),
RefreshButton: () => createElement('button', { type: 'button' }, 'refresh-button'), RefreshButton: () => createElement('button', { type: 'button' }, 'refresh-button'),
ReturnButton: ({ ReturnButton: ({
@@ -97,6 +103,7 @@ vi.mock('../../../hooks/use-post-page-number', () => ({
})); }));
vi.mock('../../../hooks/use-directories', () => ({ vi.mock('../../../hooks/use-directories', () => ({
useDirectories: () => testState.directories,
useDirectoryByAddress: () => testState.directoryEntry, useDirectoryByAddress: () => testState.directoryEntry,
})); }));
+23 -8
View File
@@ -8,6 +8,7 @@ import {
CatalogSearchResultsLabel, CatalogSearchResultsLabel,
ReturnButton, ReturnButton,
CatalogButton, CatalogButton,
shouldShowCatalogButton,
TopButton, TopButton,
UpdateButton, UpdateButton,
AutoButton, AutoButton,
@@ -19,7 +20,7 @@ import useReplyModalStore from '../../stores/use-reply-modal-store';
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store'; import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import { usePostPageNumber } from '../../hooks/use-post-page-number'; import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectoryByAddress } from '../../hooks/use-directories'; import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers'; import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import styles from './footer.module.css'; import styles from './footer.module.css';
@@ -82,15 +83,21 @@ interface CatalogFooterFirstRowProps {
} }
export const CatalogFooterFirstRow = ({ communityAddress, isInAllView = false, isInSubscriptionsView = false, isInModView = false }: CatalogFooterFirstRowProps) => { export const CatalogFooterFirstRow = ({ communityAddress, isInAllView = false, isInSubscriptionsView = false, isInModView = false }: CatalogFooterFirstRowProps) => {
const params = useParams();
const directories = useDirectories();
const showCatalogButton = shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView });
return ( return (
<div className={styles.footerRow}> <div className={styles.footerRow}>
<div className={styles.footerLeft}> <div className={styles.footerLeft}>
<span> <span>
[<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</span> </span>
<span> {showCatalogButton && (
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] <span>
</span> [<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</span>
)}
<span> <span>
[<TopButton />] [<TopButton />]
</span> </span>
@@ -155,6 +162,8 @@ export const ThreadFooterFirstRow = ({ postCid, threadNumber, communityAddress,
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModView = isModView(location.pathname); const isInModView = isModView(location.pathname);
const directories = useDirectories();
const showCatalogButton = shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView });
const handlePostReplyClick = () => { const handlePostReplyClick = () => {
if (isThreadClosed) return; if (isThreadClosed) return;
@@ -167,9 +176,11 @@ export const ThreadFooterFirstRow = ({ postCid, threadNumber, communityAddress,
<span> <span>
[<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</span> </span>
<span> {showCatalogButton && (
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] <span>
</span> [<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
</span>
)}
<span> <span>
[<TopButton />] [<TopButton />]
</span> </span>
@@ -234,6 +245,8 @@ export const ThreadFooterMobile = ({ postCid, threadNumber, communityAddress, is
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModView = isModView(location.pathname); const isInModView = isModView(location.pathname);
const directories = useDirectories();
const showCatalogButton = shouldShowCatalogButton(params.boardIdentifier, directories, { isInAllView, isInSubscriptionsView, isInModView });
const communityIdentifier = useCommunityIdentifier(communityAddress); const communityIdentifier = useCommunityIdentifier(communityAddress);
const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier }); const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
@@ -258,7 +271,9 @@ export const ThreadFooterMobile = ({ postCid, threadNumber, communityAddress, is
</div> </div>
<div className={styles.mobileFooterButtons}> <div className={styles.mobileFooterButtons}>
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} /> <ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} /> {showCatalogButton && (
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
<TopButton /> <TopButton />
</div> </div>
<div className={styles.mobileFooterButtons}> <div className={styles.mobileFooterButtons}>
@@ -13,6 +13,7 @@ import {
isDirectoryListRoute, isDirectoryListRoute,
isDirectoryRoute, isDirectoryRoute,
isFeedRoute, isFeedRoute,
isFlashBoardRoute,
isLegacyBoardModQueueRoute, isLegacyBoardModQueueRoute,
isModQueueRoute, isModQueueRoute,
isPendingPostRoute, isPendingPostRoute,
@@ -33,6 +34,7 @@ const communities = [
title: '/mu/ - Music', title: '/mu/ - Music',
}, },
{ address: 'random.eth', directoryCode: 'b', title: 'Random' }, { address: 'random.eth', directoryCode: 'b', title: 'Random' },
{ address: 'flash-posting.bso', directoryCode: 'f', title: '/f/ - Flash' },
]; ];
beforeEach(() => { beforeEach(() => {
@@ -66,6 +68,11 @@ describe('directory mapping helpers', () => {
expect(isDirectoryRoute('business.eth', communities)).toBe(false); expect(isDirectoryRoute('business.eth', communities)).toBe(false);
expect(isDirectoryBoard('biz', communities)).toBe(true); expect(isDirectoryBoard('biz', communities)).toBe(true);
expect(isDirectoryBoard('business.eth', communities)).toBe(false); expect(isDirectoryBoard('business.eth', communities)).toBe(false);
expect(isFlashBoardRoute('f', communities)).toBe(true);
expect(isFlashBoardRoute('flash-posting.bso', communities)).toBe(true);
expect(isFlashBoardRoute('mu', communities)).toBe(false);
expect(isFlashBoardRoute('music-posting.bso', communities)).toBe(false);
}); });
}); });
+12
View File
@@ -1,4 +1,5 @@
import { DirectoryCommunity, findDirectoryByAddress, normalizeBoardAddress } from '../../hooks/use-directories'; import { DirectoryCommunity, findDirectoryByAddress, normalizeBoardAddress } from '../../hooks/use-directories';
import { isFlashDirectory, isFlashDirectoryCode } from '../flash-tags';
import { getEffectiveTimeFilterName, getSearchWithTimeFilter } from './time-filter-utils'; import { getEffectiveTimeFilterName, getSearchWithTimeFilter } from './time-filter-utils';
/** /**
@@ -122,6 +123,17 @@ export const isDirectoryRoute = (boardIdentifier: string, communities: Directory
return directoryToAddress.has(boardIdentifier); return directoryToAddress.has(boardIdentifier);
}; };
/** True when the board route segment refers to a flash (/f/) upload directory board. */
export const isFlashBoardRoute = (boardIdentifier: string | undefined, communities: DirectoryCommunity[]): boolean => {
if (isFlashDirectoryCode(boardIdentifier)) {
return true;
}
if (!boardIdentifier) {
return false;
}
return isFlashDirectory(findDirectoryByAddress(communities, boardIdentifier));
};
/** @deprecated Use {@link isDirectoryRoute} */ /** @deprecated Use {@link isDirectoryRoute} */
export const isDirectoryBoard = isDirectoryRoute; export const isDirectoryBoard = isDirectoryRoute;
+3 -7
View File
@@ -3,7 +3,7 @@ import { Link, useParams } from 'react-router-dom';
import { useFeed, useCommunity } from '@bitsocial/bitsocial-react-hooks'; import { useFeed, useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { shouldShowSnow } from '../../lib/snow'; import { shouldShowSnow } from '../../lib/snow';
import { BottomButton, CatalogButton, ReturnButton, TopButton } from '../../components/board-buttons/board-buttons'; import { BottomButton, BracketedCatalogButton, CatalogButton, ReturnButton, TopButton } from '../../components/board-buttons/board-buttons';
import ErrorDisplay from '../../components/error-display/error-display'; import ErrorDisplay from '../../components/error-display/error-display';
import { PageFooterDesktop, PageFooterMobile, ThreadFooterStyleRow } from '../../components/footer'; import { PageFooterDesktop, PageFooterMobile, ThreadFooterStyleRow } from '../../components/footer';
import LoadingEllipsis from '../../components/loading-ellipsis'; import LoadingEllipsis from '../../components/loading-ellipsis';
@@ -106,9 +106,7 @@ const ArchiveDesktopTopControls = ({ communityAddress }: { communityAddress: str
<span> <span>
[<ReturnButton address={communityAddress} />] [<ReturnButton address={communityAddress} />]
</span> </span>
<span> <BracketedCatalogButton address={communityAddress} />
[<CatalogButton address={communityAddress} />]
</span>
<span> <span>
[<BottomButton />] [<BottomButton />]
</span> </span>
@@ -120,9 +118,7 @@ const ArchiveDesktopFooterControls = ({ communityAddress }: { communityAddress:
<span> <span>
[<ReturnButton address={communityAddress} />] [<ReturnButton address={communityAddress} />]
</span> </span>
<span> <BracketedCatalogButton address={communityAddress} />
[<CatalogButton address={communityAddress} />]
</span>
<span> <span>
[<TopButton />] [<TopButton />]
</span> </span>
+3 -7
View File
@@ -3,7 +3,7 @@ import { Link, Navigate, useParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useCommunity } from '@bitsocial/bitsocial-react-hooks'; import { useCommunity } from '@bitsocial/bitsocial-react-hooks';
import { shouldShowSnow } from '../../lib/snow'; import { shouldShowSnow } from '../../lib/snow';
import { BottomButton, CatalogButton, ReturnButton, TopButton } from '../../components/board-buttons/board-buttons'; import { BottomButton, BracketedCatalogButton, CatalogButton, ReturnButton, TopButton } from '../../components/board-buttons/board-buttons';
import { PageFooterDesktop, PageFooterMobile, ThreadFooterStyleRow } from '../../components/footer'; import { PageFooterDesktop, PageFooterMobile, ThreadFooterStyleRow } from '../../components/footer';
import LoadingEllipsis from '../../components/loading-ellipsis'; import LoadingEllipsis from '../../components/loading-ellipsis';
import Tooltip from '../../components/tooltip'; import Tooltip from '../../components/tooltip';
@@ -49,9 +49,7 @@ const DirectoryDesktopTopControls = ({ communityAddress }: { communityAddress: s
<span> <span>
[<ReturnButton address={communityAddress} />] [<ReturnButton address={communityAddress} />]
</span> </span>
<span> <BracketedCatalogButton address={communityAddress} />
[<CatalogButton address={communityAddress} />]
</span>
<span> <span>
[<BottomButton />] [<BottomButton />]
</span> </span>
@@ -63,9 +61,7 @@ const DirectoryDesktopFooterControls = ({ communityAddress }: { communityAddress
<span> <span>
[<ReturnButton address={communityAddress} />] [<ReturnButton address={communityAddress} />]
</span> </span>
<span> <BracketedCatalogButton address={communityAddress} />
[<CatalogButton address={communityAddress} />]
</span>
<span> <span>
[<TopButton />] [<TopButton />]
</span> </span>