mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(directory): load direct board routes
This commit is contained in:
@@ -333,6 +333,10 @@ describe('App', () => {
|
|||||||
testState.account = { author: { address: '0x123' } };
|
testState.account = { author: { address: '0x123' } };
|
||||||
testState.accountComments = {};
|
testState.accountComments = {};
|
||||||
testState.accountCommunityAddresses = [];
|
testState.accountCommunityAddresses = [];
|
||||||
|
testState.directories = [
|
||||||
|
{ address: 'music-posting.eth', title: '/mu/ - Music', nsfw: false },
|
||||||
|
{ address: 'tech-posting.eth', title: '/g/ - Technology', nsfw: false },
|
||||||
|
];
|
||||||
testState.isMobile = false;
|
testState.isMobile = false;
|
||||||
testState.isSpecialEnabled = false;
|
testState.isSpecialEnabled = false;
|
||||||
testState.replyModalState = {
|
testState.replyModalState = {
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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 { useCommunityIdentifier, useCommunityIdentifiers } from '../use-community-identifiers';
|
||||||
|
|
||||||
|
(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>;
|
||||||
|
|
||||||
|
type TestDirectoryCommunity = {
|
||||||
|
address: string;
|
||||||
|
name?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
directoryCode?: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testState = vi.hoisted(() => ({
|
||||||
|
address: undefined as string | undefined,
|
||||||
|
addresses: [] as Array<string | undefined>,
|
||||||
|
directories: [] as TestDirectoryCommunity[],
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../use-directories', () => ({
|
||||||
|
useDirectories: () => testState.directories,
|
||||||
|
findDirectoryByAddress: (directories: TestDirectoryCommunity[], address: string | undefined) =>
|
||||||
|
address
|
||||||
|
? directories.find((directory) =>
|
||||||
|
[directory.address, directory.name, directory.publicKey, directory.directoryCode, directory.title].some((identifier) => identifier === address),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let latestIdentifier: ReturnType<typeof useCommunityIdentifier>;
|
||||||
|
let latestIdentifiers: ReturnType<typeof useCommunityIdentifiers>;
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
|
||||||
|
const SingleHarness = () => {
|
||||||
|
latestIdentifier = useCommunityIdentifier(testState.address);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MultiHarness = () => {
|
||||||
|
latestIdentifiers = useCommunityIdentifiers(testState.addresses);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHarness = async (element: React.ReactElement) => {
|
||||||
|
await act(async () => {
|
||||||
|
root.render(element);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useCommunityIdentifier', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
testState.address = undefined;
|
||||||
|
testState.addresses = [];
|
||||||
|
testState.directories = [];
|
||||||
|
latestIdentifier = undefined;
|
||||||
|
latestIdentifiers = [];
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => root.unmount());
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses vendored directory public keys before the main directory list has hydrated', async () => {
|
||||||
|
testState.address = 'animals-and-nature.bso';
|
||||||
|
|
||||||
|
await renderHarness(createElement(SingleHarness));
|
||||||
|
|
||||||
|
expect(latestIdentifier).toEqual({
|
||||||
|
name: 'animals-and-nature.bso',
|
||||||
|
publicKey: '12D3KooWSpKszPM2c17KBgbnoRrkWPCHJosGGFg3bKnzarYhHeSc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses candidate public keys for non-primary boards in a directory list', async () => {
|
||||||
|
testState.address = 'bizraelis.bso';
|
||||||
|
testState.directories = [
|
||||||
|
{
|
||||||
|
address: 'business-and-finance.bso',
|
||||||
|
directoryCode: 'biz',
|
||||||
|
publicKey: '12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh',
|
||||||
|
title: '/biz/ - Business & Finance',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await renderHarness(createElement(SingleHarness));
|
||||||
|
|
||||||
|
expect(latestIdentifier).toEqual({
|
||||||
|
name: 'bizraelis.bso',
|
||||||
|
publicKey: '12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps unlisted domain routes as direct community names', async () => {
|
||||||
|
testState.address = 'unlisted-board.bso';
|
||||||
|
|
||||||
|
await renderHarness(createElement(SingleHarness));
|
||||||
|
|
||||||
|
expect(latestIdentifier).toEqual({ name: 'unlisted-board.bso' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps raw IPNS route identifiers as public keys', async () => {
|
||||||
|
testState.address = '12D3KooWExamplePublicKey';
|
||||||
|
|
||||||
|
await renderHarness(createElement(SingleHarness));
|
||||||
|
|
||||||
|
expect(latestIdentifier).toEqual({ publicKey: '12D3KooWExamplePublicKey' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the same resolution to feed community arrays', async () => {
|
||||||
|
testState.addresses = ['bizraelis.bso', 'unlisted-board.bso', undefined, '12D3KooWExamplePublicKey'];
|
||||||
|
|
||||||
|
await renderHarness(createElement(MultiHarness));
|
||||||
|
|
||||||
|
expect(latestIdentifiers).toEqual([
|
||||||
|
{
|
||||||
|
name: 'bizraelis.bso',
|
||||||
|
publicKey: '12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE',
|
||||||
|
},
|
||||||
|
{ name: 'unlisted-board.bso' },
|
||||||
|
{ publicKey: '12D3KooWExamplePublicKey' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { CommunityIdentifier } from '@bitsocial/bitsocial-react-hooks';
|
import type { CommunityIdentifier } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import { findDirectoryByAddress, type DirectoryCommunity, useDirectories } from './use-directories';
|
import { findDirectoryByAddress, type DirectoryCommunity, useDirectories } from './use-directories';
|
||||||
|
import { getDirectoryCandidateBoardByAddress } from '../lib/utils/directory-list-lookup-utils';
|
||||||
|
|
||||||
const isLikelyCommunityName = (value: string) => value.includes('.');
|
const isLikelyCommunityName = (value: string) => value.includes('.');
|
||||||
|
|
||||||
@@ -10,20 +11,24 @@ const getCommunityIdentifier = (communityAddress: string | undefined, directorie
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directory = findDirectoryByAddress(directories, communityAddress);
|
const directory = findDirectoryByAddress(directories, communityAddress);
|
||||||
if (directory?.name && directory.publicKey) {
|
const directoryCandidate = getDirectoryCandidateBoardByAddress(communityAddress);
|
||||||
|
const name = directory?.name ?? directory?.address ?? directoryCandidate?.address;
|
||||||
|
const publicKey = directory?.publicKey ?? directoryCandidate?.publicKey;
|
||||||
|
|
||||||
|
if (name && publicKey) {
|
||||||
return {
|
return {
|
||||||
name: directory.name,
|
name,
|
||||||
publicKey: directory.publicKey,
|
publicKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (directory?.publicKey) {
|
if (publicKey) {
|
||||||
return {
|
return {
|
||||||
publicKey: directory.publicKey,
|
publicKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (directory?.name) {
|
if (name) {
|
||||||
return {
|
return {
|
||||||
name: directory.name,
|
name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
type DirectoryCommunity,
|
type DirectoryCommunity,
|
||||||
type DirectoryList,
|
type DirectoryList,
|
||||||
} from '../lib/utils/directory-list-utils';
|
} from '../lib/utils/directory-list-utils';
|
||||||
|
import { normalizeBoardAddress } from '../lib/utils/directory-list-lookup-utils';
|
||||||
|
|
||||||
export type { DirectoriesData, DirectoryCommunity } from '../lib/utils/directory-list-utils';
|
export type { DirectoriesData, DirectoryCommunity } from '../lib/utils/directory-list-utils';
|
||||||
|
export { normalizeBoardAddress };
|
||||||
|
|
||||||
interface DirectoriesMetadata {
|
interface DirectoriesMetadata {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -42,8 +44,6 @@ let cacheMetadata: DirectoriesMetadata | null = null;
|
|||||||
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
||||||
let lastSuccessfulGitHubFetchAt: number | null = null;
|
let lastSuccessfulGitHubFetchAt: number | null = null;
|
||||||
let lastGitHubFetchAttemptAt: number | null = null;
|
let lastGitHubFetchAttemptAt: number | null = null;
|
||||||
const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const;
|
|
||||||
|
|
||||||
// Exposed for deterministic unit tests around module-level cache state.
|
// Exposed for deterministic unit tests around module-level cache state.
|
||||||
export const __resetDirectoriesModuleStateForTests = () => {
|
export const __resetDirectoriesModuleStateForTests = () => {
|
||||||
cacheCommunities = null;
|
cacheCommunities = null;
|
||||||
@@ -120,16 +120,6 @@ const adaptV2Directories = (value: Record<string, unknown>): DirectoryCommunity[
|
|||||||
return dedupeCommunities(communities);
|
return dedupeCommunities(communities);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeBoardAddress = (address: string): string => {
|
|
||||||
for (const suffix of DIRECTORY_ALIAS_SUFFIXES) {
|
|
||||||
if (address.endsWith(suffix)) {
|
|
||||||
return address.slice(0, -suffix.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return address;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findDirectoryByAddress = (directories: DirectoryCommunity[], address: string | undefined): DirectoryCommunity | undefined => {
|
export const findDirectoryByAddress = (directories: DirectoryCommunity[], address: string | undefined): DirectoryCommunity | undefined => {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { type DirectoryCommunity, normalizeBoardAddress, useDirectories } from './use-directories';
|
import { type DirectoryCommunity, useDirectories } from './use-directories';
|
||||||
import { type DirectoryList, type DirectoryListBoard, normalizeDirectoryList, sortDirectoryBoardsByRank } from '../lib/utils/directory-list-utils';
|
import { type DirectoryList, type DirectoryListBoard, normalizeDirectoryList, sortDirectoryBoardsByRank } from '../lib/utils/directory-list-utils';
|
||||||
import directoryListsData from '../data/5chan-directory-lists.json';
|
import { getDirectoryCodeForBoardAddress, getVendoredDirectoryList } from '../lib/utils/directory-list-lookup-utils';
|
||||||
|
|
||||||
export type { DirectoryListBoard } from '../lib/utils/directory-list-utils';
|
export type { DirectoryListBoard } from '../lib/utils/directory-list-utils';
|
||||||
|
export { getDirectoryCodeForBoardAddress };
|
||||||
|
|
||||||
interface DirectoryListState {
|
interface DirectoryListState {
|
||||||
list: DirectoryList | null;
|
list: DirectoryList | null;
|
||||||
@@ -29,33 +30,6 @@ const moduleCaches = new Map<string, DirectoryList>();
|
|||||||
const inFlightFetches = new Map<string, Promise<DirectoryList | null>>();
|
const inFlightFetches = new Map<string, Promise<DirectoryList | null>>();
|
||||||
const lastFetchSuccessAt = new Map<string, number>();
|
const lastFetchSuccessAt = new Map<string, number>();
|
||||||
const lastFetchAttemptAt = 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 synthesizeFromMainDirectory = (directoryCode: string, directories: DirectoryCommunity[]): DirectoryList | null => {
|
||||||
const match = directories.find((community) => community.directoryCode === directoryCode);
|
const match = directories.find((community) => community.directoryCode === directoryCode);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import directoryListsData from '../../data/5chan-directory-lists.json';
|
||||||
|
import { normalizeDirectoryList, type DirectoryList, type DirectoryListBoard } from './directory-list-utils';
|
||||||
|
|
||||||
|
const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const;
|
||||||
|
|
||||||
|
let vendoredDirectoryListsCache: DirectoryList[] | null = null;
|
||||||
|
|
||||||
|
export const normalizeBoardAddress = (address: string): string => {
|
||||||
|
for (const suffix of DIRECTORY_ALIAS_SUFFIXES) {
|
||||||
|
if (address.endsWith(suffix)) {
|
||||||
|
return address.slice(0, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVendoredDirectoryList = (directoryCode: string): DirectoryList | null =>
|
||||||
|
getVendoredDirectoryLists().find((directory) => directory.directoryCode === directoryCode) ?? null;
|
||||||
|
|
||||||
|
const findBoardInList = (list: DirectoryList, address: string): DirectoryListBoard | undefined => {
|
||||||
|
const normalizedAddress = normalizeBoardAddress(address);
|
||||||
|
return list.boards.find((board) => normalizeBoardAddress(board.address) === normalizedAddress || board.publicKey === address);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDirectoryCandidateBoardByAddress = (address: string | undefined): DirectoryListBoard | undefined => {
|
||||||
|
if (!address) return undefined;
|
||||||
|
|
||||||
|
for (const directory of getVendoredDirectoryLists()) {
|
||||||
|
const board = findBoardInList(directory, address);
|
||||||
|
if (board) return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDirectoryCodeForBoardAddress = (address: string | undefined): string | undefined => {
|
||||||
|
if (!address) return undefined;
|
||||||
|
|
||||||
|
return getVendoredDirectoryLists().find((directory) => findBoardInList(directory, address))?.directoryCode;
|
||||||
|
};
|
||||||
@@ -396,7 +396,12 @@ describe('Board', () => {
|
|||||||
routePath: '/:boardIdentifier/*',
|
routePath: '/:boardIdentifier/*',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testState.feedOptionsCalls.map((call) => call.communities)).toContainEqual([{ name: 'bizraelis.bso' }]);
|
expect(testState.feedOptionsCalls.map((call) => call.communities)).toContainEqual([
|
||||||
|
{
|
||||||
|
name: 'bizraelis.bso',
|
||||||
|
publicKey: '12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the current page feed, inserts recent account comments, and wires footer actions', async () => {
|
it('renders the current page feed, inserts recent account comments, and wires footer actions', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user