fix(directory): use resolved board for directory feeds

This commit is contained in:
Tommaso Casaburi
2026-05-21 00:05:29 +07:00
parent 536d9ac0b8
commit 616fea39fb
9 changed files with 198 additions and 50 deletions
+29 -2
View File
@@ -25,7 +25,7 @@ const testState = vi.hoisted(() => ({
directories: [
{ address: 'music-posting.eth', title: '/mu/ - Music', nsfw: false },
{ address: 'tech-posting.eth', title: '/g/ - Technology', nsfw: false },
] as Array<{ address: string; title?: string; nsfw?: boolean }>,
] as Array<{ address: string; title?: string; nsfw?: boolean; directoryCode?: string }>,
initSnowMock: vi.fn(),
isMobile: false,
isSpecialEnabled: false,
@@ -41,6 +41,8 @@ const testState = vi.hoisted(() => ({
threadNumber: null,
} as ReplyModalShape,
resolvedCommunityAddress: undefined as string | undefined,
resolvedDirectoryBoardPath: undefined as string | undefined,
isDirectoryCandidate: false,
communities: {} as Record<string, unknown>,
useThemeMock: vi.fn(),
}));
@@ -72,7 +74,11 @@ vi.mock('../hooks/use-is-mobile', () => ({
}));
vi.mock('../hooks/use-resolved-community-address', () => ({
useResolvedCommunityAddress: () => testState.resolvedCommunityAddress,
useResolvedCommunityAddress: (boardIdentifier?: string) => testState.resolvedCommunityAddress ?? boardIdentifier,
useResolvedDirectoryBoardPath: () => ({
boardPath: testState.resolvedDirectoryBoardPath,
isDirectoryCandidate: testState.isDirectoryCandidate,
}),
}));
vi.mock('../hooks/use-theme', () => ({
@@ -340,6 +346,8 @@ describe('App', () => {
threadNumber: null,
} as ReplyModalShape;
testState.resolvedCommunityAddress = undefined;
testState.resolvedDirectoryBoardPath = undefined;
testState.isDirectoryCandidate = false;
testState.communities = {};
testState.useThemeMock.mockReset();
testState.closeCreateBoardModalMock.mockReset();
@@ -416,6 +424,25 @@ describe('App', () => {
expect(container.querySelector('[data-testid="post-view"]')).toBeTruthy();
});
it('canonicalizes a direct route for the current resolved directory board', async () => {
testState.resolvedDirectoryBoardPath = 'biz';
testState.isDirectoryCandidate = true;
await renderApp('/bizraelis.bso/thread/comment-1?focus=1');
expect(latestLocation).toBe('/biz/thread/comment-1?focus=1');
expect(container.querySelector('[data-testid="post-view"]')).toBeTruthy();
});
it('does not canonicalize a directory candidate address when another board is resolved', async () => {
testState.directories = [{ address: 'business-and-finance.bso', directoryCode: 'biz', title: '/biz/ - Business & Finance', nsfw: false }];
testState.isDirectoryCandidate = true;
await renderApp('/business-and-finance.bso?focus=1');
expect(latestLocation).toBe('/business-and-finance.bso?focus=1');
});
it('routes invalid mod aliases and unknown mod paths to not-found', async () => {
await renderApp('/mu/modqueue');
expect(latestLocation).toBe('/not-found');
+4 -4
View File
@@ -14,12 +14,11 @@ import { useAccountCommunityAddresses } from './hooks/use-account-community-addr
import useTheme from './hooks/use-theme';
import { useDirectories } from './hooks/use-directories';
import { useCommunityIdentifier } from './hooks/use-community-identifiers';
import { useResolvedCommunityAddress } from './hooks/use-resolved-community-address';
import { useResolvedCommunityAddress, useResolvedDirectoryBoardPath } from './hooks/use-resolved-community-address';
import useSafeAccountComment from './hooks/use-safe-account-comment';
import { getCommentCommunityAddress } from './lib/utils/comment-utils';
import {
getBoardPath,
getCommunityAddress,
isBoardModRoute,
isDirectoryBoard,
isArchiveRoute,
@@ -77,7 +76,8 @@ const BoardLayout = () => {
const isInSubscriptionsView = isSubscriptionsView(pathname, useParams());
const isInModView = isModView(pathname);
const directories = useDirectories();
const communityAddress = boardIdentifier ? getCommunityAddress(boardIdentifier, directories) : undefined;
const communityAddress = useResolvedCommunityAddress(boardIdentifier);
const { boardPath: resolvedDirectoryBoardPath, isDirectoryCandidate } = useResolvedDirectoryBoardPath(boardIdentifier);
const pendingPost = useSafeAccountComment({ commentIndex: accountCommentIndex });
const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost);
const { closeCreateBoardModal } = useCreateBoardModalStore();
@@ -128,7 +128,7 @@ const BoardLayout = () => {
// Normalize address URLs to directory codes: /anime-and-manga.eth/thread/xxx -> /a/thread/xxx
if (boardIdentifier && !isDirectoryBoard(boardIdentifier, directories)) {
const canonicalBoardIdentifier = getBoardPath(boardIdentifier, directories);
const canonicalBoardIdentifier = resolvedDirectoryBoardPath ?? (isDirectoryCandidate ? boardIdentifier : getBoardPath(boardIdentifier, directories));
if (canonicalBoardIdentifier !== boardIdentifier) {
const canonicalPath = pathname.replace(`/${boardIdentifier}`, `/${canonicalBoardIdentifier}`);
return <Navigate to={canonicalPath + (search || '')} replace />;
@@ -2,13 +2,14 @@ import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useResolvedCommunityAddress } from '../use-resolved-community-address';
import { useResolvedCommunityAddress, useResolvedDirectoryBoardPath } from '../use-resolved-community-address';
(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 testState = vi.hoisted(() => ({
boardIdentifier: 'biz',
boardIdentifierOverride: undefined as string | undefined,
directories: [
{
address: 'business-and-finance.bso',
@@ -20,7 +21,7 @@ const testState = vi.hoisted(() => ({
directoryCode: 'biz',
boards: [
{ address: 'business-and-finance.bso', score: 100 },
{ address: 'backup-business.bso', score: 10 },
{ address: 'bizraelis.bso', score: 10 },
],
},
offlineStates: {} as Record<string, { updatedAt?: number; state?: string }>,
@@ -37,6 +38,7 @@ vi.mock('react-router-dom', async () => {
vi.mock('../use-directories', () => ({
useDirectories: () => testState.directories,
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
}));
vi.mock('../use-directory-list', async () => {
@@ -56,11 +58,13 @@ vi.mock('../../stores/use-community-offline-store', () => ({
}));
let latestValue: string | undefined;
let latestDirectoryBoardPath: { boardPath: string | undefined; isDirectoryCandidate: boolean };
let container: HTMLDivElement;
let root: Root;
const HookHarness = () => {
latestValue = useResolvedCommunityAddress();
latestValue = useResolvedCommunityAddress(testState.boardIdentifierOverride);
latestDirectoryBoardPath = useResolvedDirectoryBoardPath(testState.boardIdentifier);
return null;
};
@@ -75,7 +79,9 @@ describe('useResolvedCommunityAddress', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01T00:00:10Z'));
latestValue = undefined;
latestDirectoryBoardPath = { boardPath: undefined, isDirectoryCandidate: false };
testState.boardIdentifier = 'biz';
testState.boardIdentifierOverride = undefined;
testState.offlineStates = {};
testState.offlineSelections = [];
@@ -99,7 +105,7 @@ describe('useResolvedCommunityAddress', () => {
await renderHook();
expect(latestValue).toBe('backup-business.bso');
expect(latestValue).toBe('bizraelis.bso');
});
it('keeps a higher-ranked directory board when its last update is newer than 30 minutes', async () => {
@@ -125,7 +131,7 @@ describe('useResolvedCommunityAddress', () => {
await renderHook();
expect(latestValue).toBe('custom-board.bso');
expect(testState.offlineSelections).toEqual([undefined]);
expect(testState.offlineSelections.every((selection) => selection === undefined)).toBe(true);
});
it('switches away from a directory board when it crosses the offline threshold while mounted', async () => {
@@ -143,6 +149,52 @@ describe('useResolvedCommunityAddress', () => {
vi.advanceTimersByTime(2 * 60 * 1000);
});
expect(latestValue).toBe('backup-business.bso');
expect(latestValue).toBe('bizraelis.bso');
});
it('uses an explicit directory identifier for cached board feeds', async () => {
testState.boardIdentifier = 'all';
testState.boardIdentifierOverride = 'biz';
testState.offlineStates = {
'business-and-finance.bso': {
updatedAt: 1_704_067_210 - 31 * 60,
},
};
await renderHook();
expect(latestValue).toBe('bizraelis.bso');
});
it('canonicalizes the direct address for the current directory winner', async () => {
testState.boardIdentifier = 'bizraelis.bso';
testState.offlineStates = {
'business-and-finance.bso': {
updatedAt: 1_704_067_210 - 31 * 60,
},
};
await renderHook();
expect(latestDirectoryBoardPath).toEqual({
boardPath: 'biz',
isDirectoryCandidate: true,
});
});
it('does not canonicalize a directory candidate address when it is not the current winner', async () => {
testState.boardIdentifier = 'business-and-finance.bso';
testState.offlineStates = {
'business-and-finance.bso': {
updatedAt: 1_704_067_210 - 31 * 60,
},
};
await renderHook();
expect(latestDirectoryBoardPath).toEqual({
boardPath: undefined,
isDirectoryCandidate: true,
});
});
});
+37 -6
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { DirectoryCommunity, useDirectories } from './use-directories';
import { type DirectoryCommunity, normalizeBoardAddress, useDirectories } from './use-directories';
import { type DirectoryList, type DirectoryListBoard, normalizeDirectoryList, sortDirectoryBoardsByRank } from '../lib/utils/directory-list-utils';
import directoryListsData from '../data/5chan-directory-lists.json';
export type { DirectoryListBoard } from '../lib/utils/directory-list-utils';
@@ -28,6 +29,33 @@ const moduleCaches = new Map<string, DirectoryList>();
const inFlightFetches = new Map<string, Promise<DirectoryList | null>>();
const lastFetchSuccessAt = new Map<string, number>();
const lastFetchAttemptAt = new Map<string, number>();
let vendoredDirectoryListsCache: DirectoryList[] | null = null;
const getVendoredDirectoryLists = (): DirectoryList[] => {
if (vendoredDirectoryListsCache) return vendoredDirectoryListsCache;
const directories = Array.isArray(directoryListsData.directories) ? directoryListsData.directories : [];
vendoredDirectoryListsCache = directories.flatMap((directory) => {
const directoryCode = typeof directory.directoryCode === 'string' ? directory.directoryCode : undefined;
if (!directoryCode) return [];
const normalized = normalizeDirectoryList(directory, directoryCode);
return normalized ? [normalized] : [];
});
return vendoredDirectoryListsCache;
};
const getVendoredDirectoryList = (directoryCode: string): DirectoryList | null =>
getVendoredDirectoryLists().find((directory) => directory.directoryCode === directoryCode) ?? null;
export const getDirectoryCodeForBoardAddress = (address: string | undefined): string | undefined => {
if (!address) return undefined;
const normalizedAddress = normalizeBoardAddress(address);
return getVendoredDirectoryLists().find((directory) =>
directory.boards.some((board) => normalizeBoardAddress(board.address) === normalizedAddress || board.publicKey === address),
)?.directoryCode;
};
const synthesizeFromMainDirectory = (directoryCode: string, directories: DirectoryCommunity[]): DirectoryList | null => {
const match = directories.find((community) => community.directoryCode === directoryCode);
@@ -48,6 +76,9 @@ const synthesizeFromMainDirectory = (directoryCode: string, directories: Directo
};
};
const getFallbackDirectoryList = (directoryCode: string, directories: DirectoryCommunity[]): DirectoryList | null =>
getVendoredDirectoryList(directoryCode) ?? synthesizeFromMainDirectory(directoryCode, directories);
const mergeDirectoryListDefaults = (list: DirectoryList, fallback: DirectoryList | null): DirectoryList => ({
...list,
...(list.title || !fallback?.title ? {} : { title: fallback.title }),
@@ -145,13 +176,13 @@ const fetchDirectoryListDeduped = (code: string): Promise<DirectoryList | null>
/**
* Fetch the candidate boards for a single directory code (e.g. 'biz').
*
* Source: `bitsocialnet/lists/5chan-directories/5chan-{code}-directory.json`. When the network is unavailable
* or the file is not yet published, falls back to a synthesized single-entry list derived
* from the merged directory assignments.
* Source: `bitsocialnet/lists/5chan-directories/5chan-{code}-directory.json`. While the network
* is unavailable or the remote file is still loading, falls back to the vendored list
* generated from that repo, then to a synthesized single-entry list from directory assignments.
*/
export const useDirectoryList = (directoryCode: string | undefined): DirectoryListState => {
const directories = useDirectories();
const fallback = useMemo(() => (directoryCode ? synthesizeFromMainDirectory(directoryCode, directories) : null), [directoryCode, directories]);
const fallback = useMemo(() => (directoryCode ? getFallbackDirectoryList(directoryCode, directories) : null), [directoryCode, directories]);
const [state, setState] = useState<DirectoryListState>(() => {
if (!directoryCode) {
@@ -233,7 +264,7 @@ export const useDirectoryLists = (directoryCodes: string[] | undefined): Directo
const fallbackByCode = useMemo(() => {
const normalizedDirectoryCodes = directoryCodesKey ? directoryCodesKey.split('\0') : [];
return Object.fromEntries(normalizedDirectoryCodes.map((directoryCode) => [directoryCode, synthesizeFromMainDirectory(directoryCode, directories)])) as Record<
return Object.fromEntries(normalizedDirectoryCodes.map((directoryCode) => [directoryCode, getFallbackDirectoryList(directoryCode, directories)])) as Record<
string,
DirectoryList | null
>;
+42 -6
View File
@@ -1,23 +1,28 @@
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useDirectories } from './use-directories';
import { pickDirectoryWinner, useDirectoryList } from './use-directory-list';
import { getDirectoryCodeForBoardAddress, pickDirectoryWinner, useDirectoryList } from './use-directory-list';
import useCommunityOfflineStore from '../stores/use-community-offline-store';
import { getCommunityAddress, getBoardPath, isDirectoryRoute } from '../lib/utils/route-utils';
import { areSameBoardAddress, getCommunityAddress, getBoardPath, isDirectoryRoute } from '../lib/utils/route-utils';
import { isCommunityKnownOffline } from '../lib/utils/community-freshness-utils';
import { useNowSeconds } from './use-now-seconds';
interface ResolvedDirectoryBoardPath {
boardPath: string | undefined;
isDirectoryCandidate: boolean;
}
/**
* Resolve a board identifier from URL params to canonical community address.
* Resolve a board identifier to its canonical community address.
*
* For directory codes (e.g. /biz) with a per-directory list of candidates, picks the
* highest-ranked candidate that is not currently flagged offline. Falls back to the
* vendored single-candidate default while the per-directory list is still loading.
* vendored directory list while the remote list is still loading.
*/
export const useResolvedCommunityAddress = (): string | undefined => {
export const useResolvedCommunityAddress = (boardIdentifierOverride?: string): string | undefined => {
const params = useParams<{ boardIdentifier?: string }>();
const directories = useDirectories();
const boardIdentifier = params.boardIdentifier;
const boardIdentifier = boardIdentifierOverride ?? params.boardIdentifier;
const isCode = !!boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
const { list } = useDirectoryList(isCode ? boardIdentifier : undefined);
const offlineStates = useCommunityOfflineStore((state) => (isCode ? state.communityOfflineState : undefined));
@@ -34,6 +39,37 @@ export const useResolvedCommunityAddress = (): string | undefined => {
}, [boardIdentifier, directories, isCode, list, offlineStates, nowSeconds]);
};
/**
* Return the directory code only when a direct board-address route points at the
* board currently winning that directory.
*/
export const useResolvedDirectoryBoardPath = (boardIdentifier: string | undefined): ResolvedDirectoryBoardPath => {
const directories = useDirectories();
const isCode = !!boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
const directoryCode = useMemo(() => (boardIdentifier && !isCode ? getDirectoryCodeForBoardAddress(boardIdentifier) : undefined), [boardIdentifier, isCode]);
const { list } = useDirectoryList(directoryCode);
const offlineStates = useCommunityOfflineStore((state) => (directoryCode ? state.communityOfflineState : undefined));
const nowSeconds = useNowSeconds(!!directoryCode);
return useMemo(() => {
if (!boardIdentifier || !directoryCode) {
return { boardPath: undefined, isDirectoryCandidate: false };
}
if (!list || list.boards.length === 0) {
return { boardPath: undefined, isDirectoryCandidate: true };
}
const isOffline = (address: string) => isCommunityKnownOffline(offlineStates?.[address], nowSeconds);
const winner = pickDirectoryWinner(list.boards, isOffline);
return {
boardPath: winner && areSameBoardAddress(winner.address, boardIdentifier) ? directoryCode : undefined,
isDirectoryCandidate: true,
};
}, [boardIdentifier, directoryCode, list, offlineStates, nowSeconds]);
};
/**
* Resolve a community address to board path (directory code or address) for links.
*/
+2 -8
View File
@@ -10,7 +10,7 @@ import LoadingEllipsis from '../../components/loading-ellipsis';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useCommunityField } from '../../hooks/use-stable-community';
import { useFeedStateString } from '../../hooks/use-state-string';
import { getCommunityAddress, getBoardPath } from '../../lib/utils/route-utils';
import { getBoardPath } from '../../lib/utils/route-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { removeMarkdown } from '../../lib/utils/post-utils';
import { useDirectories } from '../../hooks/use-directories';
@@ -151,13 +151,7 @@ const Archive = () => {
const boardIdentifier = params.boardIdentifier;
const directories = useDirectories();
const resolvedAddressFromUrl = useResolvedCommunityAddress();
const communityAddress = useMemo(() => {
if (boardIdentifier) {
return getCommunityAddress(boardIdentifier, directories);
}
return resolvedAddressFromUrl;
}, [boardIdentifier, directories, resolvedAddressFromUrl]);
const communityAddress = useResolvedCommunityAddress(boardIdentifier);
const boardPath = useMemo(() => {
if (!communityAddress) {
+22 -2
View File
@@ -25,7 +25,7 @@ const testState = vi.hoisted(() => ({
accountComments: [] as TestComment[],
accountCommentsCalls: [] as Array<{ commentIndices?: number[]; communityAddress?: string; newerThan?: number; sortType?: 'new' | 'old' } | undefined>,
accountCommunityAddresses: [] as string[],
directories: [{ address: 'music-posting.eth', title: '/mu/ - Music' }] as Array<{ address: string; title?: string }>,
directories: [{ address: 'music-posting.eth', title: '/mu/ - Music' }] as Array<{ address: string; title?: string; directoryCode?: string }>,
directoryByAddress: {
'music-posting.eth': {
address: 'music-posting.eth',
@@ -33,7 +33,7 @@ const testState = vi.hoisted(() => ({
},
} as Record<string, { address: string; features?: Record<string, unknown> }>,
feed: [] as TestComment[],
feedOptionsCalls: [] as Array<{ communitiesLength?: number; newerThan?: number; postsPerPage?: number; sortType?: string }>,
feedOptionsCalls: [] as Array<{ communities?: unknown[]; communitiesLength?: number; newerThan?: number; postsPerPage?: number; sortType?: string }>,
feedState: undefined as string | undefined,
feedStateString: 'syncing' as string | undefined,
filteredDirectoryAddresses: ['music-posting.eth'] as string[],
@@ -129,6 +129,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
sortType?: string;
}) => {
testState.feedOptionsCalls.push({
communities: options?.communities,
communitiesLength: options?.communities?.length,
newerThan: options?.newerThan,
postsPerPage: options?.postsPerPage,
@@ -379,6 +380,25 @@ describe('Board', () => {
localStorage.clear();
});
it('uses the resolved directory winner for cached board feeds', async () => {
testState.directories = [{ address: 'business-and-finance.bso', directoryCode: 'biz', title: '/biz/ - Business & Finance' }];
testState.directoryByAddress = {
'bizraelis.bso': {
address: 'bizraelis.bso',
features: { postsPerPage: 2 },
},
};
testState.resolvedCommunityAddress = 'bizraelis.bso';
await renderBoard({
boardProps: { boardIdentifier: 'biz', viewType: 'board' },
initialEntry: '/biz',
routePath: '/:boardIdentifier/*',
});
expect(testState.feedOptionsCalls.map((call) => call.communities)).toContainEqual([{ name: 'bizraelis.bso' }]);
});
it('renders the current page feed, inserts recent account comments, and wires footer actions', async () => {
const currentTimestamp = Math.floor(Date.now() / 1000);
testState.feed = [
+2 -8
View File
@@ -22,7 +22,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
import { useSuggestionFeedLoader } from '../../hooks/use-suggestion-feed-loader';
import useTimeFilter from '../../hooks/use-time-filter';
import { getPageSlice } from '../../lib/utils/board-feed-pagination';
import { getPageFromFeedPath, getCommunityAddress, isDirectoryBoard, normalizeMultiboardFeedPath, stripPageFromFeedPath } from '../../lib/utils/route-utils';
import { getPageFromFeedPath, isDirectoryBoard, normalizeMultiboardFeedPath, stripPageFromFeedPath } from '../../lib/utils/route-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
import { getSearchWithTimeFilter, getTimeFilterSuggestion, type TimeFilterSuggestion } from '../../lib/utils/time-filter-utils';
@@ -166,13 +166,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
const multiboardTimeFilterSeconds = isMultiboardView ? timeFilterSeconds : undefined;
const directories = useDirectories();
const resolvedAddressFromUrl = useResolvedCommunityAddress();
const communityAddress = useMemo(() => {
if (boardIdentifierProp) {
return getCommunityAddress(boardIdentifierProp, directories);
}
return resolvedAddressFromUrl;
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
const communityAddress = useResolvedCommunityAddress(boardIdentifierProp);
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
+2 -8
View File
@@ -23,7 +23,7 @@ import useFeedResetStore from '../../stores/use-feed-reset-store';
import useHiddenCatalogThreadsStore from '../../stores/use-hidden-catalog-threads-store';
import useSortingStore from '../../stores/use-sorting-store';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import { getCommunityAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
import { isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
import CatalogRow from '../../components/catalog-row';
import { CatalogFooterFirstRow, CatalogFooterStyleRow, PageFooterDesktop, PageFooterMobile } from '../../components/footer';
import { ReturnButton, ArchiveButton, TopButton, RefreshButton } from '../../components/board-buttons/board-buttons';
@@ -285,13 +285,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const effectiveInfiniteScroll = isMultiboard;
const directories = useDirectories();
const resolvedAddressFromUrl = useResolvedCommunityAddress();
const communityAddress = useMemo(() => {
if (boardIdentifierProp) {
return getCommunityAddress(boardIdentifierProp, directories);
}
return resolvedAddressFromUrl;
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
const communityAddress = useResolvedCommunityAddress(boardIdentifierProp);
const filterItems = useCatalogFiltersStore((state) => state.filterItems);
const searchText = useCatalogFiltersStore((state) => state.searchText);