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:
@@ -438,6 +438,63 @@ describe('ChallengeModal', () => {
|
||||
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('advances through multiple iframe challenges before publishing answers', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
createStoredChallenge(
|
||||
[
|
||||
{
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
{
|
||||
challenge: 'https://flags.5chan.app/iframe/session-flag',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
],
|
||||
publication,
|
||||
),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-123',
|
||||
},
|
||||
origin: 'https://spamblocker.bitsocial.net',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).not.toHaveBeenCalled();
|
||||
expect(testState.removeChallengeMock).not.toHaveBeenCalled();
|
||||
expect(container.textContent).toContain('mu wants to open flags.5chan.app.');
|
||||
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-flag',
|
||||
},
|
||||
origin: 'https://flags.5chan.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['', '']);
|
||||
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('ignores iframe completion messages with the wrong session id', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
|
||||
@@ -373,19 +373,41 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const completeCurrentChallenge = useCallback(
|
||||
(challengeAnswers: string[]) => {
|
||||
if (!publication) return;
|
||||
const resolvedAnswers = challengeAnswers.length ? challengeAnswers : [''];
|
||||
const updatedAnswers = [...answers];
|
||||
resolvedAnswers.forEach((answer, index) => {
|
||||
updatedAnswers[currentChallengeIndex + index] = answer;
|
||||
});
|
||||
|
||||
const nextChallengeIndex = currentChallengeIndex + resolvedAnswers.length;
|
||||
if (challenges?.[nextChallengeIndex]) {
|
||||
setAnswers(updatedAnswers);
|
||||
setCurrentChallengeIndex(nextChallengeIndex);
|
||||
setReadyIframeChallengeKey('');
|
||||
return;
|
||||
}
|
||||
|
||||
publication.publishChallengeAnswers(updatedAnswers);
|
||||
setAnswers([]);
|
||||
closeModal();
|
||||
},
|
||||
[answers, challenges, closeModal, currentChallengeIndex, publication],
|
||||
);
|
||||
|
||||
const onIframeDone = useCallback(() => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(['']);
|
||||
closeModal();
|
||||
}, [closeModal, publication]);
|
||||
completeCurrentChallenge(['']);
|
||||
}, [completeCurrentChallenge, publication]);
|
||||
|
||||
const onIframeAutoComplete = useCallback(
|
||||
(challengeAnswers: string[]) => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(challengeAnswers);
|
||||
closeModal();
|
||||
completeCurrentChallenge(challengeAnswers);
|
||||
},
|
||||
[closeModal, publication],
|
||||
[completeCurrentChallenge, publication],
|
||||
);
|
||||
|
||||
const onIframeReady = useCallback(() => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getCommentFlagFlairs, getAuthorFlagViewModels } from '../lib/comment-flags';
|
||||
import styles from '../views/post/post.module.css';
|
||||
|
||||
interface PostAuthorFlagsProps {
|
||||
author: unknown;
|
||||
comment: unknown;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const getBackgroundPosition = (x: number, y: number) => `${x === 0 ? 0 : -x}px ${y === 0 ? 0 : -y}px`;
|
||||
|
||||
const PostAuthorFlags = ({ author, comment, enabled }: PostAuthorFlagsProps) => {
|
||||
if (!enabled) return null;
|
||||
|
||||
const flags = getAuthorFlagViewModels(getCommentFlagFlairs(comment, author));
|
||||
if (flags.length === 0) return null;
|
||||
|
||||
return (
|
||||
<span className={styles.authorFlags}>
|
||||
{flags.map((flag) => (
|
||||
<span
|
||||
key={flag.key}
|
||||
aria-label={flag.label}
|
||||
className={styles.authorFlag}
|
||||
role='img'
|
||||
style={{
|
||||
backgroundImage: `url("${flag.spritePath}")`,
|
||||
backgroundPosition: getBackgroundPosition(flag.x, flag.y),
|
||||
width: flag.width,
|
||||
height: flag.height,
|
||||
}}
|
||||
title={flag.label}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostAuthorFlags;
|
||||
@@ -31,6 +31,7 @@ import EditMenu from '../edit-menu/edit-menu';
|
||||
import FailedPublishNotice from '../failed-publish-notice';
|
||||
import { canEmbed } from '../embed';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostAuthorFlags from '../post-author-flags';
|
||||
import PostMenuDesktop from './post-menu-desktop';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
@@ -239,7 +240,9 @@ const PostInfo = ({
|
||||
const isReply = parentCid;
|
||||
const { showOmittedReplies } = useShowOmittedReplies();
|
||||
const directories = useDirectories();
|
||||
const directoryEntry = communityAddress ? findDirectoryByAddress(directories, communityAddress) : undefined;
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
||||
const showAuthorFlags = directoryEntry?.features?.hasFlags === true && !(deleted || removed || purged);
|
||||
const postMenuProps = selectPostMenuProps(post);
|
||||
|
||||
const params = useParams();
|
||||
@@ -415,6 +418,7 @@ const PostInfo = ({
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<PostAuthorFlags author={author} comment={post} enabled={showAuthorFlags} />
|
||||
<span className={styles.dateTime}>
|
||||
{isInModQueueView && isOverThreshold ? (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -28,6 +28,7 @@ import CommentContent from '../comment-content';
|
||||
import CommentMedia, { MediaLoadFailureInfo } from '../comment-media';
|
||||
import FailedPublishNotice from '../failed-publish-notice';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostAuthorFlags from '../post-author-flags';
|
||||
import PostMenuMobile from './post-menu-mobile';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
@@ -87,6 +88,8 @@ const PostInfoAndMedia = ({
|
||||
const archived = isCommentArchived(resolvedPost);
|
||||
const purged = resolvedPost?.commentModeration?.purged;
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
||||
const directoryEntry = communityAddress ? findDirectoryByAddress(directories, communityAddress) : undefined;
|
||||
const showAuthorFlags = directoryEntry?.features?.hasFlags === true && !(deleted || removed || purged);
|
||||
const displayBoardPath =
|
||||
boardPath && communityAddress
|
||||
? boardPath !== communityAddress
|
||||
@@ -348,6 +351,7 @@ const PostInfoAndMedia = ({
|
||||
){' '}
|
||||
</>
|
||||
)}
|
||||
<PostAuthorFlags author={author} comment={resolvedPost} enabled={showAuthorFlags} />
|
||||
{pinned && (
|
||||
<span className={styles.stickyIconWrapper}>
|
||||
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
|
||||
|
||||
@@ -18,7 +18,7 @@ const testState = vi.hoisted(() => ({
|
||||
features: {},
|
||||
title: '/mu/ - Music',
|
||||
},
|
||||
} as Record<string, { address: string; features?: Record<string, unknown>; title?: string }>,
|
||||
} as Record<string, { address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
|
||||
handleUploadMock: vi.fn(),
|
||||
isMobile: false,
|
||||
isResolvingExternalQuotes: false,
|
||||
@@ -322,6 +322,12 @@ describe('ReplyModal', () => {
|
||||
features: {},
|
||||
title: '/mu/ - Music',
|
||||
},
|
||||
'politically-incorrect.bso': {
|
||||
address: 'politically-incorrect.bso',
|
||||
directoryCode: 'pol',
|
||||
features: { hasFlags: true },
|
||||
title: '/pol/ - Politically Incorrect',
|
||||
},
|
||||
'random-nsfw.bso': {
|
||||
address: 'random-nsfw.bso',
|
||||
features: {},
|
||||
@@ -424,6 +430,52 @@ describe('ReplyModal', () => {
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
||||
});
|
||||
|
||||
it('shows a flag selector on flag boards and publishes the default geographic request', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
const flagSelect = container.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
|
||||
expect(flagSelect).toBeTruthy();
|
||||
expect(flagSelect?.value).toBe('country:auto');
|
||||
expect(
|
||||
Array.from(flagSelect?.options || [])
|
||||
.slice(0, 3)
|
||||
.map((option) => option.textContent),
|
||||
).toEqual(['Geographic Location', 'Anarcho-Capitalist', 'Anarchist']);
|
||||
|
||||
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('publishes selected political flags from the reply modal', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
const flagSelect = container.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
await dispatchInput(container.querySelector<HTMLTextAreaElement>('textarea') as HTMLTextAreaElement, 'reply body');
|
||||
await act(async () => {
|
||||
if (flagSelect) {
|
||||
flagSelect.value = 'pol:AC';
|
||||
flagSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: 'reply body',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ type: 'pol', code: 'AC', text: 'flag:pol:AC' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the shared loading ellipsis while a reply upload is running', async () => {
|
||||
testState.isUploading = true;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
background-image: var(--close-button-background-image);
|
||||
}
|
||||
|
||||
.container input[type="text"], .container textarea {
|
||||
.container input[type="text"], .container textarea, .container select {
|
||||
border: var(--reply-modal-field-input-border, revert);
|
||||
font-family: var(--post-form-field-font-family, revert);
|
||||
appearance: var(--post-form-field-input-appearance, revert);
|
||||
@@ -53,7 +53,7 @@
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.container input[type="text"]:focus, .container textarea:focus {
|
||||
.container input[type="text"]:focus, .container textarea:focus, .container select:focus {
|
||||
border: var(--reply-modal-field-input-border-focus, revert);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.container input[type="text"], .container textarea {
|
||||
.container input[type="text"], .container textarea, .container select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsFromSelection } from '../../lib/comment-flag-selection';
|
||||
import {
|
||||
type DiceRoll,
|
||||
type FortuneEntry,
|
||||
@@ -61,6 +62,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||
usePublishReply({
|
||||
cid: parentCid,
|
||||
@@ -86,6 +88,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
});
|
||||
const urlRef = 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);
|
||||
@@ -163,9 +166,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
setError(null);
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? `/${postOptionsDirectoryCode || params.boardIdentifier || communityAddress}` : null;
|
||||
publishReply({ content: publishContent });
|
||||
publishReply({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -512,6 +517,17 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{flagOptions.length > 0 && (
|
||||
<div>
|
||||
<select key={flagOptions.map((option) => option.value).join('|')} aria-label={t('flag')} ref={flagRef} defaultValue={flagOptions[0]?.value}>
|
||||
{flagOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.link}>
|
||||
<input
|
||||
type='text'
|
||||
|
||||
Reference in New Issue
Block a user