fix(post): show specific role in moderation posting warning

Replace the generic "posting as moderator" label with the effective role
(5chan dev, owner, admin, or moderator) in post and reply forms.
This commit is contained in:
Tommaso Casaburi
2026-06-01 12:48:42 +07:00
parent d48cf10ee8
commit 812b6bc79c
6 changed files with 47 additions and 6 deletions
@@ -1027,6 +1027,24 @@ describe('PostForm', () => {
expect(container.textContent).not.toContain('warning: posting as moderator'); expect(container.textContent).not.toContain('warning: posting as moderator');
}); });
it('uses the 5chan dev label for known developer moderator posts', async () => {
testState.account = {
author: { address: 'plebeius.bso', displayName: 'Tom' },
subscriptions: ['music-posting.eth'],
};
testState.resolvedCommunityAddress = 'music-posting.eth';
testState.rolesByCommunity = {
'music-posting.eth': {
'plebeius.bso': { role: 'owner' },
},
};
await renderPostForm('/mu');
await clickByText(container, 'start_new_thread');
expect(container.textContent).toContain('warning: posting as 5chan dev');
});
it('shows the pasted file-link filename next to the upload button', async () => { it('shows the pasted file-link filename next to the upload button', async () => {
testState.uploadedFileName = null; testState.uploadedFileName = null;
+4 -1
View File
@@ -21,6 +21,7 @@ import {
} from '../../lib/utils/post-options-utils'; } from '../../lib/utils/post-options-utils';
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils'; import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils'; import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { hasModQueueAccessRole } from '../../lib/utils/mod-access'; import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getBoardPath } from '../../lib/utils/route-utils'; import { getBoardPath } from '../../lib/utils/route-utils';
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
@@ -536,6 +537,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const roles = useCommunityField(effectiveBoardAddress, (community) => community?.roles); const roles = useCommunityField(effectiveBoardAddress, (community) => community?.roles);
const accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined; const accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined;
const showBbcodeToolbar = hasModQueueAccessRole(accountRole) || (!effectiveBoardAddress && isInModView && accountCommunityAddresses.length > 0); const showBbcodeToolbar = hasModQueueAccessRole(accountRole) || (!effectiveBoardAddress && isInModView && accountCommunityAddresses.length > 0);
const moderationPostingRoleLabel = getModerationPostingRoleLabel({ address: accountAddress, role: accountRole });
const moderationPostingWarning = showBbcodeToolbar && moderationPostingRoleLabel ? `warning: posting as ${moderationPostingRoleLabel}` : undefined;
const [lengthError, setLengthError] = useState<string | null>(null); const [lengthError, setLengthError] = useState<string | null>(null);
const [formError, setFormError] = useState<string | PostOptionsValidationError | null>(null); const [formError, setFormError] = useState<string | PostOptionsValidationError | null>(null);
@@ -877,7 +880,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
/> />
</tbody> </tbody>
</table> </table>
{showBbcodeToolbar ? <div className={`${styles.error} ${styles.formError}`}>warning: posting as moderator</div> : null} {moderationPostingWarning ? <div className={`${styles.error} ${styles.formError}`}>{moderationPostingWarning}</div> : null}
{formError ? ( {formError ? (
<div className={`${styles.error} ${styles.formError}`}> <div className={`${styles.error} ${styles.formError}`}>
{isPostOptionsValidationError(formError) ? <PostOptionsErrorMessage error={formError} directories={directories} /> : formError} {isPostOptionsValidationError(formError) ? <PostOptionsErrorMessage error={formError} directories={directories} /> : formError}
@@ -759,8 +759,8 @@ describe('ReplyModal', () => {
expect(container.querySelector('select[aria-label="Text color"]')).toBeNull(); expect(container.querySelector('select[aria-label="Text color"]')).toBeNull();
expect(container.textContent).not.toContain('mods only'); expect(container.textContent).not.toContain('mods only');
expect(container.textContent).not.toContain('Mod editor'); expect(container.textContent).not.toContain('Mod editor');
expect(container.textContent).toContain('warning: posting as moderator'); expect(container.textContent).toContain('warning: posting as admin');
const moderatorWarning = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'warning: posting as moderator'); const moderatorWarning = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'warning: posting as admin');
expect(moderatorWarning?.className).toContain('error'); expect(moderatorWarning?.className).toContain('error');
expect(container.querySelector('button[aria-label="Quote"]')).toBeNull(); expect(container.querySelector('button[aria-label="Quote"]')).toBeNull();
@@ -778,7 +778,7 @@ describe('ReplyModal', () => {
expect(container.querySelector('button[aria-label="Red text"]')).toBeNull(); expect(container.querySelector('button[aria-label="Red text"]')).toBeNull();
expect(container.textContent).not.toContain('mods only'); expect(container.textContent).not.toContain('mods only');
expect(container.textContent).not.toContain('warning: posting as moderator'); expect(container.textContent).not.toContain('warning: posting as admin');
}); });
it('updates account state, applies upload completions, and closes once publishing succeeds', async () => { it('updates account state, applies upload completions, and closes once publishing succeeds', async () => {
+4 -1
View File
@@ -18,6 +18,7 @@ import {
} from '../../lib/utils/post-options-utils'; } from '../../lib/utils/post-options-utils';
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils'; import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
import { hasModQueueAccessRole } from '../../lib/utils/mod-access'; import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useSelectedTextStore from '../../stores/use-selected-text-store'; import useSelectedTextStore from '../../stores/use-selected-text-store';
import useReplyModalStore from '../../stores/use-reply-modal-store'; import useReplyModalStore from '../../stores/use-reply-modal-store';
@@ -81,6 +82,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const roles = useCommunityField(communityAddress, (community) => community?.roles); const roles = useCommunityField(communityAddress, (community) => community?.roles);
const accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined; const accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined;
const showBbcodeToolbar = hasModQueueAccessRole(accountRole); const showBbcodeToolbar = hasModQueueAccessRole(accountRole);
const moderationPostingRoleLabel = getModerationPostingRoleLabel({ address: accountAddress, role: accountRole });
const moderationPostingWarning = showBbcodeToolbar && moderationPostingRoleLabel ? `warning: posting as ${moderationPostingRoleLabel}` : undefined;
const textRef = useRef<HTMLTextAreaElement | null>(null); const textRef = useRef<HTMLTextAreaElement | null>(null);
const setTextRef = useRef((element: HTMLTextAreaElement | null) => { const setTextRef = useRef((element: HTMLTextAreaElement | null) => {
textRef.current = element; textRef.current = element;
@@ -609,7 +612,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
{t('post')} {t('post')}
</button> </button>
</div> </div>
{showBbcodeToolbar ? <div className={styles.error}>warning: posting as moderator</div> : null} {moderationPostingWarning ? <div className={styles.error}>{moderationPostingWarning}</div> : null}
{lengthError ? ( {lengthError ? (
<div className={styles.error}>{lengthError}</div> <div className={styles.error}>{lengthError}</div>
) : error ? ( ) : error ? (
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { KNOWN_5CHAN_DEVELOPER_ENTRIES, get5chanDeveloperBadge, getAuthorBadge, isKnown5chanDeveloper } from '../author-display-utils'; import { KNOWN_5CHAN_DEVELOPER_ENTRIES, get5chanDeveloperBadge, getAuthorBadge, getModerationPostingRoleLabel, isKnown5chanDeveloper } from '../author-display-utils';
describe('author display utils', () => { describe('author display utils', () => {
it('recognizes the hardcoded 5chan developer addresses', () => { it('recognizes the hardcoded 5chan developer addresses', () => {
@@ -61,4 +61,12 @@ describe('author display utils', () => {
title: 'administrator_of_this_board', title: 'administrator_of_this_board',
}); });
}); });
it('returns the posting warning label for the effective moderator role', () => {
expect(getModerationPostingRoleLabel({ address: 'plebeius.bso', role: 'owner' })).toBe('5chan dev');
expect(getModerationPostingRoleLabel({ address: 'other.bso', role: 'owner' })).toBe('owner');
expect(getModerationPostingRoleLabel({ address: 'other.bso', role: 'admin' })).toBe('admin');
expect(getModerationPostingRoleLabel({ address: 'other.bso', role: 'moderator' })).toBe('moderator');
expect(getModerationPostingRoleLabel({ address: 'other.bso', role: 'viewer' })).toBeUndefined();
});
}); });
+9
View File
@@ -51,3 +51,12 @@ export const getAuthorBadge = ({ address, role }: { address?: string; role?: str
title: isMod ? 'moderator_of_this_board' : 'administrator_of_this_board', title: isMod ? 'moderator_of_this_board' : 'administrator_of_this_board',
}; };
}; };
export const getModerationPostingRoleLabel = ({ address, role }: { address?: string; role?: string }): string | undefined => {
if (isKnown5chanDeveloper(address)) return '5chan dev';
const normalizedRole = role?.trim().toLowerCase();
if (normalizedRole === 'owner' || normalizedRole === 'admin' || normalizedRole === 'moderator') return normalizedRole;
return undefined;
};