mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(posts): add mod bbcode editor
This commit is contained in:
@@ -9,6 +9,7 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
||||
|
||||
type TestComment = {
|
||||
author?: {
|
||||
address?: string;
|
||||
community?: {
|
||||
banExpiresAt?: number;
|
||||
};
|
||||
@@ -179,9 +180,11 @@ vi.mock('../../tooltip', () => ({
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const renderContent = async (comment: TestComment) => {
|
||||
type TestRoleMap = Record<string, { role?: string }>;
|
||||
|
||||
const renderContent = async (comment: TestComment, roles?: TestRoleMap) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(CommentContent, { comment } as any));
|
||||
root.render(createElement(CommentContent, { comment, roles } as any));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -288,6 +291,67 @@ describe('CommentContent', () => {
|
||||
expect(blockquote?.querySelectorAll('br')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders whitelisted BBCode only for board moderator authors', async () => {
|
||||
await renderContent(
|
||||
{
|
||||
author: { address: '0xmod' },
|
||||
cid: 'post-1',
|
||||
communityAddress: 'music-posting.eth',
|
||||
content: '[b]bold[/b] [color=red][size=24][url=https://example.com]large red[/url][/size][/color] [x]literal[/x]',
|
||||
postCid: 'post-1',
|
||||
},
|
||||
{
|
||||
'0xmod': { role: 'moderator' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(container.querySelector('strong')?.textContent).toBe('bold');
|
||||
expect(container.querySelector('[class*="colorRed"]')?.textContent).toBe('large red');
|
||||
expect(container.querySelector('[class*="size24"]')?.textContent).toBe('large red');
|
||||
expect(container.querySelector('a')?.getAttribute('href')).toBe('https://example.com/');
|
||||
expect(container.textContent).toContain('[x]literal[/x]');
|
||||
|
||||
await renderContent(
|
||||
{
|
||||
author: { address: '0xuser' },
|
||||
cid: 'post-2',
|
||||
communityAddress: 'music-posting.eth',
|
||||
content: '[b]plain[/b]',
|
||||
postCid: 'post-2',
|
||||
},
|
||||
{
|
||||
'0xuser': { role: 'user' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(container.querySelector('strong')).toBeNull();
|
||||
expect(queryMarkdownText()).toEqual(['[b]plain[/b]']);
|
||||
});
|
||||
|
||||
it('ignores unsupported BBCode styling values for moderator authors', async () => {
|
||||
await renderContent(
|
||||
{
|
||||
author: { address: '0xmod' },
|
||||
cid: 'post-1',
|
||||
content: '[color=#ff0000]hex[/color] [color=blue]blue[/color] [url=javascript:alert(1)]bad[/url] [size=huge]huge[/size]',
|
||||
postCid: 'post-1',
|
||||
},
|
||||
{
|
||||
'0xmod': { role: 'admin' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain('hex');
|
||||
expect(container.textContent).toContain('blue');
|
||||
expect(container.textContent).toContain('bad');
|
||||
expect(container.textContent).toContain('huge');
|
||||
expect(container.querySelector('[class*="colorRed"]')).toBeNull();
|
||||
expect(container.querySelector('[class*="colorBlue"]')).toBeNull();
|
||||
expect(container.querySelector('a')).toBeNull();
|
||||
expect(container.querySelector('[class*="sizeLarge"]')).toBeNull();
|
||||
expect(container.querySelector('[class*="size24"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('truncates long comments outside the post view and expands them on demand', async () => {
|
||||
const longComment = 'x'.repeat(1105);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isPostPageView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useStateString from '../../hooks/use-state-string';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import BbcodeContent from '../../components/bbcode-content/bbcode-content';
|
||||
import ErrorDisplay from '../../components/error-display/error-display';
|
||||
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
||||
import Markdown from '../../components/markdown';
|
||||
@@ -19,6 +20,7 @@ import styles from '../../views/post/post.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { getCommentCommunityAddress, withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||
import { formatErrorMessageForDisplay } from '../../lib/utils/error-utils';
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
|
||||
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
||||
const quotedNumber = usePostNumberStore((state) => state.cidToNumber[cid]);
|
||||
@@ -71,7 +73,24 @@ const getFailedCommentError = (comment: Comment | undefined): unknown => {
|
||||
return Array.isArray(comment.errors) ? comment.errors.find(Boolean) : undefined;
|
||||
};
|
||||
|
||||
const CommentContent = ({ appendContent, comment: post, prependContent }: { appendContent?: ReactNode; comment: Comment; prependContent?: ReactNode }) => {
|
||||
const getRoleByAddress = (roles: unknown, address?: string): string | undefined => {
|
||||
if (!roles || !address || typeof roles !== 'object') return undefined;
|
||||
|
||||
const roleEntry = (roles as Record<string, { role?: unknown } | undefined>)[address];
|
||||
return typeof roleEntry?.role === 'string' ? roleEntry.role : undefined;
|
||||
};
|
||||
|
||||
const CommentContent = ({
|
||||
appendContent,
|
||||
comment: post,
|
||||
prependContent,
|
||||
roles,
|
||||
}: {
|
||||
appendContent?: ReactNode;
|
||||
comment: Comment;
|
||||
prependContent?: ReactNode;
|
||||
roles?: unknown;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
@@ -82,6 +101,9 @@ const CommentContent = ({ appendContent, comment: post, prependContent }: { appe
|
||||
|
||||
const { cid, content, deleted, edit, original, parentCid, postCid, pendingApproval, quotedCids, reason, removed, state } = resolvedPost || {};
|
||||
const communityAddress = getCommentCommunityAddress(resolvedPost);
|
||||
const authorAddress = resolvedPost?.author?.address;
|
||||
const authorRole = getRoleByAddress(roles, authorAddress);
|
||||
const shouldRenderBbcode = hasModQueueAccessRole(authorRole);
|
||||
const purged = resolvedPost?.commentModeration?.purged;
|
||||
const banExpiresAt = resolvedPost?.author?.community?.banExpiresAt;
|
||||
const banned = !!banExpiresAt;
|
||||
@@ -146,6 +168,12 @@ const CommentContent = ({ appendContent, comment: post, prependContent }: { appe
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const renderContent = (value: string | undefined) =>
|
||||
shouldRenderBbcode ? (
|
||||
<BbcodeContent content={value || ''} postCid={postCid} communityAddress={communityAddress} />
|
||||
) : (
|
||||
<Markdown content={value || ''} postCid={postCid} communityAddress={communityAddress} />
|
||||
);
|
||||
|
||||
return (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
||||
@@ -185,7 +213,7 @@ const CommentContent = ({ appendContent, comment: post, prependContent }: { appe
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{!showOriginal && <Markdown content={displayContent} postCid={postCid} communityAddress={communityAddress} />}
|
||||
{!showOriginal && renderContent(displayContent)}
|
||||
{pendingApproval && (
|
||||
<>
|
||||
<br />
|
||||
@@ -221,7 +249,7 @@ const CommentContent = ({ appendContent, comment: post, prependContent }: { appe
|
||||
)}
|
||||
{edit && original?.content !== content && (
|
||||
<span className={styles.editedInfo}>
|
||||
{showOriginal && <Markdown content={original?.content} postCid={postCid} communityAddress={communityAddress} />}
|
||||
{showOriginal && renderContent(original?.content)}
|
||||
<br />
|
||||
<br />
|
||||
<Trans
|
||||
|
||||
Reference in New Issue
Block a user