mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(flash board): add SWF posting support (#1145)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user