mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(directory): load per-directory list files (#1134)
Merge board directories from the new bitsocialnet/lists per-directory layout on the client side. Update vendored fallback data, sync tooling, directory references, and tests. Treat failed board stat fetches as complete on the home page so stats do not stay loading forever.
This commit is contained in:
@@ -17,7 +17,6 @@ const testState = vi.hoisted(() => ({
|
||||
{
|
||||
address: 'anime-and-manga.bso',
|
||||
score: 12,
|
||||
managedByDevs: false,
|
||||
},
|
||||
],
|
||||
directories: [
|
||||
@@ -157,7 +156,6 @@ const renderDirectory = async () => {
|
||||
const createDirectoryBoard = (address: string, score = 12) => ({
|
||||
address,
|
||||
score,
|
||||
managedByDevs: false,
|
||||
});
|
||||
|
||||
const createCommunity = (address: string, updatedAt = testState.nowSeconds - 60) => ({
|
||||
|
||||
@@ -145,7 +145,7 @@ const DirectoryRow = ({ board, nowSeconds, rank, onVote }: DirectoryRowProps) =>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.scoreCell}>
|
||||
<span className={styles.scoreValue}>{board.score}</span>
|
||||
<span className={styles.scoreValue}>{board.score ?? DIRECTORY_STATUS_UNAVAILABLE_MARKER}</span>
|
||||
</td>
|
||||
<td className={styles.actionsCell}>
|
||||
[
|
||||
@@ -166,7 +166,7 @@ const DirectoryRow = ({ board, nowSeconds, rank, onVote }: DirectoryRowProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
const getRepoEditUrl = (directoryCode: string) => `https://github.com/bitsocialnet/lists/edit/master/5chan-${directoryCode}-directory.json`;
|
||||
const getRepoEditUrl = (directoryCode: string) => `https://github.com/bitsocialnet/lists/edit/master/5chan-directories/5chan-${directoryCode}-directory.json`;
|
||||
|
||||
const Directory = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -70,9 +70,9 @@ const FAQ_SECTIONS: FAQSection[] = [
|
||||
answer: (
|
||||
<>
|
||||
Directories are short paths such as <code>/g/</code>, <code>/biz/</code>, or <code>/mu/</code> that point to featured boards. Today they are handpicked
|
||||
through pull requests to{' '}
|
||||
<a href='https://github.com/bitsocialnet/lists/blob/master/5chan-directories.json' {...externalLinkProps}>
|
||||
5chan-directories.json
|
||||
through pull requests to the{' '}
|
||||
<a href='https://github.com/bitsocialnet/lists/tree/master/5chan-directories' {...externalLinkProps}>
|
||||
5chan directory files
|
||||
</a>{' '}
|
||||
until directory voting is available. A board does not need a directory slot to exist or be reachable.
|
||||
</>
|
||||
@@ -330,9 +330,9 @@ const FAQ_SECTIONS: FAQSection[] = [
|
||||
question: 'How do I get my board into a directory?',
|
||||
answer: (
|
||||
<>
|
||||
Open a pull request against{' '}
|
||||
<a href='https://github.com/bitsocialnet/lists/blob/master/5chan-directories.json' {...externalLinkProps}>
|
||||
5chan-directories.json
|
||||
Open a pull request against the relevant file in the{' '}
|
||||
<a href='https://github.com/bitsocialnet/lists/tree/master/5chan-directories' {...externalLinkProps}>
|
||||
5chan-directories folder
|
||||
</a>{' '}
|
||||
with the board title, address, and NSFW status. Current expectations include high uptime, active use, relevant topic fit, and responsible moderation. Future
|
||||
directory voting is planned for 5chan Pass holders.
|
||||
|
||||
@@ -18,7 +18,7 @@ const testState = vi.hoisted(() => ({
|
||||
nowSeconds: 1_704_067_210,
|
||||
navigateMock: vi.fn(),
|
||||
communities: {} as Record<string, unknown>,
|
||||
communityStats: {} as Record<string, { allPostCount?: number; weekActiveUserCount?: number }>,
|
||||
communityStats: {} as Record<string, { allPostCount?: number; weekActiveUserCount?: number; state?: string }>,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -227,6 +227,20 @@ describe('Home', () => {
|
||||
expect(container.textContent).toContain('boards_tracked 2');
|
||||
});
|
||||
|
||||
it('does not keep aggregate stats loading after a directory stats fetch fails', () => {
|
||||
testState.communityStats = {
|
||||
'music-posting.eth': { allPostCount: 5, weekActiveUserCount: 2 },
|
||||
'tech-posting.eth': { state: 'failed' },
|
||||
};
|
||||
|
||||
renderHome();
|
||||
|
||||
expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0);
|
||||
expect(container.textContent).toContain('total_posts 5');
|
||||
expect(container.textContent).toContain('current_users 2');
|
||||
expect(container.textContent).toContain('boards_tracked 2');
|
||||
});
|
||||
|
||||
it('navigates to the canonical board path when the search form is submitted', async () => {
|
||||
renderHome();
|
||||
|
||||
|
||||
+15
-2
@@ -18,7 +18,7 @@ import lowerCase from 'lodash/lowerCase';
|
||||
import useCommunitiesLoadingStartTimestamps from '../../stores/use-communities-loading-start-timestamps-store';
|
||||
import { useNowSeconds } from '../../hooks/use-now-seconds';
|
||||
|
||||
// https://github.com/bitsocialnet/lists/blob/master/5chan-directories.json
|
||||
// https://github.com/bitsocialnet/lists/tree/master/5chan-directories
|
||||
|
||||
const SearchBar = () => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -97,6 +97,7 @@ interface StatValueProps {
|
||||
type HomepageStats = {
|
||||
allPostCount?: number;
|
||||
weekActiveUserCount?: number;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
const StatValue = ({ isLoaded, loadingLabel, value }: StatValueProps) => (isLoaded ? <>{value}</> : <LoadingEllipsis string={loadingLabel} />);
|
||||
@@ -108,6 +109,7 @@ const getDirectoryCode = (directory: DirectoryCommunity): string | null => direc
|
||||
const getUniqueAddresses = (addresses: string[]): string[] => [...new Set(addresses.filter((address) => address.length > 0))];
|
||||
|
||||
const hasLoadedStats = (stat: HomepageStats | undefined): stat is HomepageStats & { allPostCount: number } => stat?.allPostCount !== undefined;
|
||||
const hasFailedStats = (stat: HomepageStats | undefined): boolean => stat?.state === 'failed';
|
||||
|
||||
const Stats = ({ directories }: { directories: DirectoryCommunity[] }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -153,15 +155,26 @@ const Stats = ({ directories }: { directories: DirectoryCommunity[] }) => {
|
||||
candidateAddresses.forEach((address) => collectorAddressSet.add(address));
|
||||
|
||||
let stat: HomepageStats | undefined;
|
||||
let failedStat: HomepageStats | undefined;
|
||||
let allCandidateStatsResolved = candidateAddresses.length > 0;
|
||||
for (const address of candidateAddresses) {
|
||||
const candidateStats = communitiesStats[address];
|
||||
if (hasLoadedStats(candidateStats)) {
|
||||
stat = candidateStats;
|
||||
break;
|
||||
}
|
||||
if (hasFailedStats(candidateStats)) {
|
||||
failedStat ??= candidateStats;
|
||||
continue;
|
||||
}
|
||||
allCandidateStatsResolved = false;
|
||||
}
|
||||
|
||||
if (!hasLoadedStats(stat)) {
|
||||
if (!stat && allCandidateStatsResolved) {
|
||||
stat = failedStat;
|
||||
}
|
||||
|
||||
if (!stat || (!hasLoadedStats(stat) && !hasFailedStats(stat))) {
|
||||
allDirectoryStatsLoaded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ vi.mock('../../box-modal', () => ({
|
||||
const directories = [
|
||||
{ address: 'music-posting.eth', title: '/mu/ - Music' },
|
||||
{ address: 'tech-posting.eth', title: '/g/ - Technology' },
|
||||
{ address: 'tv-posting.eth', title: '/tv/ - Television & Film Directory' },
|
||||
];
|
||||
|
||||
let container: HTMLDivElement;
|
||||
@@ -158,6 +159,24 @@ describe('PopularThreadsBox', () => {
|
||||
expect(container.querySelector('.spoilertext')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not show a stale directory suffix in board titles', () => {
|
||||
testState.popularPosts = [
|
||||
{
|
||||
cid: 'thread-tv',
|
||||
communityAddress: 'tv-posting.eth',
|
||||
content: 'thread content',
|
||||
link: 'https://cdn.example/thread-tv.jpg',
|
||||
thumbnailUrl: 'https://cdn.example/thread-tv-thumb.jpg',
|
||||
title: '',
|
||||
},
|
||||
];
|
||||
|
||||
renderPopularThreadsBox();
|
||||
|
||||
expect(container.textContent).toContain('Television & Film');
|
||||
expect(container.textContent).not.toContain('Television & Film Directory');
|
||||
});
|
||||
|
||||
it('subscribes to feed state only while popular threads are loading', () => {
|
||||
testState.isLoading = true;
|
||||
testState.popularPosts = [];
|
||||
@@ -165,6 +184,6 @@ describe('PopularThreadsBox', () => {
|
||||
renderPopularThreadsBox();
|
||||
|
||||
expect(container.textContent).toContain('Downloading boards');
|
||||
expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith(['music-posting.eth', 'tech-posting.eth']);
|
||||
expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith(['music-posting.eth', 'tech-posting.eth', 'tv-posting.eth']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,8 @@ const PopularThreadCard = memo(
|
||||
(prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid && prevProps.boardTitle === nextProps.boardTitle && prevProps.boardPath === nextProps.boardPath,
|
||||
);
|
||||
|
||||
const getPopularThreadBoardTitle = (directoryTitle: string | undefined): string => directoryTitle?.replace(/^\/[^/]+\/\s*-\s*/, '').replace(/\s+Directory$/, '') || '';
|
||||
|
||||
const PopularThreadsBox = ({ directories, directoryAddresses }: { directories: DirectoryCommunity[]; directoryAddresses: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const showWorksafeContentOnly = usePopularThreadsOptionsStore((state) => state.showWorksafeContentOnly);
|
||||
@@ -103,7 +105,7 @@ const PopularThreadsBox = ({ directories, directoryAddresses }: { directories: D
|
||||
popularPosts.map((post: Comment) => {
|
||||
const communityAddress = getCommentCommunityAddress(post);
|
||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||
const boardTitle = directoryEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardTitle = getPopularThreadBoardTitle(directoryEntry?.title);
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : '';
|
||||
return <PopularThreadCard key={post.cid} post={post} boardTitle={boardTitle} boardPath={boardPath} />;
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user