mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/fix/direct-board-community-loading'
This commit is contained in:
@@ -333,6 +333,10 @@ describe('App', () => {
|
||||
testState.account = { author: { address: '0x123' } };
|
||||
testState.accountComments = {};
|
||||
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.isSpecialEnabled = false;
|
||||
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 type { CommunityIdentifier } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { findDirectoryByAddress, type DirectoryCommunity, useDirectories } from './use-directories';
|
||||
import { getDirectoryCandidateBoardByAddress } from '../lib/utils/directory-list-lookup-utils';
|
||||
|
||||
const isLikelyCommunityName = (value: string) => value.includes('.');
|
||||
|
||||
@@ -10,20 +11,24 @@ const getCommunityIdentifier = (communityAddress: string | undefined, directorie
|
||||
}
|
||||
|
||||
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 {
|
||||
name: directory.name,
|
||||
publicKey: directory.publicKey,
|
||||
name,
|
||||
publicKey,
|
||||
};
|
||||
}
|
||||
if (directory?.publicKey) {
|
||||
if (publicKey) {
|
||||
return {
|
||||
publicKey: directory.publicKey,
|
||||
publicKey,
|
||||
};
|
||||
}
|
||||
if (directory?.name) {
|
||||
if (name) {
|
||||
return {
|
||||
name: directory.name,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ import {
|
||||
type DirectoryCommunity,
|
||||
type DirectoryList,
|
||||
} 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 { normalizeBoardAddress };
|
||||
|
||||
interface DirectoriesMetadata {
|
||||
title: string;
|
||||
@@ -42,8 +44,6 @@ let cacheMetadata: DirectoriesMetadata | null = null;
|
||||
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
||||
let lastSuccessfulGitHubFetchAt: 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.
|
||||
export const __resetDirectoriesModuleStateForTests = () => {
|
||||
cacheCommunities = null;
|
||||
@@ -120,16 +120,6 @@ const adaptV2Directories = (value: Record<string, unknown>): DirectoryCommunity[
|
||||
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 => {
|
||||
if (!address) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 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 { getDirectoryCodeForBoardAddress };
|
||||
|
||||
interface DirectoryListState {
|
||||
list: DirectoryList | null;
|
||||
@@ -29,33 +30,6 @@ 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);
|
||||
|
||||
@@ -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/*',
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user