mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(rules page): rebuild directory rules page with vendored lists (#1147)
* feat(rules page): rebuild directory rules page with vendored lists Mirror per-directory JSON from lists into src/data/5chan-directories, rework /rules layout to match 4chan (sidebar nav, category boxes, P2P load), and keep spoiler markup visible in rule text via parseSpoilers. * fix(rules): keep directory defaults cache atomic * fix(rules): derive defaults from shared directory refresh * fix(rules): address directory refresh edge cases
This commit is contained in:
@@ -179,7 +179,10 @@ vi.mock('../external-number-quote-link', () => ({
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const renderMarkdown = async (props: { content: string; postCid?: string; communityAddress?: string; title?: string }, initialEntry = '/mu/thread/post-1') => {
|
||||
const renderMarkdown = async (
|
||||
props: { content: string; postCid?: string; communityAddress?: string; title?: string; parseSpoilers?: boolean },
|
||||
initialEntry = '/mu/thread/post-1',
|
||||
) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(MemoryRouter, { initialEntries: [initialEntry] }, createElement(Markdown, props)));
|
||||
});
|
||||
@@ -230,6 +233,16 @@ describe('Markdown', () => {
|
||||
expect(links.find((link) => link.getAttribute('href') === '/fit')?.textContent).toBe('>>>/fit/');
|
||||
});
|
||||
|
||||
it('keeps spoiler markup visible when parseSpoilers is false', async () => {
|
||||
await renderMarkdown({
|
||||
content: 'enclose it like so: [spoiler]spoiled text[/spoiler].',
|
||||
parseSpoilers: false,
|
||||
});
|
||||
|
||||
expect(container.querySelector('.spoilertext')).toBeNull();
|
||||
expect(container.textContent).toContain('[spoiler]spoiled text[/spoiler]');
|
||||
});
|
||||
|
||||
it('renders greentext for any leading marker run while preserving quote links', async () => {
|
||||
await renderMarkdown({
|
||||
content: '>green line\n>>test\n>>>>>>>test\n>>42\n>>>/fit/',
|
||||
|
||||
@@ -171,6 +171,11 @@ const COMBINED_REGEX = new RegExp(
|
||||
'g',
|
||||
);
|
||||
|
||||
const COMBINED_REGEX_WITHOUT_SPOILER = new RegExp(
|
||||
`(${CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX.source})|(${CROSSBOARD_REGEX.source})|(${QUOTE_LINK_REGEX.source})|(${URL_REGEX.source})`,
|
||||
'g',
|
||||
);
|
||||
|
||||
const makeTokenKey = (prefix: string, type: Token['type'], start: number, end: number): string => `${prefix}${type}:${start}:${end}`;
|
||||
|
||||
const isGreentextLine = (line: string): boolean => {
|
||||
@@ -236,11 +241,11 @@ function getCrossboardRoute(fullPattern: string): string | null {
|
||||
return `/${pathPart}`;
|
||||
}
|
||||
|
||||
function tokenize(text: string, keyPrefix = ''): Token[] {
|
||||
function tokenize(text: string, keyPrefix = '', parseSpoilers = true): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const regex = new RegExp(COMBINED_REGEX.source, 'g');
|
||||
const regex = new RegExp((parseSpoilers ? COMBINED_REGEX : COMBINED_REGEX_WITHOUT_SPOILER).source, 'g');
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
@@ -256,13 +261,13 @@ function tokenize(text: string, keyPrefix = ''): Token[] {
|
||||
});
|
||||
}
|
||||
|
||||
if (match[1] !== undefined) {
|
||||
if (parseSpoilers && match[1] !== undefined) {
|
||||
const innerContent = match[2];
|
||||
const key = makeTokenKey(keyPrefix, 'spoiler', matchStart, matchEnd);
|
||||
tokens.push({ key, type: 'spoiler', tokens: tokenize(innerContent, `${key}/`) });
|
||||
} else if (match[3] !== undefined) {
|
||||
const boardIdentifier = match[4];
|
||||
const number = parseInt(match[5], 10);
|
||||
tokens.push({ key, type: 'spoiler', tokens: tokenize(innerContent, `${key}/`, parseSpoilers) });
|
||||
} else if (match[parseSpoilers ? 3 : 1] !== undefined) {
|
||||
const boardIdentifier = match[parseSpoilers ? 4 : 2];
|
||||
const number = parseInt(match[parseSpoilers ? 5 : 3], 10);
|
||||
if (boardIdentifier && !Number.isNaN(number)) {
|
||||
tokens.push({
|
||||
key: makeTokenKey(keyPrefix, 'crossBoardNumberQuoteLink', matchStart, matchEnd),
|
||||
@@ -277,8 +282,8 @@ function tokenize(text: string, keyPrefix = ''): Token[] {
|
||||
} else {
|
||||
tokens.push({ key: makeTokenKey(keyPrefix, 'text', matchStart, matchEnd), type: 'text', value: fullMatch });
|
||||
}
|
||||
} else if (match[6] !== undefined) {
|
||||
const pathPart = match[7];
|
||||
} else if (match[parseSpoilers ? 6 : 4] !== undefined) {
|
||||
const pathPart = match[parseSpoilers ? 7 : 5];
|
||||
const fullPattern = `>>>/${pathPart}`;
|
||||
const route = getCrossboardRoute(fullPattern);
|
||||
if (route) {
|
||||
@@ -291,10 +296,10 @@ function tokenize(text: string, keyPrefix = ''): Token[] {
|
||||
} else {
|
||||
tokens.push({ key: makeTokenKey(keyPrefix, 'text', matchStart, matchEnd), type: 'text', value: fullMatch });
|
||||
}
|
||||
} else if (match[8] !== undefined) {
|
||||
const number = parseInt(match[9], 10);
|
||||
} else if (match[parseSpoilers ? 8 : 6] !== undefined) {
|
||||
const number = parseInt(match[parseSpoilers ? 9 : 7], 10);
|
||||
tokens.push({ key: makeTokenKey(keyPrefix, 'quoteLink', matchStart, matchEnd), type: 'quoteLink', number });
|
||||
} else if (match[10] !== undefined) {
|
||||
} else if (match[parseSpoilers ? 10 : 8] !== undefined) {
|
||||
const { href, trailingText } = splitUrlTrailingText(fullMatch);
|
||||
const linkEnd = trailingText ? matchEnd - trailingText.length : matchEnd;
|
||||
tokens.push({ key: makeTokenKey(keyPrefix, 'url', matchStart, linkEnd), type: 'url', href });
|
||||
@@ -322,6 +327,7 @@ interface RenderContext {
|
||||
postCid?: string;
|
||||
communityAddress?: string;
|
||||
enableQstBbcode: boolean;
|
||||
parseSpoilers: boolean;
|
||||
}
|
||||
|
||||
interface MarkdownProps {
|
||||
@@ -329,6 +335,8 @@ interface MarkdownProps {
|
||||
title?: string;
|
||||
postCid?: string;
|
||||
communityAddress?: string;
|
||||
/** When false, [spoiler] tags stay visible (e.g. rules text teaching the syntax). Default true. */
|
||||
parseSpoilers?: boolean;
|
||||
}
|
||||
|
||||
const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number: number; threadPostCid?: string; communityAddress?: string }) => {
|
||||
@@ -575,7 +583,13 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod
|
||||
const matchEnd = matchStart + fullMatch.length;
|
||||
|
||||
if (matchStart > lastIndex) {
|
||||
elements.push(<TokenList key={`text-${lastIndex}-${matchStart}`} tokens={tokenize(line.slice(lastIndex, matchStart), `${lastIndex}:`)} context={context} />);
|
||||
elements.push(
|
||||
<TokenList
|
||||
key={`text-${lastIndex}-${matchStart}`}
|
||||
tokens={tokenize(line.slice(lastIndex, matchStart), `${lastIndex}:`, context.parseSpoilers)}
|
||||
context={context}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (nextMatch.type === 'dice') {
|
||||
@@ -586,7 +600,7 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod
|
||||
if (fortune) {
|
||||
elements.push(<Fortune key={`fortune-${matchStart}`} color={fortune.color} text={fortune.text} />);
|
||||
} else {
|
||||
elements.push(<TokenList key={`text-${matchStart}-${matchEnd}`} tokens={tokenize(fullMatch, `${matchStart}:`)} context={context} />);
|
||||
elements.push(<TokenList key={`text-${matchStart}-${matchEnd}`} tokens={tokenize(fullMatch, `${matchStart}:`, context.parseSpoilers)} context={context} />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,13 +608,15 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
elements.push(<TokenList key={`text-${lastIndex}-${line.length}`} tokens={tokenize(line.slice(lastIndex), `${lastIndex}:`)} context={context} />);
|
||||
elements.push(
|
||||
<TokenList key={`text-${lastIndex}-${line.length}`} tokens={tokenize(line.slice(lastIndex), `${lastIndex}:`, context.parseSpoilers)} context={context} />,
|
||||
);
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps) => {
|
||||
const Markdown = ({ content, title, postCid, communityAddress, parseSpoilers = true }: MarkdownProps) => {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||
@@ -612,7 +628,7 @@ const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps)
|
||||
const elements: React.ReactNode[] = [];
|
||||
let lineOffset = 0;
|
||||
|
||||
const context = { isInCatalogView, postCid, communityAddress, enableQstBbcode };
|
||||
const context = { isInCatalogView, postCid, communityAddress, enableQstBbcode, parseSpoilers };
|
||||
|
||||
lines.forEach((line, lineIndex) => {
|
||||
const lineKey = `line-${lineOffset}`;
|
||||
@@ -640,7 +656,7 @@ const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps)
|
||||
});
|
||||
|
||||
return elements;
|
||||
}, [content, isInCatalogView, postCid, communityAddress, enableQstBbcode]);
|
||||
}, [content, isInCatalogView, postCid, communityAddress, enableQstBbcode, parseSpoilers]);
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /3/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-3-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "3dcg-posting.bso",
|
||||
"publicKey": "12D3KooWBRCcwxWTx23tyK7Uhqs9ViE3MnSgvWSLip2ikrWaD3c2",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /a/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-a-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "anime-and-manga.bso",
|
||||
"publicKey": "12D3KooWMDt3RMVMHrm2pEEyfraBtHP86dmkim7EVAapDj3Mchdy",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /adv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-adv-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "advice-posting.bso",
|
||||
"publicKey": "12D3KooWH5UFNs9yfwJVsSUhEW5bYrtF8KmzfeJzwg84qWMoFJVY",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /an/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-an-directory.json",
|
||||
"createdAt": 1779262963,
|
||||
"updatedAt": 1779262963,
|
||||
"boards": [
|
||||
{
|
||||
"address": "animals-and-nature.bso",
|
||||
"publicKey": "12D3KooWSpKszPM2c17KBgbnoRrkWPCHJosGGFg3bKnzarYhHeSc",
|
||||
"addedAt": 1779262963,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /b/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-b-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "random-nsfw.bso",
|
||||
"publicKey": "12D3KooWJkJYkuVbJQhapcVGpo8oNmsXsKmFRrFqc1SHezZW9fX8",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /bant/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-bant-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "international-nsfw.bso",
|
||||
"publicKey": "12D3KooWPuYtTzrq8gFnv6yM1r4egy1rd4cxLr1nsXTjfdYkfwcN",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"description": "Boards competing to host the /biz/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-biz-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779212075,
|
||||
"boards": [
|
||||
{
|
||||
"address": "business-and-finance.bso",
|
||||
"publicKey": "12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "rinse12.bso"
|
||||
},
|
||||
{
|
||||
"address": "bizraelis.bso",
|
||||
"publicKey": "12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE",
|
||||
"addedAt": 1779212075,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /c/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-c-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "anime-cute.bso",
|
||||
"publicKey": "12D3KooWMKVj7dzag2fcYP24kgmxDz2DSvE7yGzBW9fJEYA94aMC",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /ck/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ck-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "food-and-cooking.bso",
|
||||
"publicKey": "12D3KooWFfANpSeemwvqqGsXNPHDuyDMCK6wE8cxGTuNw7xoWUp7",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /co/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-co-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "comics-and-cartoons.bso",
|
||||
"publicKey": "12D3KooWRLmyg671KVYb1xekZ8eTQSMfRraB3DmQZPsKbCdMsEuc",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /diy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-diy-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "do-it-yourself.bso",
|
||||
"publicKey": "12D3KooWKKbiaFu7x2EwueN6wxw3DJRQsvCv8KVRGbTTYpsGZXVa",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json",
|
||||
"createdAt": 1780123897,
|
||||
"updatedAt": 1780123897,
|
||||
"rules": [
|
||||
"The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban."
|
||||
],
|
||||
"boards": [
|
||||
{
|
||||
"address": "flash-posting.bso",
|
||||
"publicKey": "12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F",
|
||||
"addedAt": 1780123897,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /fa/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fa-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "fashion-posting.bso",
|
||||
"publicKey": "12D3KooWMLd7K4MKPY4YJrpwV2vrjFN357x93bHs2g4gSetTLJ6t",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /fit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fit-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "fitness-posting.bso",
|
||||
"publicKey": "12D3KooWHcqcJJmjAiBJA3uyhSW6ep4HFadUfteDrzU1Pn9ALkkw",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /g/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-g-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "technology-posting.bso",
|
||||
"publicKey": "12D3KooWFnLrUYHpvqki7gbL4w9JzdxjpQPKE2JBDEd23Ly6X82X",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /gd/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gd-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "graphic-design.bso",
|
||||
"publicKey": "12D3KooWSrH1Kp6TvaLxGS3vtoqqFdy3gRMkTKfrYGPaDAcJRJAd",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /gif/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gif-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "adult-gif.bso",
|
||||
"publicKey": "12D3KooWJP8v5VUzKUfxr38BbhQPb5yidF3WapGQ6CjLctBV6QC1",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /his/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-his-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "history-posting.bso",
|
||||
"publicKey": "12D3KooWN7bzigSgU4rxayrPCjvq2ETJMN9Zv3cyazpRYh36QWiN",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /i/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-i-directory.json",
|
||||
"createdAt": 1780054375,
|
||||
"updatedAt": 1780054375,
|
||||
"boards": [
|
||||
{
|
||||
"address": "oekaki-posting.bso",
|
||||
"publicKey": "12D3KooWNAqMhmHi3hPSbcH27Q7cqBCuN2Mu9HNzs6fB8YX14k4o",
|
||||
"addedAt": 1780054375,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /ic/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ic-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "artwork-critique.bso",
|
||||
"publicKey": "12D3KooWQZ5HkAspHUPGEVoSaAZgqachZGtxCXxAXYBDTreR4uWJ",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /int/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-int-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "international-sfw.bso",
|
||||
"publicKey": "12D3KooWMSUYcergS2BwbRzFynR5hQURc962W9iR81eTJKz89Q6n",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /jp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-jp-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "otaku-culture.bso",
|
||||
"publicKey": "12D3KooWFtPbTBQ6Lit5wH2PHCvQQqj3RB5v2QHPtT1cnGeKjBaW",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /k/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-k-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "weapons-posting.bso",
|
||||
"publicKey": "12D3KooWNRSdq3aLHHCvm4zyFQfASgYzy5LGXRXnKC4uj6aUXzHq",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /lit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-lit-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "literature-posting.bso",
|
||||
"publicKey": "12D3KooWKXimxiZWtgF3LoTdaW3btHhDzJN7P3qrsgHwRkow186x",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /m/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-m-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "mecha-posting.bso",
|
||||
"publicKey": "12D3KooWH7TCLGzDmvptLmnoxbLK2EjSuysMfj1ScN1ekFuba1Fu",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /mlp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mlp-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "pony-posting.bso",
|
||||
"publicKey": "12D3KooWEViZKRnJjqKDXGadQtz1547wgLTFPAaFYPBZ5NVwJ3jJ",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /mu/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mu-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "music-posting.bso",
|
||||
"publicKey": "12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /n/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-n-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "transportation-posting.bso",
|
||||
"publicKey": "12D3KooWBLfdUT4sbVjzsQBz6EFPGWJC5byjnG44i5HHBKk7XKc3",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /news/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-news-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "current-news.bso",
|
||||
"publicKey": "12D3KooWSdDpdxUajbj19SHhUxcNS7wG9QbQqYvj29mCwokqnFNS",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /o/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-o-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "auto-posting.bso",
|
||||
"publicKey": "12D3KooWBD4agW9pc73hBNXtXWTpZCukcFvigmkM8Z2MDKR72CCk",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /out/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-out-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "outdoors-posting.bso",
|
||||
"publicKey": "12D3KooWQ2nPGxQxuSMY92kM3zKu1rBekJsXxXb5uNkHLDfSSxTm",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /p/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-p-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "photography-posting.bso",
|
||||
"publicKey": "12D3KooWK7qqtH32qAeZ5nUZGHBEsiaGsXGhMU9AWDKYzKCcit7w",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /po/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-po-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "papercraft-and-origami.bso",
|
||||
"publicKey": "12D3KooWGfhD9o9T4QRYX2RQgwXRmEmWXmaw4bAGyDHioz98g3ro",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"description": "Boards competing to host the /pol/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pol-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779212075,
|
||||
"boards": [
|
||||
{
|
||||
"address": "politically-incorrect.bso",
|
||||
"publicKey": "12D3KooWMVob74DQoTLGZ4B8kWgPwDfbtiWqdJE4DrGtf2rmVw36",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "rinse12.bso"
|
||||
},
|
||||
{
|
||||
"address": "nothing-is-beyond-our-reach.bso",
|
||||
"publicKey": "12D3KooWDpiDmJCGKbXB3RJFfATmRMzmwTkjimBvf8xKZmUx2L1P",
|
||||
"addedAt": 1779212075,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /pw/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pw-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "professional-wrestling.bso",
|
||||
"publicKey": "12D3KooWBMHPHSJhwKivg6KhbGScgau8xBBNqVQKZk24NZQo7beU",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /qst/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-qst-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "quests-posting.bso",
|
||||
"publicKey": "12D3KooWBmh43DUBbqXAeJVxxu9qgWgMqn91UBECgP4aTuxS5uK2",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /r9k/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-r9k-directory.json",
|
||||
"createdAt": 1779523735,
|
||||
"updatedAt": 1779525738,
|
||||
"boards": [
|
||||
{
|
||||
"address": "robot9002.bso",
|
||||
"publicKey": "12D3KooWPXkEw8GSZcM8N97fkWJjZGV7hLccZSrf2PqHUKrp4xJQ",
|
||||
"addedAt": 1779525738,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /s5s/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-s5s-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "shit-5chan-says.bso",
|
||||
"publicKey": "12D3KooWR4vHjSsTHs3MU436avk4eW1XRqY9zKfVcvvZXkLdzJo6",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /sci/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sci-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "science-and-math.bso",
|
||||
"publicKey": "12D3KooWEtHqwtEpgHsXS8VSFcGDt2DhzeqaLm9tTCRFucXaWp62",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /soc/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-soc-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "cams-and-meetups.bso",
|
||||
"publicKey": "12D3KooWCGnYfbrm8UZpv8YxViXgJJx9hha486LCAFFXSEYNtj1B",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /sp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sp-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "sports-posting.bso",
|
||||
"publicKey": "12D3KooWGJA6zN3Q63FtSgwNhtfA26Skdzdxz5X7A9PFfE4FBMGE",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /t/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-t-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "torrents-posting.bso",
|
||||
"publicKey": "12D3KooWFwieYtPDEd3UBK5qG6HFZ3Qu8cDd4B4RcY2HJ9CBkfaZ",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "rinse12.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /tg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tg-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "traditional-games.bso",
|
||||
"publicKey": "12D3KooWK6G3EHUCkfcgX6kPDihXJAdpucrZvn4WjPwf7fF484nt",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /toy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-toy-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "toys-posting.bso",
|
||||
"publicKey": "12D3KooWQrRW8JHu1HagE9E3QUB7hAnSbeYHLkz5mTbSivhkoPtJ",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /trv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-trv-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "travel-posting.bso",
|
||||
"publicKey": "12D3KooWP9kG9qYxHoyxfDSWiNpeuGfuxwbDdfu6vtRwvVY9uU96",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /tv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tv-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "television-and-film.bso",
|
||||
"publicKey": "12D3KooWGSRg4crzV5vXEya1ghXD7UBTqimqXTQvNUqy9NdzZNyd",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /v/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-v-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-posting.bso",
|
||||
"publicKey": "12D3KooWRAbo3HU5ThCAkJGQ5q7MTEJfhAX6Hu7xHyPtxHGi1Fgh",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vg-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogame-generals.bso",
|
||||
"publicKey": "12D3KooWMvAcJFV45CS3hSZcx7EFhwhQrXfXhVHXQJFPfKeakWfq",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vip/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vip-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "very-important-posts.bso",
|
||||
"publicKey": "12D3KooWNMnBJ5phkBWrcjfdnzdqPwCARmXwGWeZDKNJvCVPR8GT",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vm/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vm-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-multiplayer.bso",
|
||||
"publicKey": "12D3KooWMW3B4R3C7z5M6zDo9Q9t3fCMuUwHbnPWARsDSqiUYybU",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vmg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vmg-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-mobile.bso",
|
||||
"publicKey": "12D3KooWLSaxYpQb4Wx7KxiJaXQD9SgzmBD67W41CN6gNLTxASVg",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vp-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "pokemon-posting.bso",
|
||||
"publicKey": "12D3KooWFhrazFTHLh6sSA9qL2wTf4piBar2HXtirKNTfTwSTjN3",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vr-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "retro-games.bso",
|
||||
"publicKey": "12D3KooWJfwp2WRwU3gsaNNPjZ1iHgxc1QFySr361cQarkhLp1fY",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vrpg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vrpg-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-rpg.bso",
|
||||
"publicKey": "12D3KooWJ4HZgaL3tPSRp5LfnFmwasxdQenPuDpPBYHHEaUMJDiF",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vst/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vst-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-strategy.bso",
|
||||
"publicKey": "12D3KooWBNAetYuTx5Zosf6jRVJ2GpMx3jzEQufDcP4DRTXzAgwp",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /vt/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vt-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "virtual-youtubers.bso",
|
||||
"publicKey": "12D3KooWKiziqgcvVLMDThXQC4cPi5SJmFSQFoc6jv1kSRU1XXxV",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /w/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-w-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "anime-wallpapers.bso",
|
||||
"publicKey": "12D3KooWPpdMSMLnz5qZ6NR3jTjqmqdpbMT3tDM5RwzWSPYtd9AV",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /wg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wg-directory.json",
|
||||
"createdAt": 1779517639,
|
||||
"updatedAt": 1779517639,
|
||||
"boards": [
|
||||
{
|
||||
"address": "wallpapers-general.bso",
|
||||
"publicKey": "12D3KooWJCXw8a5CKgH5248qXMDQdG5AFxP4ZvbsAmFCGfuykE3U",
|
||||
"addedAt": 1779517639,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /wsg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsg-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "worksafe-gif.bso",
|
||||
"publicKey": "12D3KooWAGHV9th2FN48KPvHMJwxen8h5mSM39pkaZWvvzcWMPr9",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /wsr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsr-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "worksafe-requests.bso",
|
||||
"publicKey": "12D3KooWKuKGqAAKQT7GY1cnXqiXcbeVPJULXPwTByfSy4Ci7eBg",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /x/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-x-directory.json",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "paranormal-posting.bso",
|
||||
"publicKey": "12D3KooWFZn7r5gYKbq5YQMr1FYQ5eUwoZr41Tjd8KmTgQ4dxTTP",
|
||||
"addedAt": 1779182014,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "Boards competing to host the /xs/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-xs-directory.json",
|
||||
"createdAt": 1779357555,
|
||||
"updatedAt": 1779357555,
|
||||
"boards": [
|
||||
{
|
||||
"address": "extreme-sports.bso",
|
||||
"publicKey": "12D3KooWBwnXwtridc4b3VNnhK2KyNHsm52wDrGPYYGLNf8f34bm",
|
||||
"addedAt": 1779357555,
|
||||
"owner": "plebeius.bso"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,857 +0,0 @@
|
||||
{
|
||||
"title": "5chan directories",
|
||||
"description": "Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories",
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1780123897,
|
||||
"directories": [
|
||||
{
|
||||
"directoryCode": "a",
|
||||
"title": "/a/ - Anime & Manga",
|
||||
"description": "Boards competing to host the /a/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-a-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "anime-and-manga.bso",
|
||||
"publicKey": "12D3KooWMDt3RMVMHrm2pEEyfraBtHP86dmkim7EVAapDj3Mchdy",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "f",
|
||||
"title": "/f/ - Flash",
|
||||
"description": "Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": false,
|
||||
"hasFlags": false,
|
||||
"postFlairs": true,
|
||||
"requirePostFlairs": true,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": false,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 50
|
||||
},
|
||||
"rules": [
|
||||
"The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban."
|
||||
],
|
||||
"createdAt": 1780123897,
|
||||
"updatedAt": 1780123897,
|
||||
"boards": [
|
||||
{
|
||||
"address": "flash-posting.bso",
|
||||
"publicKey": "12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1780123897
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "co",
|
||||
"title": "/co/ - Comics & Cartoons",
|
||||
"description": "Boards competing to host the /co/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-co-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "comics-and-cartoons.bso",
|
||||
"publicKey": "12D3KooWRLmyg671KVYb1xekZ8eTQSMfRraB3DmQZPsKbCdMsEuc",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "ck",
|
||||
"title": "/ck/ - Food & Cooking",
|
||||
"description": "Boards competing to host the /ck/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ck-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "food-and-cooking.bso",
|
||||
"publicKey": "12D3KooWFfANpSeemwvqqGsXNPHDuyDMCK6wE8cxGTuNw7xoWUp7",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "pol",
|
||||
"title": "/pol/ - Politically Incorrect",
|
||||
"description": "Boards competing to host the /pol/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pol-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-post",
|
||||
"safeForWork": false,
|
||||
"hasFlags": true,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 20
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779212075,
|
||||
"boards": [
|
||||
{
|
||||
"address": "politically-incorrect.bso",
|
||||
"publicKey": "12D3KooWMVob74DQoTLGZ4B8kWgPwDfbtiWqdJE4DrGtf2rmVw36",
|
||||
"owner": "rinse12.bso",
|
||||
"addedAt": 1779182014
|
||||
},
|
||||
{
|
||||
"address": "nothing-is-beyond-our-reach.bso",
|
||||
"publicKey": "12D3KooWDpiDmJCGKbXB3RJFfATmRMzmwTkjimBvf8xKZmUx2L1P",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779212075
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "biz",
|
||||
"title": "/biz/ - Business & Finance",
|
||||
"description": "Boards competing to host the /biz/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-biz-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-post",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 20
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779212075,
|
||||
"boards": [
|
||||
{
|
||||
"address": "business-and-finance.bso",
|
||||
"publicKey": "12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh",
|
||||
"owner": "rinse12.bso",
|
||||
"addedAt": 1779182014
|
||||
},
|
||||
{
|
||||
"address": "bizraelis.bso",
|
||||
"publicKey": "12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779212075
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "sci",
|
||||
"title": "/sci/ - Science & Math",
|
||||
"description": "Boards competing to host the /sci/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sci-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "science-and-math.bso",
|
||||
"publicKey": "12D3KooWEtHqwtEpgHsXS8VSFcGDt2DhzeqaLm9tTCRFucXaWp62",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "g",
|
||||
"title": "/g/ - Technology",
|
||||
"description": "Boards competing to host the /g/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-g-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "technology-posting.bso",
|
||||
"publicKey": "12D3KooWFnLrUYHpvqki7gbL4w9JzdxjpQPKE2JBDEd23Ly6X82X",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "v",
|
||||
"title": "/v/ - Video Games",
|
||||
"description": "Boards competing to host the /v/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-v-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogames-posting.bso",
|
||||
"publicKey": "12D3KooWRAbo3HU5ThCAkJGQ5q7MTEJfhAX6Hu7xHyPtxHGi1Fgh",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "vg",
|
||||
"title": "/vg/ - Video Game Generals",
|
||||
"description": "Boards competing to host the /vg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vg-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 20
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "videogame-generals.bso",
|
||||
"publicKey": "12D3KooWMvAcJFV45CS3hSZcx7EFhwhQrXfXhVHXQJFPfKeakWfq",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "vr",
|
||||
"title": "/vr/ - Retro Games",
|
||||
"description": "Boards competing to host the /vr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vr-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "retro-games.bso",
|
||||
"publicKey": "12D3KooWJfwp2WRwU3gsaNNPjZ1iHgxc1QFySr361cQarkhLp1fY",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "fit",
|
||||
"title": "/fit/ - Fitness",
|
||||
"description": "Boards competing to host the /fit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fit-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "fitness-posting.bso",
|
||||
"publicKey": "12D3KooWHcqcJJmjAiBJA3uyhSW6ep4HFadUfteDrzU1Pn9ALkkw",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "sp",
|
||||
"title": "/sp/ - Sports",
|
||||
"description": "Boards competing to host the /sp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sp-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": true,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "sports-posting.bso",
|
||||
"publicKey": "12D3KooWGJA6zN3Q63FtSgwNhtfA26Skdzdxz5X7A9PFfE4FBMGE",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "tg",
|
||||
"title": "/tg/ - Traditional Games",
|
||||
"description": "Boards competing to host the /tg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tg-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "traditional-games.bso",
|
||||
"publicKey": "12D3KooWK6G3EHUCkfcgX6kPDihXJAdpucrZvn4WjPwf7fF484nt",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "adv",
|
||||
"title": "/adv/ - Advice",
|
||||
"description": "Boards competing to host the /adv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-adv-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "advice-posting.bso",
|
||||
"publicKey": "12D3KooWH5UFNs9yfwJVsSUhEW5bYrtF8KmzfeJzwg84qWMoFJVY",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "wsg",
|
||||
"title": "/wsg/ - Worksafe GIF",
|
||||
"description": "Boards competing to host the /wsg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsg-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "worksafe-gif.bso",
|
||||
"publicKey": "12D3KooWAGHV9th2FN48KPvHMJwxen8h5mSM39pkaZWvvzcWMPr9",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "diy",
|
||||
"title": "/diy/ - Do It Yourself",
|
||||
"description": "Boards competing to host the /diy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-diy-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "do-it-yourself.bso",
|
||||
"publicKey": "12D3KooWKKbiaFu7x2EwueN6wxw3DJRQsvCv8KVRGbTTYpsGZXVa",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "out",
|
||||
"title": "/out/ - Outdoors",
|
||||
"description": "Boards competing to host the /out/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-out-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "outdoors-posting.bso",
|
||||
"publicKey": "12D3KooWQ2nPGxQxuSMY92kM3zKu1rBekJsXxXb5uNkHLDfSSxTm",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "i",
|
||||
"title": "/i/ - Oekaki",
|
||||
"description": "Boards competing to host the /i/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-i-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": false,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1780054375,
|
||||
"updatedAt": 1780054375,
|
||||
"boards": [
|
||||
{
|
||||
"address": "oekaki-posting.bso",
|
||||
"publicKey": "12D3KooWNAqMhmHi3hPSbcH27Q7cqBCuN2Mu9HNzs6fB8YX14k4o",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1780054375
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "ic",
|
||||
"title": "/ic/ - Artwork/Critique",
|
||||
"description": "Boards competing to host the /ic/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ic-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "artwork-critique.bso",
|
||||
"publicKey": "12D3KooWQZ5HkAspHUPGEVoSaAZgqachZGtxCXxAXYBDTreR4uWJ",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "mu",
|
||||
"title": "/mu/ - Music",
|
||||
"description": "Boards competing to host the /mu/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mu-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "music-posting.bso",
|
||||
"publicKey": "12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "int",
|
||||
"title": "/int/ - International",
|
||||
"description": "Boards competing to host the /int/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-int-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": true,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "international-sfw.bso",
|
||||
"publicKey": "12D3KooWMSUYcergS2BwbRzFynR5hQURc962W9iR81eTJKz89Q6n",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "lit",
|
||||
"title": "/lit/ - Literature",
|
||||
"description": "Boards competing to host the /lit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-lit-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "literature-posting.bso",
|
||||
"publicKey": "12D3KooWKXimxiZWtgF3LoTdaW3btHhDzJN7P3qrsgHwRkow186x",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "his",
|
||||
"title": "/his/ - History & Humanities",
|
||||
"description": "Boards competing to host the /his/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-his-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "history-posting.bso",
|
||||
"publicKey": "12D3KooWN7bzigSgU4rxayrPCjvq2ETJMN9Zv3cyazpRYh36QWiN",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "tv",
|
||||
"title": "/tv/ - Television & Film",
|
||||
"description": "Boards competing to host the /tv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tv-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": false,
|
||||
"noSpoilerReplies": false,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "television-and-film.bso",
|
||||
"publicKey": "12D3KooWGSRg4crzV5vXEya1ghXD7UBTqimqXTQvNUqy9NdzZNyd",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "t",
|
||||
"title": "/t/ - Torrents",
|
||||
"description": "Boards competing to host the /t/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-t-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": false,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "torrents-posting.bso",
|
||||
"publicKey": "12D3KooWFwieYtPDEd3UBK5qG6HFZ3Qu8cDd4B4RcY2HJ9CBkfaZ",
|
||||
"owner": "rinse12.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "x",
|
||||
"title": "/x/ - Paranormal",
|
||||
"description": "Boards competing to host the /x/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-x-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "paranormal-posting.bso",
|
||||
"publicKey": "12D3KooWFZn7r5gYKbq5YQMr1FYQ5eUwoZr41Tjd8KmTgQ4dxTTP",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "vip",
|
||||
"title": "/vip/ - Very Important Posts",
|
||||
"description": "Boards competing to host the /vip/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vip-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "very-important-posts.bso",
|
||||
"publicKey": "12D3KooWNMnBJ5phkBWrcjfdnzdqPwCARmXwGWeZDKNJvCVPR8GT",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "gif",
|
||||
"title": "/gif/ - Adult GIF",
|
||||
"description": "Boards competing to host the /gif/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gif-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": false,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "adult-gif.bso",
|
||||
"publicKey": "12D3KooWJP8v5VUzKUfxr38BbhQPb5yidF3WapGQ6CjLctBV6QC1",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "bant",
|
||||
"title": "/bant/ - International/Random",
|
||||
"description": "Boards competing to host the /bant/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-bant-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-post",
|
||||
"safeForWork": false,
|
||||
"hasFlags": true,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "international-nsfw.bso",
|
||||
"publicKey": "12D3KooWPuYtTzrq8gFnv6yM1r4egy1rd4cxLr1nsXTjfdYkfwcN",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "b",
|
||||
"title": "/b/ - Random",
|
||||
"description": "Boards competing to host the /b/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-b-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": false,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779182014,
|
||||
"updatedAt": 1779182014,
|
||||
"boards": [
|
||||
{
|
||||
"address": "random-nsfw.bso",
|
||||
"publicKey": "12D3KooWJkJYkuVbJQhapcVGpo8oNmsXsKmFRrFqc1SHezZW9fX8",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779182014
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"directoryCode": "an",
|
||||
"title": "/an/ - Animals & Nature",
|
||||
"description": "Boards competing to host the /an/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-an-directory.json",
|
||||
"features": {
|
||||
"pseudonymityMode": "per-reply",
|
||||
"safeForWork": true,
|
||||
"hasFlags": false,
|
||||
"requirePostLink": true,
|
||||
"requirePostLinkIsMedia": true,
|
||||
"bumpLimit": 300,
|
||||
"noSpoilers": true,
|
||||
"noSpoilerReplies": true,
|
||||
"postsPerPage": 15
|
||||
},
|
||||
"createdAt": 1779262963,
|
||||
"updatedAt": 1779262963,
|
||||
"boards": [
|
||||
{
|
||||
"address": "animals-and-nature.bso",
|
||||
"publicKey": "12D3KooWSpKszPM2c17KBgbnoRrkWPCHJosGGFg3bKnzarYhHeSc",
|
||||
"owner": "plebeius.bso",
|
||||
"addedAt": 1779262963
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { normalizeDirectoryDefaultsData, normalizeDirectoryList, sortDirectoryLists, type DirectoryList } from '../lib/utils/directory-list-utils';
|
||||
|
||||
/**
|
||||
* Offline fallback for the directory data.
|
||||
*
|
||||
* `./5chan-directories/` is a byte-for-byte mirror of
|
||||
* https://github.com/bitsocialnet/lists/tree/master/5chan-directories (kept fresh by
|
||||
* `yarn sync:directories`). The raw per-directory files only carry candidate boards; their
|
||||
* code/title/features come from the filename and the shared defaults file, exactly like the
|
||||
* GitHub fetch path. This module assembles the same merged shape the app consumes so the
|
||||
* directory list and rules keep working when GitHub is unreachable.
|
||||
*/
|
||||
const rawModules = import.meta.glob<{ default: unknown }>('./5chan-directories/*.json', { eager: true });
|
||||
|
||||
// Matched against the file's basename (not the full path) so the leading "5chan-directories/" folder
|
||||
// segment cannot be greedily captured as part of the directory code.
|
||||
const DIRECTORY_FILE_RE = /^5chan-(.+)-directory\.json$/;
|
||||
const DEFAULTS_FILE_NAME = '5chan-directories-defaults.json';
|
||||
|
||||
// 5chan-side metadata for the assembled list (the raw mirror has none of its own).
|
||||
const VENDORED_METADATA = {
|
||||
title: '5chan directories',
|
||||
description: 'Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories',
|
||||
};
|
||||
|
||||
export interface VendoredDirectoryLists {
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
directories: DirectoryList[];
|
||||
}
|
||||
|
||||
const directoryEntries: Array<{ code: string; raw: unknown }> = [];
|
||||
let defaultsRaw: unknown = null;
|
||||
|
||||
for (const [path, module] of Object.entries(rawModules)) {
|
||||
const raw = module.default;
|
||||
const fileName = path.split('/').pop() ?? '';
|
||||
if (fileName === DEFAULTS_FILE_NAME) {
|
||||
defaultsRaw = raw;
|
||||
continue;
|
||||
}
|
||||
const match = fileName.match(DIRECTORY_FILE_RE);
|
||||
if (match) {
|
||||
directoryEntries.push({ code: match[1], raw });
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = normalizeDirectoryDefaultsData(defaultsRaw);
|
||||
|
||||
const directories = sortDirectoryLists(
|
||||
directoryEntries.map(({ code, raw }) => normalizeDirectoryList(raw, code, defaults)).filter((list): list is DirectoryList => list !== null),
|
||||
);
|
||||
|
||||
const timestamps = [defaults.createdAt, defaults.updatedAt, ...directories.flatMap((list) => [list.createdAt, list.updatedAt])].filter(
|
||||
(value): value is number => typeof value === 'number',
|
||||
);
|
||||
|
||||
export const vendoredDirectoryLists: VendoredDirectoryLists = {
|
||||
...VENDORED_METADATA,
|
||||
createdAt: timestamps.length > 0 ? Math.min(...timestamps) : 0,
|
||||
updatedAt: timestamps.length > 0 ? Math.max(...timestamps) : 0,
|
||||
directories,
|
||||
};
|
||||
|
||||
// Raw defaults JSON, consumed by the directory-defaults fallback (it normalizes on its own).
|
||||
export const vendoredDirectoryDefaults: unknown = defaultsRaw;
|
||||
@@ -9,15 +9,18 @@ import {
|
||||
useDirectories,
|
||||
useDirectoriesMetadata,
|
||||
useDirectoriesState,
|
||||
useDirectoryDefaults,
|
||||
useDirectoryAddresses,
|
||||
useDirectoryByAddress,
|
||||
type DirectoriesData,
|
||||
type DirectoryDefaultsData,
|
||||
} from '../use-directories';
|
||||
|
||||
(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 LOCALSTORAGE_KEY = '5chan-directories-cache';
|
||||
const LOCALSTORAGE_DEFAULTS_KEY = '5chan-directory-defaults-cache';
|
||||
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
||||
|
||||
type Snapshot = {
|
||||
@@ -25,6 +28,7 @@ type Snapshot = {
|
||||
state: ReturnType<typeof useDirectoriesState>;
|
||||
addresses: ReturnType<typeof useDirectoryAddresses>;
|
||||
directory: ReturnType<typeof useDirectoryByAddress>;
|
||||
directoryDefaults: DirectoryDefaultsData;
|
||||
metadata: ReturnType<typeof useDirectoriesMetadata>;
|
||||
};
|
||||
|
||||
@@ -47,6 +51,7 @@ const HookHarness = ({ address = 'music-posting.eth' }: { address?: string }) =>
|
||||
const state = useDirectoriesState();
|
||||
const addresses = useDirectoryAddresses();
|
||||
const directory = useDirectoryByAddress(address);
|
||||
const directoryDefaults = useDirectoryDefaults();
|
||||
const metadata = useDirectoriesMetadata();
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -55,9 +60,10 @@ const HookHarness = ({ address = 'music-posting.eth' }: { address?: string }) =>
|
||||
state,
|
||||
addresses,
|
||||
directory,
|
||||
directoryDefaults,
|
||||
metadata,
|
||||
};
|
||||
}, [addresses, directory, directories, metadata, state]);
|
||||
}, [addresses, directory, directories, directoryDefaults, metadata, state]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -273,6 +279,9 @@ describe('use-directories', () => {
|
||||
const persisted = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY) ?? '{}');
|
||||
expect(persisted.title).toBe('5chan directories');
|
||||
expect(persisted.communities.length).toBeGreaterThan(1);
|
||||
|
||||
const persistedDefaults = JSON.parse(localStorage.getItem(LOCALSTORAGE_DEFAULTS_KEY) ?? '{}');
|
||||
expect(persistedDefaults.description).toBe('remote defaults');
|
||||
});
|
||||
|
||||
it('does not refetch GitHub directories for each later hook mount after a successful refresh', async () => {
|
||||
@@ -342,6 +351,64 @@ describe('use-directories', () => {
|
||||
expect(warnSpy.mock.calls.some((call: ConsoleWarnCall) => String(call[0]).includes('Failed to fetch directory list "biz"'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not expose refreshed defaults before the matching directory payload commits', async () => {
|
||||
const pendingDefaults = createDeferred<ReturnType<typeof createFetchResponse>>();
|
||||
const pendingFetches = new Map<string, Deferred<ReturnType<typeof createFetchResponse>>>();
|
||||
fetchMock.mockImplementation((url: unknown) => {
|
||||
if (isDefaultsUrl(url)) {
|
||||
return pendingDefaults.promise;
|
||||
}
|
||||
const code = getDirectoryCodeFromUrl(url);
|
||||
const deferred = createDeferred<ReturnType<typeof createFetchResponse>>();
|
||||
pendingFetches.set(code, deferred);
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
renderHarness();
|
||||
await flushEffects();
|
||||
|
||||
pendingDefaults.resolve(createFetchResponse(createRemoteDefaults()));
|
||||
await flushEffects();
|
||||
|
||||
renderHarness('business-and-finance.bso');
|
||||
await flushEffects();
|
||||
|
||||
expect(latestSnapshot?.directoryDefaults.description).not.toBe('remote defaults');
|
||||
|
||||
pendingFetches.forEach((pendingFetch, code) => {
|
||||
pendingFetch.resolve(createFetchResponse(createRemoteDirectoryList(code)));
|
||||
});
|
||||
await flushEffects(8);
|
||||
|
||||
expect(latestSnapshot?.directoryDefaults.description).toBe('remote defaults');
|
||||
});
|
||||
|
||||
it('hydrates cached directory defaults with cached communities when GitHub refresh fails', async () => {
|
||||
const cachedDefaults = createRemoteDefaults(REMOTE_DIRECTORY_CODES, {
|
||||
mu: { title: '/mu/ - Cached Remote Music' },
|
||||
});
|
||||
const cachedData: DirectoriesData = {
|
||||
title: 'Cached directories',
|
||||
description: 'cached description',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
communities: [{ address: 'music-posting.bso', title: '/mu/ - Cached Music', directoryCode: 'mu', nsfw: false }],
|
||||
};
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(cachedData));
|
||||
localStorage.setItem(LOCALSTORAGE_DEFAULTS_KEY, JSON.stringify(cachedDefaults));
|
||||
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, String(Date.now()));
|
||||
fetchMock.mockRejectedValueOnce(new Error('network down'));
|
||||
|
||||
renderHarness('music-posting.bso');
|
||||
await flushEffects(8);
|
||||
|
||||
expect(latestSnapshot?.directories.map((community) => community.address)).toEqual(['music-posting.bso']);
|
||||
expect(latestSnapshot?.directoryDefaults.description).toBe('remote defaults');
|
||||
expect(latestSnapshot?.directoryDefaults.directories.mu.title).toBe('/mu/ - Cached Remote Music');
|
||||
expect(warnSpy.mock.calls.some((call: ConsoleWarnCall) => String(call[0]).includes('Failed to fetch directories'))).toBe(true);
|
||||
});
|
||||
|
||||
it('clears invalid recent cache entries and falls back to vendored data when GitHub refresh fails', async () => {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify({ title: 'broken cache' }));
|
||||
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, String(Date.now()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import directoryListsData from '../data/5chan-directory-lists.json';
|
||||
import { vendoredDirectoryLists as directoryListsData, vendoredDirectoryDefaults as directoryDefaultsData } from '../data/vendored-directory-lists';
|
||||
import {
|
||||
directoryListToCommunity,
|
||||
isRecord,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} 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, DirectoryDefaultsData } from '../lib/utils/directory-list-utils';
|
||||
export { normalizeBoardAddress };
|
||||
|
||||
interface DirectoriesMetadata {
|
||||
@@ -34,6 +34,7 @@ interface DirectoriesState {
|
||||
const GITHUB_URL_TEMPLATE = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories/5chan-{code}-directory.json';
|
||||
const GITHUB_DEFAULTS_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories/5chan-directories-defaults.json';
|
||||
const LOCALSTORAGE_KEY = '5chan-directories-cache';
|
||||
const LOCALSTORAGE_DEFAULTS_KEY = '5chan-directory-defaults-cache';
|
||||
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
||||
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||
const FETCH_RETRY_DELAY_MS = 60 * 1000; // 1 minute
|
||||
@@ -44,6 +45,7 @@ let cacheMetadata: DirectoriesMetadata | null = null;
|
||||
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
||||
let lastSuccessfulGitHubFetchAt: number | null = null;
|
||||
let lastGitHubFetchAttemptAt: number | null = null;
|
||||
let cacheDefaults: DirectoryDefaultsData | null = null;
|
||||
// Exposed for deterministic unit tests around module-level cache state.
|
||||
export const __resetDirectoriesModuleStateForTests = () => {
|
||||
cacheCommunities = null;
|
||||
@@ -51,7 +53,9 @@ export const __resetDirectoriesModuleStateForTests = () => {
|
||||
inFlightGitHubFetch = null;
|
||||
lastSuccessfulGitHubFetchAt = null;
|
||||
lastGitHubFetchAttemptAt = null;
|
||||
cacheDefaults = null;
|
||||
fallbackDirectoriesData = null;
|
||||
fallbackDirectoryDefaults = null;
|
||||
};
|
||||
|
||||
const getDirectoryIdentifiers = (community: DirectoryCommunity): string[] => [
|
||||
@@ -212,6 +216,13 @@ const normalizeDirectoriesData = (value: unknown): DirectoriesData | null => {
|
||||
};
|
||||
|
||||
let fallbackDirectoriesData: DirectoriesData | null = null;
|
||||
let fallbackDirectoryDefaults: DirectoryDefaultsData | null = null;
|
||||
|
||||
export const getFallbackDirectoryDefaults = (): DirectoryDefaultsData => {
|
||||
if (fallbackDirectoryDefaults) return fallbackDirectoryDefaults;
|
||||
fallbackDirectoryDefaults = normalizeDirectoryDefaultsData(directoryDefaultsData as unknown);
|
||||
return fallbackDirectoryDefaults;
|
||||
};
|
||||
|
||||
export const getFallbackDirectoriesData = (): DirectoriesData => {
|
||||
if (fallbackDirectoriesData) return fallbackDirectoriesData;
|
||||
@@ -224,6 +235,26 @@ export const getFallbackDirectoriesData = (): DirectoriesData => {
|
||||
return fallbackDirectoriesData;
|
||||
};
|
||||
|
||||
const getDirectoryDefaultsFromLocalStorage = (): DirectoryDefaultsData | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem(LOCALSTORAGE_DEFAULTS_KEY);
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = normalizeDirectoryDefaultsData(JSON.parse(cached));
|
||||
if (Object.keys(normalized.directories).length > 0) {
|
||||
return normalized;
|
||||
}
|
||||
console.warn('Invalid directory defaults cache format, clearing stale cache');
|
||||
localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY);
|
||||
} catch (e) {
|
||||
console.warn('Failed to read directory defaults from localStorage:', e);
|
||||
localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFromLocalStorage = (): DirectoriesData | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||
@@ -234,10 +265,12 @@ const getFromLocalStorage = (): DirectoriesData | null => {
|
||||
const parsed = JSON.parse(cached);
|
||||
const normalized = normalizeDirectoriesData(parsed);
|
||||
if (normalized) {
|
||||
cacheDefaults ??= getDirectoryDefaultsFromLocalStorage();
|
||||
return normalized;
|
||||
}
|
||||
console.warn('Invalid directories cache format, clearing stale cache');
|
||||
localStorage.removeItem(LOCALSTORAGE_KEY);
|
||||
localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY);
|
||||
localStorage.removeItem(LOCALSTORAGE_TIMESTAMP_KEY);
|
||||
}
|
||||
}
|
||||
@@ -247,9 +280,12 @@ const getFromLocalStorage = (): DirectoriesData | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const saveToLocalStorage = (data: DirectoriesData) => {
|
||||
const saveToLocalStorage = (data: DirectoriesData, defaults?: DirectoryDefaultsData) => {
|
||||
try {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(data));
|
||||
if (defaults) {
|
||||
localStorage.setItem(LOCALSTORAGE_DEFAULTS_KEY, JSON.stringify(defaults));
|
||||
}
|
||||
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, Date.now().toString());
|
||||
} catch (e) {
|
||||
console.warn('Failed to save to localStorage:', e);
|
||||
@@ -356,8 +392,9 @@ const fetchDirectoriesFromGitHub = async (): Promise<DirectoriesData> => {
|
||||
throw new Error('Invalid directories payload');
|
||||
}
|
||||
hydrateModuleCaches(data);
|
||||
cacheDefaults = defaults;
|
||||
lastSuccessfulGitHubFetchAt = Date.now();
|
||||
saveToLocalStorage(data);
|
||||
saveToLocalStorage(data, defaults);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -463,6 +500,12 @@ export const useDirectories = () => {
|
||||
return cacheCommunities || state.communities || getFallbackDirectoriesData().communities;
|
||||
};
|
||||
|
||||
export const useDirectoryDefaults = (): DirectoryDefaultsData => {
|
||||
// Subscribe to the shared directory refresh so defaults update only when the matching directory payload commits.
|
||||
useDirectories();
|
||||
return cacheDefaults ?? getFallbackDirectoryDefaults();
|
||||
};
|
||||
|
||||
export const useDirectoriesState = () => {
|
||||
// Use vendored data as fallback to prevent theme flash on first load
|
||||
const [state, setState] = useState<DirectoriesState>({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import directoryListsData from '../../data/5chan-directory-lists.json';
|
||||
import { vendoredDirectoryLists as directoryListsData } from '../../data/vendored-directory-lists';
|
||||
import { normalizeDirectoryList, type DirectoryList, type DirectoryListBoard } from './directory-list-utils';
|
||||
|
||||
const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Rules from '../rules';
|
||||
|
||||
@@ -13,8 +14,15 @@ const testState = vi.hoisted(() => ({
|
||||
directories: [
|
||||
{ address: 'anime-posting.eth', title: '/a/ - Anime & Manga' },
|
||||
{ address: 'random-posting.eth', title: '/b/ - Random' },
|
||||
] as Array<{ address: string; title?: string }>,
|
||||
navigateMock: vi.fn(),
|
||||
{ address: 'flash-posting.eth', title: '/f/ - Flash' },
|
||||
] as Array<{ address: string; title?: string; directoryCode?: string }>,
|
||||
directoryDefaults: {
|
||||
directories: {
|
||||
a: { directoryCode: 'a', title: '/a/ - Anime & Manga', rules: ['All anime discussion welcome.'] },
|
||||
b: { directoryCode: 'b', title: '/b/ - Random', rules: ['Be excellent to each other.'] },
|
||||
f: { directoryCode: 'f', title: '/f/ - Flash', features: { postFlairs: true }, rules: ['Tag your uploads.'] },
|
||||
},
|
||||
} as { directories: Record<string, { directoryCode?: string; title?: string; rules?: string[]; features?: Record<string, unknown> }> },
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -27,7 +35,6 @@ vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => testState.navigateMock,
|
||||
useParams: () => ({
|
||||
boardIdentifier: testState.boardIdentifier,
|
||||
}),
|
||||
@@ -49,6 +56,7 @@ vi.mock('../../../hooks/use-directories', async () => {
|
||||
return {
|
||||
...actual,
|
||||
useDirectories: () => testState.directories,
|
||||
useDirectoryDefaults: () => testState.directoryDefaults,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -71,10 +79,31 @@ vi.mock('lodash/debounce', () => ({
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let scrollIntoViewMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
const renderRules = async () => {
|
||||
await act(async () => {
|
||||
root.render(createElement(Rules));
|
||||
root.render(createElement(MemoryRouter, null, createElement(Rules)));
|
||||
});
|
||||
};
|
||||
|
||||
// Set a controlled input's value via the native setter so React's value tracker still fires onChange.
|
||||
const setInputValue = (input: HTMLInputElement, value: string) => {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
setter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
};
|
||||
|
||||
const submitBoardAddress = async (address: string) => {
|
||||
const input = container.querySelector('input[type="text"]') as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
const form = input.closest('form') as HTMLFormElement;
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(input, address);
|
||||
});
|
||||
await act(async () => {
|
||||
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -86,8 +115,18 @@ describe('Rules', () => {
|
||||
testState.directories = [
|
||||
{ address: 'anime-posting.eth', title: '/a/ - Anime & Manga' },
|
||||
{ address: 'random-posting.eth', title: '/b/ - Random' },
|
||||
{ address: 'flash-posting.eth', title: '/f/ - Flash' },
|
||||
];
|
||||
testState.directoryDefaults = {
|
||||
directories: {
|
||||
a: { directoryCode: 'a', title: '/a/ - Anime & Manga', rules: ['All anime discussion welcome.'] },
|
||||
b: { directoryCode: 'b', title: '/b/ - Random', rules: ['Be excellent to each other.'] },
|
||||
f: { directoryCode: 'f', title: '/f/ - Flash', features: { postFlairs: true }, rules: ['Tag your uploads.'] },
|
||||
},
|
||||
};
|
||||
window.scrollTo = vi.fn();
|
||||
scrollIntoViewMock = vi.fn();
|
||||
Element.prototype.scrollIntoView = scrollIntoViewMock as unknown as typeof Element.prototype.scrollIntoView;
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
@@ -99,54 +138,140 @@ describe('Rules', () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('keeps custom-address routes out of the default board select', async () => {
|
||||
testState.boardIdentifier = 'custom-board.eth';
|
||||
it('renders a quick-jump nav link and a rules section for every directory', async () => {
|
||||
await renderRules();
|
||||
|
||||
// Quick-jump nav links use the directory name (like 4chan's board list) and point at the per-directory route.
|
||||
expect(container.querySelector('a[href="/rules/a"]')?.textContent).toBe('Anime & Manga');
|
||||
expect(container.querySelector('a[href="/rules/b"]')?.textContent).toBe('Random');
|
||||
|
||||
// One anchored rules section per directory.
|
||||
expect(container.querySelector('#a')).toBeTruthy();
|
||||
expect(container.querySelector('#b')).toBeTruthy();
|
||||
expect(container.textContent).toContain('/a/ - Anime & Manga');
|
||||
expect(container.textContent).toContain('/b/ - Random');
|
||||
});
|
||||
|
||||
it('renders directory rules from the directories JSON without loading any board over P2P', async () => {
|
||||
// communities (the P2P source) is empty, yet the rules still render because they come from the defaults JSON.
|
||||
await renderRules();
|
||||
|
||||
expect(container.textContent).toContain('All anime discussion welcome.');
|
||||
expect(container.textContent).toContain('Be excellent to each other.');
|
||||
// The directory rules are not framed as a P2P "Rules for:" board fetch.
|
||||
expect(container.textContent).not.toContain('Rules for:');
|
||||
});
|
||||
|
||||
it('insta-scrolls to a directory section when deep-linked via /rules/:code', async () => {
|
||||
testState.boardIdentifier = 'a';
|
||||
|
||||
await renderRules();
|
||||
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads a board over P2P when an address is submitted in the loader', async () => {
|
||||
testState.communities = {
|
||||
'custom-board.eth': {
|
||||
rules: ['No custom options in the select.'],
|
||||
rules: ['No spamming.'],
|
||||
shortAddress: 'custom-board.eth',
|
||||
state: 'succeeded',
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules();
|
||||
await submitBoardAddress('custom-board.eth');
|
||||
|
||||
const select = container.querySelector('select');
|
||||
expect(select).toBeTruthy();
|
||||
expect(select?.value).toBe('');
|
||||
expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']);
|
||||
expect(container.textContent).toContain('Rules for: custom-board.eth');
|
||||
expect(container.textContent).toContain('No spamming.');
|
||||
});
|
||||
|
||||
it('keeps the canonical default board selected for known directories', async () => {
|
||||
testState.boardIdentifier = 'a';
|
||||
it('clears a loaded P2P rules box when navigating to a directory route', async () => {
|
||||
testState.communities = {
|
||||
'anime-posting.eth': {
|
||||
rules: ['Stay on topic.'],
|
||||
'custom-board.eth': {
|
||||
rules: ['No spamming.'],
|
||||
shortAddress: 'custom-board.eth',
|
||||
state: 'succeeded',
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules();
|
||||
await submitBoardAddress('custom-board.eth');
|
||||
expect(container.textContent).toContain('Rules for: custom-board.eth');
|
||||
|
||||
const select = container.querySelector('select');
|
||||
expect(select).toBeTruthy();
|
||||
expect(select?.value).toBe('anime-posting.eth');
|
||||
expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']);
|
||||
expect(container.textContent).toContain('Rules for: /a/ - Anime & Manga');
|
||||
testState.boardIdentifier = 'a';
|
||||
await renderRules();
|
||||
|
||||
expect(container.textContent).not.toContain('Rules for: custom-board.eth');
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a friendly loading state string while board rules are downloading', async () => {
|
||||
testState.boardIdentifier = 'a';
|
||||
it('shows a friendly loading state string while a board over P2P is downloading', async () => {
|
||||
testState.communities = {
|
||||
'anime-posting.eth': {
|
||||
'custom-board.eth': {
|
||||
state: 'fetching-ipns',
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules();
|
||||
await submitBoardAddress('custom-board.eth');
|
||||
|
||||
expect(container.textContent).toContain('Downloading board from peers');
|
||||
expect(container.textContent).not.toContain('loading...');
|
||||
});
|
||||
|
||||
it('groups directories into Image Boards and Upload Boards with an h3 per directory', async () => {
|
||||
await renderRules();
|
||||
|
||||
expect(container.textContent).toContain('Image Boards');
|
||||
expect(container.textContent).toContain('Upload Boards');
|
||||
|
||||
const h3Titles = Array.from(container.querySelectorAll('h3')).map((h3) => h3.textContent);
|
||||
expect(h3Titles).toContain('/a/ - Anime & Manga');
|
||||
expect(h3Titles).toContain('/b/ - Random');
|
||||
expect(h3Titles).toContain('/f/ - Flash');
|
||||
expect(container.textContent).toContain('Tag your uploads.');
|
||||
});
|
||||
|
||||
it('does not scroll bare /rules back to the top when directories refresh', async () => {
|
||||
await renderRules();
|
||||
expect(window.scrollTo).toHaveBeenCalled();
|
||||
|
||||
vi.mocked(window.scrollTo).mockClear();
|
||||
testState.directories = [...testState.directories, { address: 'travel-posting.eth', title: '/trv/ - Travel', directoryCode: 'trv' }];
|
||||
testState.directoryDefaults = {
|
||||
directories: {
|
||||
...testState.directoryDefaults.directories,
|
||||
trv: { directoryCode: 'trv', title: '/trv/ - Travel', rules: ['Stay on topic.'] },
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules();
|
||||
|
||||
expect(window.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toggles the loader action to Clear, which removes the loaded rules and empties the input', async () => {
|
||||
testState.communities = {
|
||||
'custom-board.eth': {
|
||||
rules: ['No spamming.'],
|
||||
shortAddress: 'custom-board.eth',
|
||||
state: 'succeeded',
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules();
|
||||
await submitBoardAddress('custom-board.eth');
|
||||
expect(container.textContent).toContain('Rules for: custom-board.eth');
|
||||
|
||||
const clearButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Clear');
|
||||
expect(clearButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
clearButton?.click();
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain('Rules for: custom-board.eth');
|
||||
expect((container.querySelector('input[type="text"]') as HTMLInputElement).value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,22 +66,6 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.boardSelect {
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.orSeparator {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.customAddressForm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -103,13 +87,103 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Match homepage search "Go" button (home.module.css .searchButton). */
|
||||
.goButton {
|
||||
padding: 5px 10px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
flex: 0 0 29.9%;
|
||||
width: 29.9%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Green sidebar (4chan .left-box) — rules.2.css + global .boxcontent.
|
||||
* Same 93% font, 1.5em line-height, and .5em padding as other boxes (.boxContent).
|
||||
*/
|
||||
.selectorBox .boxContent > .directoryNav > ul {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
.selectorBox .directoryNav ul {
|
||||
margin: 1em;
|
||||
padding-left: 0;
|
||||
list-style: disc outside;
|
||||
}
|
||||
|
||||
/* 4chan: div ul { margin-left: 2em } — top list under boxcontent */
|
||||
.selectorBox .directoryNav > ul {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
/* 4chan: li ul { margin-left: 1em; margin-top: 0 } */
|
||||
.selectorBox .directoryNav li ul {
|
||||
margin-left: 1em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 4chan: li li ul { margin-bottom: .5em } */
|
||||
.selectorBox .directoryNav li li ul {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.selectorBox .directoryNav li {
|
||||
margin: 0;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.selectorBox .directoryNav a {
|
||||
color: #00e;
|
||||
text-decoration: underline;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.selectorBox .directoryNavHeader {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
color: #00e;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.directoryTitle {
|
||||
font-size: 100%;
|
||||
font-weight: 700;
|
||||
color: #006;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.directoryDivider {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
/* Match the rules box border color (#006, the rulesBox currentColor), like 4chan's <hr>. */
|
||||
background: #006;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.rulesBox {
|
||||
background: #eff;
|
||||
color: #006;
|
||||
@@ -120,14 +194,15 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 4chan's .right-box ol overrides only top + left of the base 1em margin (bottom/right stay 1em). */
|
||||
.box ol {
|
||||
margin: 1em;
|
||||
margin-left: 2em;
|
||||
margin: 0.5em 1em 1em 2.5em;
|
||||
}
|
||||
|
||||
/* No per-item spacing: line height drives the gap. 1.5 matches 4chan's rule list exactly. */
|
||||
.box ol li {
|
||||
list-style: decimal outside;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -142,28 +217,56 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.boardSelect,
|
||||
.addressInput {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.orSeparator {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Keep the input and button on one row on mobile instead of stacking them. */
|
||||
.customAddressForm {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.goButton {
|
||||
box-sizing: border-box;
|
||||
margin-top: 5px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mobile: tighter list margins + left inset (font size stays same as desktop .boxContent) */
|
||||
.leftColumn .selectorBox .boxContent {
|
||||
line-height: 130%;
|
||||
padding: 0.25em 0.5em 0 10px;
|
||||
}
|
||||
|
||||
.leftColumn .selectorBox .directoryNav ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2em;
|
||||
list-style: disc outside;
|
||||
}
|
||||
|
||||
.leftColumn .selectorBox .directoryNav > ul {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.leftColumn .selectorBox .directoryNav li ul {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.leftColumn .selectorBox .directoryNav li {
|
||||
padding: 5px 10px 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+191
-75
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState, FormEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Fragment, useEffect, useRef, useState, FormEvent } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useCommunity } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { Footer, HomeLogo } from '../home';
|
||||
import { useDirectories, DirectoryCommunity, findDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useDirectories, useDirectoryDefaults, DirectoryCommunity, DirectoryDefaultsData } from '../../hooks/use-directories';
|
||||
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
||||
import { getCommunityAddress, getBoardPath } from '../../lib/utils/route-utils';
|
||||
import { getCommunityAddress, getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import Markdown from '../../components/markdown';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import useStateString from '../../hooks/use-state-string';
|
||||
@@ -12,6 +12,12 @@ import styles from './rules.module.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import lowerCase from 'lodash/lowerCase';
|
||||
|
||||
interface CategoryGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
communities: DirectoryCommunity[];
|
||||
}
|
||||
|
||||
const getBoardShortCode = (title?: string): string => {
|
||||
if (!title) return '';
|
||||
const match = title.match(/^\/([^/]+)\//);
|
||||
@@ -24,31 +30,128 @@ const getBoardName = (title?: string): string => {
|
||||
return match ? match[1] : title;
|
||||
};
|
||||
|
||||
const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress: string; directories: DirectoryCommunity[] }) => {
|
||||
const getDirectoryCode = (community: DirectoryCommunity): string => community.directoryCode ?? getBoardShortCode(community.title);
|
||||
|
||||
const getDirectoryDisplayTitle = (community: DirectoryCommunity): string => {
|
||||
const shortCode = getDirectoryCode(community);
|
||||
const boardName = getBoardName(community.title);
|
||||
if (shortCode && boardName) {
|
||||
return `/${shortCode}/ - ${boardName}`;
|
||||
}
|
||||
return community.title ?? community.address;
|
||||
};
|
||||
|
||||
const getDirectoryRules = (defaults: DirectoryDefaultsData, code: string): string[] => (code ? (defaults.directories[code]?.rules ?? []) : []);
|
||||
|
||||
// Upload boards (e.g. /f/ - Flash) require post flairs/tagging on uploads; everything else is an image board, mirroring 4chan's split.
|
||||
const isUploadDirectory = (defaults: DirectoryDefaultsData, code: string): boolean => !!defaults.directories[code]?.features?.postFlairs;
|
||||
|
||||
// Split the directories into ordered category groups (image boards first, then upload boards), dropping empty ones.
|
||||
const groupDirectoriesByCategory = (directories: DirectoryCommunity[], defaults: DirectoryDefaultsData): CategoryGroup[] =>
|
||||
[
|
||||
{ key: 'image', label: 'Image Boards' },
|
||||
{ key: 'upload', label: 'Upload Boards' },
|
||||
]
|
||||
.map(({ key, label }) => ({
|
||||
key,
|
||||
label,
|
||||
communities: directories.filter((community) => (key === 'upload') === isUploadDirectory(defaults, getDirectoryCode(community))),
|
||||
}))
|
||||
.filter((group) => group.communities.length > 0);
|
||||
|
||||
// Resolve a /rules/:boardIdentifier segment (directory code or board address) to a directory code.
|
||||
const resolveDirectoryCode = (identifier: string, directories: DirectoryCommunity[]): string | null => {
|
||||
if (isDirectoryRoute(identifier, directories)) {
|
||||
return identifier;
|
||||
}
|
||||
const code = getBoardPath(getCommunityAddress(identifier, directories), directories);
|
||||
return isDirectoryRoute(code, directories) ? code : null;
|
||||
};
|
||||
|
||||
// A single directory's rules (h3 title + ordered rules), anchored by code for deep-link scrolling.
|
||||
const DirectorySection = ({ community, rules }: { community: DirectoryCommunity; rules: string[] }) => {
|
||||
const code = getDirectoryCode(community);
|
||||
return (
|
||||
<div id={code || undefined}>
|
||||
<h3 className={styles.directoryTitle}>{getDirectoryDisplayTitle(community)}</h3>
|
||||
{rules.length > 0 ? (
|
||||
<ol>
|
||||
{rules.map((rule, index) => (
|
||||
<li key={`${index}-${rule}`}>
|
||||
<Markdown content={rule} parseSpoilers={false} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p>
|
||||
<em>This directory has no specific rules.</em>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Directory rules come straight from the directories JSON (defaults), so a whole category renders at once without a P2P fetch.
|
||||
const CategoryRulesBox = ({ group, defaults }: { group: CategoryGroup; defaults: DirectoryDefaultsData }) => (
|
||||
<div className={`${styles.box} ${styles.rulesBox}`} id={`category-${group.key}`}>
|
||||
<div className={styles.boxBar}>
|
||||
<h2 className={styles.rulesBoxTitle}>{group.label}</h2>
|
||||
</div>
|
||||
<div className={styles.boxContent}>
|
||||
{group.communities.map((community, index) => (
|
||||
<Fragment key={community.address}>
|
||||
<DirectorySection community={community} rules={getDirectoryRules(defaults, getDirectoryCode(community))} />
|
||||
{/* Separator below each entry except the last (matches 4chan's <hr> between board rules). */}
|
||||
{index < group.communities.length - 1 && <hr className={styles.directoryDivider} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Quick-jump nav (left column) grouped by category; clicking a directory insta-scrolls to its rules via /rules/:code.
|
||||
const DirectoryNav = ({ groups }: { groups: CategoryGroup[] }) => (
|
||||
<div className={`${styles.box} ${styles.selectorBox}`}>
|
||||
<div className={styles.boxBar}>
|
||||
<h2 className={styles.selectorBoxTitle}>Directories</h2>
|
||||
</div>
|
||||
<div className={styles.boxContent}>
|
||||
<nav className={styles.directoryNav}>
|
||||
<ul>
|
||||
{groups.map((group) => (
|
||||
<li key={group.key}>
|
||||
{/* Button (not an anchor) so the HashRouter route hash is preserved while still scrolling to the category. */}
|
||||
<button type='button' className={styles.directoryNavHeader} onClick={() => document.getElementById(`category-${group.key}`)?.scrollIntoView()}>
|
||||
{group.label}
|
||||
</button>
|
||||
<ul>
|
||||
{group.communities.map((community) => {
|
||||
const code = getDirectoryCode(community);
|
||||
return (
|
||||
<li key={community.address}>
|
||||
<Link to={`/rules/${code}`}>{getBoardName(community.title) || getDirectoryDisplayTitle(community)}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// P2P board rules: fetched live from peers for an arbitrary board address.
|
||||
const BoardRulesDisplay = ({ communityAddress }: { communityAddress: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||
const { rules, state, title, shortAddress } = community || {};
|
||||
const { rules, state, shortAddress } = community || {};
|
||||
const stateString = useStateString(community) || t('downloading_board');
|
||||
const isLoaded = state === 'succeeded';
|
||||
|
||||
const defaultSub = directories.find((sub) => sub.address === communityAddress);
|
||||
let displayTitle: string;
|
||||
if (defaultSub?.title) {
|
||||
const shortCode = getBoardShortCode(defaultSub.title);
|
||||
const boardName = getBoardName(defaultSub.title);
|
||||
displayTitle = `Rules for: /${shortCode}/ - ${boardName}`;
|
||||
} else if (title) {
|
||||
const shortCode = getBoardShortCode(title);
|
||||
const boardName = getBoardName(title);
|
||||
if (shortCode && boardName && boardName !== title) {
|
||||
displayTitle = `Rules for: /${shortCode}/ - ${boardName}`;
|
||||
} else {
|
||||
displayTitle = `Rules for: ${shortAddress || communityAddress}`;
|
||||
}
|
||||
} else {
|
||||
displayTitle = `Rules for: ${shortAddress || communityAddress}`;
|
||||
}
|
||||
const displayTitle = `Rules for: ${shortAddress || communityAddress}`;
|
||||
|
||||
return (
|
||||
<div className={`${styles.box} ${styles.rulesBox}`}>
|
||||
@@ -64,7 +167,7 @@ const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress
|
||||
<ol>
|
||||
{rules.map((rule: string, index: number) => (
|
||||
<li key={`${index}-${rule}`}>
|
||||
<Markdown content={rule} />
|
||||
<Markdown content={rule} parseSpoilers={false} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@@ -78,60 +181,33 @@ const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress
|
||||
);
|
||||
};
|
||||
|
||||
const BoardSelector = ({
|
||||
directories,
|
||||
selectedAddress,
|
||||
onSelect,
|
||||
}: {
|
||||
directories: DirectoryCommunity[];
|
||||
selectedAddress: string;
|
||||
onSelect: (address: string) => void;
|
||||
}) => {
|
||||
// Load any board's own rules live from peers (P2P), separate from the static directory rules below.
|
||||
// Once a board is loaded the action toggles to "Clear", which drops the result box and empties the input.
|
||||
const LoadBoardRules = ({ onLoad, onClear, isLoaded }: { onLoad: (address: string) => void; onClear: () => void; isLoaded: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const [customAddress, setCustomAddress] = useState('');
|
||||
const selectedDefaultBoard = findDirectoryByAddress(directories, selectedAddress);
|
||||
const selectedBoardValue = selectedDefaultBoard?.address ?? '';
|
||||
|
||||
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
onSelect(value);
|
||||
setCustomAddress('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomSubmit = (e: FormEvent) => {
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmed = customAddress.trim();
|
||||
if (trimmed) {
|
||||
onSelect(trimmed);
|
||||
onLoad(trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleClear = () => {
|
||||
setCustomAddress('');
|
||||
onClear();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.box} ${styles.selectorBox}`}>
|
||||
<div className={styles.boxBar}>
|
||||
<h2 className={styles.selectorBoxTitle}>Load rules from a board</h2>
|
||||
<h2 className={styles.selectorBoxTitle}>Load rules P2P from any board</h2>
|
||||
</div>
|
||||
<div className={styles.boxContent}>
|
||||
<div className={styles.selectorRow}>
|
||||
<select value={selectedBoardValue} onChange={handleSelectChange} className={styles.boardSelect}>
|
||||
<option value=''>Select board…</option>
|
||||
{directories
|
||||
.toSorted((a, b) => getBoardShortCode(a.title).localeCompare(getBoardShortCode(b.title)))
|
||||
.map((sub) => {
|
||||
const shortCode = getBoardShortCode(sub.title);
|
||||
const boardName = getBoardName(sub.title);
|
||||
return (
|
||||
<option key={sub.address} value={sub.address}>
|
||||
/{shortCode}/ - {boardName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className={styles.orSeparator}>or</span>
|
||||
<form onSubmit={handleCustomSubmit} className={styles.customAddressForm}>
|
||||
<form onSubmit={handleSubmit} className={styles.customAddressForm}>
|
||||
<input
|
||||
type='text'
|
||||
aria-label={lowerCase(t('enter_board_address'))}
|
||||
@@ -140,9 +216,15 @@ const BoardSelector = ({
|
||||
onChange={(e) => setCustomAddress(e.target.value)}
|
||||
className={styles.addressInput}
|
||||
/>
|
||||
<button type='submit' className={styles.goButton}>
|
||||
Open Board
|
||||
</button>
|
||||
{isLoaded ? (
|
||||
<button type='button' className={styles.goButton} onClick={handleClear}>
|
||||
Clear
|
||||
</button>
|
||||
) : (
|
||||
<button type='submit' className={styles.goButton}>
|
||||
Load
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,21 +234,44 @@ const BoardSelector = ({
|
||||
|
||||
const Rules = () => {
|
||||
const { boardIdentifier } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const directories = useDirectories();
|
||||
const directoryDefaults = useDirectoryDefaults();
|
||||
const [loadedAddress, setLoadedAddress] = useState('');
|
||||
const scrolledForRef = useRef<string | null>(null);
|
||||
|
||||
const selectedAddress = boardIdentifier ? getCommunityAddress(boardIdentifier, directories) : '';
|
||||
// Order directories alphabetically by directory code (e.g. /3/, /a/, /aco/...), like 4chan, not by title.
|
||||
const directoriesWithCode = directories.filter((community) => getDirectoryCode(community)).toSorted((a, b) => getDirectoryCode(a).localeCompare(getDirectoryCode(b)));
|
||||
const categoryGroups = groupDirectoriesByCategory(directoriesWithCode, directoryDefaults);
|
||||
|
||||
const handleBoardSelect = (address: string) => {
|
||||
const path = getBoardPath(address, directories);
|
||||
navigate(`/rules/${path}`, { replace: true });
|
||||
const handleLoad = (address: string) => {
|
||||
setLoadedAddress(getCommunityAddress(address, directories));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
document.title = 'Rules - 5chan';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadedAddress('');
|
||||
if (!boardIdentifier) {
|
||||
scrolledForRef.current = null;
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [boardIdentifier]);
|
||||
|
||||
// Deep-link: /rules/:code insta-scrolls to that directory's rules once the matching section is rendered.
|
||||
useEffect(() => {
|
||||
if (!boardIdentifier || scrolledForRef.current === boardIdentifier) {
|
||||
return;
|
||||
}
|
||||
const code = resolveDirectoryCode(boardIdentifier, directories);
|
||||
const element = code ? document.getElementById(code) : null;
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
scrolledForRef.current = boardIdentifier;
|
||||
}
|
||||
}, [boardIdentifier, directories]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.content}>
|
||||
@@ -177,14 +282,25 @@ const Rules = () => {
|
||||
</div>
|
||||
<div className={styles.boxContent}>
|
||||
5chan does <i>not</i> have global rules or moderators. It is a serverless, adminless, static tool for browsing and posting to decentralized imageboards.{' '}
|
||||
<strong>Each board sets its own rules independently</strong>, determined by the board owner and board admins, and enforced by the board moderators.
|
||||
<strong>Each directory sets its own rules</strong>, listed below and expected of the boards that host it; individual board owners and admins may add their
|
||||
own.
|
||||
<br />
|
||||
<br />
|
||||
Please read and respect the rules of whatever board you decide to post to.
|
||||
</div>
|
||||
</div>
|
||||
<BoardSelector directories={directories} selectedAddress={selectedAddress} onSelect={handleBoardSelect} />
|
||||
{selectedAddress && <BoardRulesDisplay communityAddress={selectedAddress} directories={directories} />}
|
||||
<LoadBoardRules onLoad={handleLoad} onClear={() => setLoadedAddress('')} isLoaded={!!loadedAddress} />
|
||||
{loadedAddress && <BoardRulesDisplay communityAddress={loadedAddress} />}
|
||||
<div className={styles.columns}>
|
||||
<div className={styles.leftColumn}>
|
||||
<DirectoryNav groups={categoryGroups} />
|
||||
</div>
|
||||
<div className={styles.rightColumn}>
|
||||
{categoryGroups.map((group) => (
|
||||
<CategoryRulesBox key={group.key} group={group} defaults={directoryDefaults} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user