mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(flags): add comment flags (#1140)
* feat(flags): add comment flags * fix(flags): address review feedback * fix(flags): clear stale flag publish data
This commit is contained in:
@@ -20,7 +20,7 @@ const testState = vi.hoisted(() => ({
|
||||
directories: [
|
||||
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
|
||||
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
|
||||
] as Array<{ address: string; features?: Record<string, unknown>; title?: string }>,
|
||||
] as Array<{ address: 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(),
|
||||
@@ -426,6 +426,7 @@ describe('PostForm', () => {
|
||||
testState.comments = {};
|
||||
testState.directories = [
|
||||
{ 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: 'silly-stuff.bso', features: {}, title: '/s5s/ - Silly Stuff' },
|
||||
{ address: 'traditional-games.bso', features: {}, title: '/tg/ - Traditional Games' },
|
||||
@@ -606,6 +607,59 @@ describe('PostForm', () => {
|
||||
expect(testState.publishedPostOptions?.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shows a 4chan-style flag field on flag boards and publishes the default geographic request', async () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.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');
|
||||
expect(
|
||||
Array.from(flagSelect?.options || [])
|
||||
.slice(0, 4)
|
||||
.map((option) => option.textContent),
|
||||
).toEqual(['Geographic Location', 'Anarcho-Capitalist', 'Anarchist', 'Black Nationalist']);
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'flagged post');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledWith({
|
||||
content: 'flagged post',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes selected political flags from the post form', async () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.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');
|
||||
|
||||
await dispatchChange(flagSelect as HTMLSelectElement, 'pol:AC');
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'memeflag post');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledWith({
|
||||
content: 'memeflag post',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ type: 'pol', code: 'AC', text: 'flag:pol:AC' }],
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.postFormTable input[type="text"], .postFormTable textarea {
|
||||
.postFormTable input[type="text"], .postFormTable textarea, .postFormTable select {
|
||||
border: var(--post-form-field-input-border, revert);
|
||||
font-family: var(--post-form-field-font-family, revert);
|
||||
appearance: var(--post-form-field-input-appearance, revert);
|
||||
@@ -80,7 +80,7 @@
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.postFormTable input[type="text"]:focus, .postFormTable textarea:focus {
|
||||
.postFormTable input[type="text"]:focus, .postFormTable textarea:focus, .postFormTable select:focus {
|
||||
border: var(--post-form-field-input-focus-border, revert);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
width: 190px !important;
|
||||
}
|
||||
|
||||
.flagSelect {
|
||||
width: 254px;
|
||||
}
|
||||
|
||||
.spoilerButton input[type="checkbox"] {
|
||||
margin: 0 3px;
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/
|
||||
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, getCommentFlagPublishOptionsFromSelection, type CommentFlagSelectOption } from '../../lib/comment-flag-selection';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useCommunityField } from '../../hooks/use-stable-community';
|
||||
@@ -124,6 +125,7 @@ interface PostFormFieldsProps {
|
||||
postCid: string;
|
||||
subjectRef: React.Ref<HTMLInputElement>;
|
||||
optionsRef: React.RefObject<HTMLInputElement>;
|
||||
flagRef: React.RefObject<HTMLSelectElement>;
|
||||
textRef: React.RefObject<HTMLTextAreaElement>;
|
||||
urlRef: React.Ref<HTMLInputElement>;
|
||||
url: string;
|
||||
@@ -148,6 +150,7 @@ interface PostFormFieldsProps {
|
||||
communityAddress: string | undefined;
|
||||
rulesPath: string;
|
||||
requirePostLinkIsMedia: boolean;
|
||||
flagOptions: CommentFlagSelectOption[];
|
||||
showBbcodeToolbar: boolean;
|
||||
onBbcodePreviewToggle: () => void;
|
||||
onPublishReply: () => void;
|
||||
@@ -166,6 +169,7 @@ const PostFormFields = ({
|
||||
postCid,
|
||||
subjectRef,
|
||||
optionsRef,
|
||||
flagRef,
|
||||
textRef,
|
||||
urlRef,
|
||||
url,
|
||||
@@ -190,6 +194,7 @@ const PostFormFields = ({
|
||||
communityAddress,
|
||||
rulesPath,
|
||||
requirePostLinkIsMedia,
|
||||
flagOptions,
|
||||
showBbcodeToolbar,
|
||||
onBbcodePreviewToggle,
|
||||
onPublishReply,
|
||||
@@ -292,6 +297,26 @@ const PostFormFields = ({
|
||||
{lengthError && <div className={styles.error}>{lengthError}</div>}
|
||||
</td>
|
||||
</tr>
|
||||
{flagOptions.length > 0 && (
|
||||
<tr>
|
||||
<td>{t('flag')}</td>
|
||||
<td>
|
||||
<select
|
||||
key={flagOptions.map((option) => option.value).join('|')}
|
||||
aria-label={t('flag')}
|
||||
className={styles.flagSelect}
|
||||
ref={flagRef}
|
||||
defaultValue={flagOptions[0]?.value}
|
||||
>
|
||||
{flagOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>{requirePostLinkIsMedia ? t('link_to_file') : t('link')}</td>
|
||||
<td className={styles.linkField}>
|
||||
@@ -420,6 +445,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
const subjectRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLInputElement>(null);
|
||||
const flagRef = useRef<HTMLSelectElement>(null);
|
||||
const fortuneEntryRef = useRef<FortuneEntry | null>(null);
|
||||
const diceRollRef = useRef<DiceRoll | null>(null);
|
||||
const nonokoRedirectPathRef = useRef<string | null>(null);
|
||||
@@ -438,6 +464,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
|
||||
const accountCommunityAddresses = useAccountCommunityAddresses();
|
||||
const accountAddress = account?.author?.address;
|
||||
@@ -483,6 +510,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
if (optionsRef.current) {
|
||||
optionsRef.current.value = '';
|
||||
}
|
||||
if (flagRef.current) {
|
||||
flagRef.current.value = flagRef.current.options[0]?.value ?? '';
|
||||
}
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
fortuneEntryRef.current = null;
|
||||
@@ -542,8 +572,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishPost({ content: publishContent });
|
||||
publishPost({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
// redirect to pending page when pending comment is created
|
||||
@@ -652,8 +684,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishReply({ content: publishContent });
|
||||
publishReply({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -712,6 +746,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
postCid={postCid}
|
||||
subjectRef={subjectRef}
|
||||
optionsRef={optionsRef}
|
||||
flagRef={flagRef}
|
||||
textRef={textRef}
|
||||
urlRef={urlRef}
|
||||
url={url}
|
||||
@@ -736,6 +771,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
communityAddress={communityAddress}
|
||||
rulesPath={rulesPath}
|
||||
requirePostLinkIsMedia={requirePostLinkIsMedia}
|
||||
flagOptions={flagOptions}
|
||||
showBbcodeToolbar={showBbcodeToolbar}
|
||||
onBbcodePreviewToggle={handleBbcodePreviewToggle}
|
||||
onPublishReply={onPublishReply}
|
||||
|
||||
Reference in New Issue
Block a user