feat(flash board): add SWF posting support (#1145)

This commit is contained in:
Tommaso Casaburi
2026-05-30 16:07:41 +07:00
committed by GitHub
parent 56894700c1
commit 9b3a95dd95
69 changed files with 1372 additions and 63 deletions
@@ -73,11 +73,23 @@ vi.mock('react-i18next', async () => {
' before posting.',
);
}
if (i18nKey === 'post_form_flash_upload_prompt') {
return React.createElement(
React.Fragment,
{},
'Recommended SWF host: ',
components?.catbox ? React.cloneElement(components.catbox, {}, 'Catbox') : 'Catbox',
'. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link.',
);
}
return i18nKey;
},
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => (options?.domain ? `${key}:${options.domain}` : key),
t: (key: string, options?: Record<string, unknown>) => {
if (key === 'choose_one') return 'Choose one:';
return options?.domain ? `${key}:${options.domain}` : key;
},
}),
};
});
@@ -433,6 +445,12 @@ describe('PostForm', () => {
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
{ address: 'politically-incorrect.bso', directoryCode: 'pol', features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' },
{ address: 'random-nsfw.bso', features: {}, title: '/b/ - Random' },
{
address: 'flash-posting.bso',
directoryCode: 'f',
features: { postFlairs: true, requirePostFlairs: true, requirePostLink: true, requirePostLinkIsMedia: false },
title: '/f/ - Flash',
},
{ address: 'silly-stuff.bso', features: {}, title: '/s5s/ - Silly Stuff' },
{ address: 'traditional-games.bso', features: {}, title: '/tg/ - Traditional Games' },
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
@@ -704,6 +722,63 @@ describe('PostForm', () => {
});
});
it('shows /f/ upload guidance and publishes the selected flash tag as a post flair', async () => {
testState.resolvedCommunityAddress = 'flash-posting.bso';
await renderPostForm('/f');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const flashTagSelect = table?.querySelector<HTMLSelectElement>('select[name="flashTag"]');
const linkInput = Array.from(table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || []).find((input) => input.getAttribute('aria-label') === 'link');
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
const catboxLink = table?.querySelector<HTMLAnchorElement>('a[href="https://catbox.moe/"]');
expect(flashTagSelect).toBeTruthy();
expect(flashTagSelect?.value).toBe('');
expect(Array.from(flashTagSelect?.options || []).map((option) => option.textContent)).toEqual([
'Choose one:',
'Hentai',
'Porn',
'Japanese',
'Anime',
'Game',
'Loop',
'Other',
]);
expect(catboxLink?.textContent).toBe('Catbox');
expect(container.textContent).toContain('Recommended SWF host: Catbox');
await dispatchChange(flashTagSelect as HTMLSelectElement, 'loop');
await dispatchInput(linkInput as HTMLInputElement, 'https://files.catbox.moe/movie.swf');
await dispatchInput(textarea as HTMLTextAreaElement, 'flash thread');
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishPostMock).toHaveBeenCalledWith({
content: 'flash thread',
flairs: [{ text: 'flash:loop' }],
});
});
it('does not publish a flash flair until a tag is selected', async () => {
testState.resolvedCommunityAddress = 'flash-posting.bso';
await renderPostForm('/f');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const linkInput = Array.from(table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || []).find((input) => input.getAttribute('aria-label') === 'link');
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
await dispatchInput(linkInput as HTMLInputElement, 'https://files.catbox.moe/movie.swf');
await dispatchInput(textarea as HTMLTextAreaElement, 'flash thread');
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishPostMock).toHaveBeenCalledWith({
content: 'flash thread',
});
});
it('validates unsupported options and stores fortune output in post content', async () => {
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
testState.resolvedCommunityAddress = 'random-nsfw.bso';
+56 -1
View File
@@ -25,6 +25,7 @@ import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getBoardPath } from '../../lib/utils/route-utils';
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
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 { useCommunityField } from '../../hooks/use-stable-community';
@@ -51,6 +52,11 @@ import debounce from 'lodash/debounce';
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
const POST_FORM_FILE_DISPLAY_MAX_LENGTH = 28;
const mergeFlairs = (...flairGroups: Array<Comment['flairs'] | undefined>): Comment['flairs'] | undefined => {
const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : []));
return flairs.length > 0 ? flairs : undefined;
};
const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string): string => {
const raw = getPublishURLFilename(url) || uploadedFileName;
if (!raw) return noFileLabel;
@@ -130,6 +136,7 @@ interface PostFormFieldsProps {
subjectRef: React.Ref<HTMLInputElement>;
optionsRef: React.RefObject<HTMLInputElement>;
flagRef: React.RefObject<HTMLSelectElement>;
flashTagRef: React.RefObject<HTMLSelectElement>;
textRef: React.RefObject<HTMLTextAreaElement>;
urlRef: React.Ref<HTMLInputElement>;
url: string;
@@ -156,6 +163,9 @@ interface PostFormFieldsProps {
rulesPath: string;
requirePostLinkIsMedia: boolean;
flagOptions: CommentFlagSelectOption[];
flashTagOptions: FlashTagOption[];
showFlashTagSelector: boolean;
showFlashUploadPrompt: boolean;
showBbcodeToolbar: boolean;
onBbcodePreviewToggle: () => void;
onPublishReply: () => void;
@@ -177,6 +187,7 @@ const PostFormFields = ({
subjectRef,
optionsRef,
flagRef,
flashTagRef,
textRef,
urlRef,
url,
@@ -203,6 +214,9 @@ const PostFormFields = ({
rulesPath,
requirePostLinkIsMedia,
flagOptions,
flashTagOptions,
showFlashTagSelector,
showFlashUploadPrompt,
showBbcodeToolbar,
onBbcodePreviewToggle,
onPublishReply,
@@ -373,6 +387,21 @@ const PostFormFields = ({
</td>
</tr>
)}
{showFlashTagSelector && (
<tr>
<td>{t('tag')}</td>
<td>
<select name='flashTag' aria-label={t('tag')} ref={flashTagRef} defaultValue=''>
<option value=''>{t('choose_one')}</option>
{flashTagOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</td>
</tr>
)}
{showOekakiControls && (
<tr>
<td>Draw</td>
@@ -440,6 +469,16 @@ const PostFormFields = ({
}}
/>
</li>
{showFlashUploadPrompt && (
<li>
<Trans
i18nKey='post_form_flash_upload_prompt'
components={{
catbox: <a href='https://catbox.moe/' target='_blank' rel='noopener noreferrer' aria-label='Catbox' />,
}}
/>
</li>
)}
{showOekakiControls && isWebRuntime() ? <li>{OEKAKI_WEB_WARNING_TEXT}</li> : null}
</ul>
</td>
@@ -467,6 +506,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const subjectRef = useRef<HTMLInputElement>(null);
const optionsRef = useRef<HTMLInputElement>(null);
const flagRef = useRef<HTMLSelectElement>(null);
const flashTagRef = useRef<HTMLSelectElement>(null);
const fortuneEntryRef = useRef<FortuneEntry | null>(null);
const diceRollRef = useRef<DiceRoll | null>(null);
const nonokoRedirectPathRef = useRef<string | null>(null);
@@ -488,6 +528,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
const showFlashUploadPrompt = isFlashDirectoryCode(postOptionsDirectoryCode);
const showFlashTagSelector = showFlashUploadPrompt && !isInPostView;
const accountCommunityAddresses = useAccountCommunityAddresses();
const accountAddress = account?.author?.address;
@@ -536,6 +578,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
if (flagRef.current) {
flagRef.current.value = flagRef.current.options[0]?.value ?? '';
}
if (flashTagRef.current) {
flashTagRef.current.value = '';
}
checkContentLength.cancel();
checkPostOptions.cancel();
fortuneEntryRef.current = null;
@@ -597,9 +642,15 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
}
const flagPublishOptions = getCommentFlagPublishOptionsForDirectory(directoryEntry, flagRef.current?.value);
const flashTagPublishOptions = getFlashTagPublishOptionsForDirectoryCode(postOptionsDirectoryCode, flashTagRef.current?.value);
const flairs = mergeFlairs(flagPublishOptions.flairs, flashTagPublishOptions.flairs);
const publishOptions = {
...flagPublishOptions,
...(flairs ? { flairs } : {}),
};
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
publishPost({ content: publishContent, ...flagPublishOptions });
publishPost({ content: publishContent, ...publishOptions });
};
// redirect to pending page when pending comment is created
@@ -785,6 +836,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
subjectRef={subjectRef}
optionsRef={optionsRef}
flagRef={flagRef}
flashTagRef={flashTagRef}
textRef={textRef}
urlRef={urlRef}
url={url}
@@ -811,6 +863,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
rulesPath={rulesPath}
requirePostLinkIsMedia={requirePostLinkIsMedia}
flagOptions={flagOptions}
flashTagOptions={FLASH_TAG_OPTIONS}
showFlashTagSelector={showFlashTagSelector}
showFlashUploadPrompt={showFlashUploadPrompt}
showBbcodeToolbar={showBbcodeToolbar}
onBbcodePreviewToggle={handleBbcodePreviewToggle}
onPublishReply={onPublishReply}