fix(flags): resolve comment flags from directory list candidates

Use directory list board entries when the active address is not yet in
the directories cache so /pol and similar boards get correct flag UI.
This commit is contained in:
Tommaso Casaburi
2026-06-02 15:00:14 +07:00
parent ea7883b107
commit 4cc2bf4ea1
6 changed files with 232 additions and 4 deletions
@@ -22,6 +22,15 @@ const testState = vi.hoisted(() => ({
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
] as Array<{ address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
directoryListsByCode: {} as Record<
string,
{
boards: Array<{ address: string; features?: Record<string, unknown>; publicKey?: string }>;
directoryCode: string;
features?: Record<string, unknown>;
title?: string;
}
>,
editedComment: undefined as { commentModeration?: { archived?: boolean }; deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean } | undefined,
gifFrameStatus: 'idle' as 'idle' | 'ready',
handleUploadMock: vi.fn(),
@@ -132,6 +141,24 @@ vi.mock('../../../hooks/use-directories', () => ({
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
}));
vi.mock('../../../hooks/use-directory-entry', async () => {
const actual = await vi.importActual<typeof import('../../../hooks/use-directory-entry')>('../../../hooks/use-directory-entry');
return {
...actual,
useDirectoryEntry: (address: string | undefined, directoryCodeHint?: string) => {
const list =
testState.directoryListsByCode[directoryCodeHint ?? ''] ??
Object.values(testState.directoryListsByCode).find((candidate) => candidate.boards.some((board) => board.address === address));
return actual.getDirectoryEntryForAddress({
address,
directories: testState.directories,
directoryCodeHint,
list,
});
},
};
});
vi.mock('../../../hooks/use-community-identifiers', () => ({
useCommunityIdentifier: (address?: string) => (address ? { name: address } : undefined),
}));
@@ -456,6 +483,7 @@ describe('PostForm', () => {
{ address: 'traditional-games.bso', features: {}, title: '/tg/ - Traditional Games' },
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
];
testState.directoryListsByCode = {};
testState.editedComment = undefined;
testState.gifFrameStatus = 'idle';
testState.isOffline = false;
@@ -700,6 +728,37 @@ describe('PostForm', () => {
});
});
it('shows the /pol/ flag field when a non-primary directory candidate is hosting /pol/', async () => {
testState.resolvedCommunityAddress = 'nothing-is-beyond-our-reach.bso';
testState.directoryListsByCode.pol = {
directoryCode: 'pol',
features: { hasFlags: true },
title: '/pol/ - Politically Incorrect',
boards: [{ address: 'politically-incorrect.bso' }, { address: 'nothing-is-beyond-our-reach.bso' }],
};
await renderPostForm('/pol');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const flagSelect = table?.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
expect(flagSelect).toBeTruthy();
expect(flagSelect?.value).toBe('country:auto');
await dispatchInput(textarea as HTMLTextAreaElement, 'candidate board flag post');
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishPostMock).toHaveBeenCalledWith({
content: 'candidate board flag post',
challengeRequest: {
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
},
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
});
});
it('publishes geographic location on /sp/ without showing a flag field', async () => {
testState.resolvedCommunityAddress = 'sports-posting.bso';
+3 -2
View File
@@ -28,7 +28,8 @@ import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, is
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory, type CommentFlagSelectOption } from '../../lib/comment-flag-selection';
import { FLASH_TAG_OPTIONS, getFlashTagPublishOptionsForDirectoryCode, isFlashDirectoryCode, type FlashTagOption } from '../../lib/flash-tags';
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
import { useDirectories } from '../../hooks/use-directories';
import { useDirectoryEntry } from '../../hooks/use-directory-entry';
import { useCommunityField } from '../../hooks/use-stable-community';
import useIsMobile from '../../hooks/use-is-mobile';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
@@ -519,7 +520,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subscriptions = account?.subscriptions || [];
const directories = useDirectories();
const directoryEntry = useDirectoryByAddress(effectiveBoardAddress);
const directoryEntry = useDirectoryEntry(effectiveBoardAddress, params?.boardIdentifier);
const pendingPostBoardPath = effectiveBoardAddress ? getBoardPath(effectiveBoardAddress, directories) : undefined;
const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules';
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
@@ -20,6 +20,15 @@ const testState = vi.hoisted(() => ({
title: '/mu/ - Music',
},
} as Record<string, { address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
directoryListsByCode: {} as Record<
string,
{
boards: Array<{ address: string; features?: Record<string, unknown>; publicKey?: string }>;
directoryCode: string;
features?: Record<string, unknown>;
title?: string;
}
>,
handleUploadMock: vi.fn(),
uploadFileMock: vi.fn(),
isMobile: false,
@@ -150,6 +159,24 @@ vi.mock('../../../hooks/use-directories', () => ({
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
}));
vi.mock('../../../hooks/use-directory-entry', async () => {
const actual = await vi.importActual<typeof import('../../../hooks/use-directory-entry')>('../../../hooks/use-directory-entry');
return {
...actual,
useDirectoryEntry: (address: string | undefined, directoryCodeHint?: string) => {
const list =
testState.directoryListsByCode[directoryCodeHint ?? ''] ??
Object.values(testState.directoryListsByCode).find((candidate) => candidate.boards.some((board) => board.address === address));
return actual.getDirectoryEntryForAddress({
address,
directories: Object.values(testState.directoryByAddress),
directoryCodeHint,
list,
});
},
};
});
vi.mock('../../../hooks/use-community-identifiers', () => ({
useCommunityIdentifier: (address?: string) => (address ? { name: address } : undefined),
}));
@@ -368,6 +395,7 @@ describe('ReplyModal', () => {
title: '/tg/ - Traditional Games',
},
};
testState.directoryListsByCode = {};
testState.handleUploadMock.mockReset();
testState.uploadFileMock.mockReset();
testState.isMobile = false;
@@ -506,6 +534,32 @@ describe('ReplyModal', () => {
});
});
it('shows the /pol/ flag selector for replies on a non-primary directory candidate board', async () => {
testState.directoryListsByCode.pol = {
directoryCode: 'pol',
features: { hasFlags: true },
title: '/pol/ - Politically Incorrect',
boards: [{ address: 'politically-incorrect.bso' }, { address: 'nothing-is-beyond-our-reach.bso' }],
};
await renderReplyModal('/pol/thread/post-1', 'nothing-is-beyond-our-reach.bso');
const flagSelect = container.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
expect(flagSelect).toBeTruthy();
expect(flagSelect?.value).toBe('country:auto');
await clickButtonByText('post');
expect(testState.publishReplyMock).toHaveBeenCalledWith({
content: '>>42\nselected text',
challengeRequest: {
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
},
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
});
});
it.each([
{ boardPath: '/bant/thread/post-1', communityAddress: 'international-nsfw.bso' },
{ boardPath: '/int/thread/post-1', communityAddress: 'international-sfw.bso' },
+3 -2
View File
@@ -24,7 +24,8 @@ import useSelectedTextStore from '../../stores/use-selected-text-store';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
import useMediaHostingStore from '../../stores/use-media-hosting-store';
import { findDirectoryByAddress, useDirectories } from '../../hooks/use-directories';
import { useDirectories } from '../../hooks/use-directories';
import { useDirectoryEntry } from '../../hooks/use-directory-entry';
import usePublishReply from '../../hooks/use-publish-reply';
import useIsMobile from '../../hooks/use-is-mobile';
import { useFileUpload } from '../../hooks/use-file-upload';
@@ -63,7 +64,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const isInModView = isModView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const directories = useDirectories();
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
const directoryEntry = useDirectoryEntry(communityAddress, params?.boardIdentifier);
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i';