feat(post form): add rules and FAQ prompt

This commit is contained in:
Tommaso Casaburi
2026-05-21 18:55:36 +07:00
parent 5fc27fda21
commit 7305e40870
38 changed files with 171 additions and 42 deletions
@@ -53,11 +53,30 @@ const testState = vi.hoisted(() => ({
uploadedFileName: 'picked.png' as string | null,
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => (options?.domain ? `${key}:${options.domain}` : key),
}),
}));
vi.mock('react-i18next', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Trans: ({ i18nKey, components }: { components?: Record<string, React.ReactElement>; i18nKey: string }) => {
if (i18nKey === 'post_form_rules_faq_prompt') {
return React.createElement(
React.Fragment,
{},
'Please read the ',
components?.rules ? React.cloneElement(components.rules, {}, 'Rules') : 'Rules',
' and ',
components?.faq ? React.cloneElement(components.faq, {}, 'FAQ') : 'FAQ',
' before posting.',
);
}
return i18nKey;
},
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => (options?.domain ? `${key}:${options.domain}` : key),
}),
};
});
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
@@ -643,6 +662,27 @@ describe('PostForm', () => {
expect(table?.textContent).toContain('file name.jpg');
});
it('shows the rules and FAQ prompt below the file row with a board-specific rules link', async () => {
testState.resolvedCommunityAddress = 'music-posting.eth';
await renderPostForm('/mu');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const rows = Array.from(table?.querySelectorAll('tr') || []);
const fileRowIndex = rows.findIndex((row) => row.querySelector('td')?.textContent === 'file');
const promptRowIndex = rows.findIndex((row) => row.textContent === 'Please read the Rules and FAQ before posting.');
const promptRow = rows[promptRowIndex];
const links = Array.from(promptRow?.querySelectorAll('a') || []);
expect(fileRowIndex).toBeGreaterThan(-1);
expect(promptRowIndex).toBe(fileRowIndex + 1);
expect(promptRow?.className).toBe('rules');
expect(promptRow?.querySelector('ul')?.className).toBe('rules');
expect(links.map((link) => link.textContent)).toEqual(['Rules', 'FAQ']);
expect(links.map((link) => link.getAttribute('href'))).toEqual(['/rules/mu', '/faq']);
});
it('shortens long pasted file-link filenames next to the upload button', async () => {
testState.uploadedFileName = null;
@@ -182,6 +182,40 @@
font-weight: normal;
}
.postFormTable :global(ul.rules) {
margin: 0px;
padding: 0px;
margin-top: 5px;
}
.postFormTable :global(ul.rules > li) {
list-style: none;
font-size: 11px;
}
.postFormTable :global(.rules > li::before) {
content: "\2022 \20";
}
.postFormTable :global(tr.rules td) {
border: 0px !important;
background-color: transparent !important;
font-weight: normal !important;
text-transform: none !important;
}
.postFormTable :global(tr.rules a),
.postFormTable :global(tr.rules a:visited) {
color: var(--post-link-text-color);
text-decoration: none;
}
.postFormTable :global(tr.rules a:hover),
.postFormTable :global(tr.rules a:visited:hover) {
color: var(--post-link-text-color-hover);
text-decoration: none;
}
.error {
font-weight: bold;
padding: 5px;
+22 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address';
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
@@ -10,6 +10,7 @@ import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
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 { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
@@ -132,6 +133,7 @@ interface PostFormFieldsProps {
accountCommunityAddresses: string[];
subscriptions: string[];
communityAddress: string | undefined;
rulesPath: string;
requirePostLinkIsMedia: boolean;
showBbcodeToolbar: boolean;
onBbcodePreviewToggle: () => void;
@@ -171,6 +173,7 @@ const PostFormFields = ({
accountCommunityAddresses,
subscriptions,
communityAddress,
rulesPath,
requirePostLinkIsMedia,
showBbcodeToolbar,
onBbcodePreviewToggle,
@@ -313,6 +316,21 @@ const PostFormFields = ({
</td>
</tr>
)}
<tr className='rules'>
<td colSpan={2}>
<ul className='rules'>
<li>
<Trans
i18nKey='post_form_rules_faq_prompt'
components={{
rules: <Link to={rulesPath} />,
faq: <Link to='/faq' />,
}}
/>
</li>
</ul>
</td>
</tr>
{((isInPostView && showSpoilerForReply) || (!isInPostView && showSpoilerForPost)) && (
<tr className={styles.spoilerButton}>
<td>{t('options')}</td>
@@ -388,6 +406,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const subscriptions = account?.subscriptions || [];
const directories = useDirectories();
const directoryEntry = useDirectoryByAddress(effectiveBoardAddress);
const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules';
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
@@ -621,6 +640,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
accountCommunityAddresses={accountCommunityAddresses}
subscriptions={subscriptions}
communityAddress={communityAddress}
rulesPath={rulesPath}
requirePostLinkIsMedia={requirePostLinkIsMedia}
showBbcodeToolbar={showBbcodeToolbar}
onBbcodePreviewToggle={handleBbcodePreviewToggle}