diff --git a/src/components/post-form/__tests__/post-form.test.tsx b/src/components/post-form/__tests__/post-form.test.tsx index 54b48024..80189e23 100644 --- a/src/components/post-form/__tests__/post-form.test.tsx +++ b/src/components/post-form/__tests__/post-form.test.tsx @@ -1052,6 +1052,24 @@ describe('PostForm', () => { 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 () => { testState.uploadedFileName = null; diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index c8c1a0ed..770d5338 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -21,6 +21,7 @@ import { } from '../../lib/utils/post-options-utils'; import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-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 { getBoardPath } from '../../lib/utils/route-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 accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined; 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(null); const [formError, setFormError] = useState(null); @@ -877,7 +880,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: /> - {showBbcodeToolbar ?
warning: posting as moderator
: null} + {moderationPostingWarning ?
{moderationPostingWarning}
: null} {formError ? (
{isPostOptionsValidationError(formError) ? : formError} diff --git a/src/components/reply-modal/__tests__/reply-modal.test.tsx b/src/components/reply-modal/__tests__/reply-modal.test.tsx index b510d2a2..5e961e08 100644 --- a/src/components/reply-modal/__tests__/reply-modal.test.tsx +++ b/src/components/reply-modal/__tests__/reply-modal.test.tsx @@ -775,8 +775,8 @@ describe('ReplyModal', () => { expect(container.querySelector('select[aria-label="Text color"]')).toBeNull(); expect(container.textContent).not.toContain('mods only'); expect(container.textContent).not.toContain('Mod editor'); - expect(container.textContent).toContain('warning: posting as moderator'); - const moderatorWarning = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === '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 admin'); expect(moderatorWarning?.className).toContain('error'); expect(container.querySelector('button[aria-label="Quote"]')).toBeNull(); @@ -794,7 +794,7 @@ describe('ReplyModal', () => { expect(container.querySelector('button[aria-label="Red text"]')).toBeNull(); 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 () => { diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index cd867dd1..f5ed67c4 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -18,6 +18,7 @@ import { } from '../../lib/utils/post-options-utils'; import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils'; 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 useSelectedTextStore from '../../stores/use-selected-text-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 accountRole = accountAddress ? roles?.[accountAddress]?.role : undefined; const showBbcodeToolbar = hasModQueueAccessRole(accountRole); + const moderationPostingRoleLabel = getModerationPostingRoleLabel({ address: accountAddress, role: accountRole }); + const moderationPostingWarning = showBbcodeToolbar && moderationPostingRoleLabel ? `warning: posting as ${moderationPostingRoleLabel}` : undefined; const textRef = useRef(null); const setTextRef = useRef((element: HTMLTextAreaElement | null) => { textRef.current = element; @@ -609,7 +612,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa {t('post')}
- {showBbcodeToolbar ?
warning: posting as moderator
: null} + {moderationPostingWarning ?
{moderationPostingWarning}
: null} {lengthError ? (
{lengthError}
) : error ? ( diff --git a/src/lib/utils/__tests__/author-display-utils.test.ts b/src/lib/utils/__tests__/author-display-utils.test.ts index 54eb1c5e..b4e54a50 100644 --- a/src/lib/utils/__tests__/author-display-utils.test.ts +++ b/src/lib/utils/__tests__/author-display-utils.test.ts @@ -1,5 +1,5 @@ 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', () => { it('recognizes the hardcoded 5chan developer addresses', () => { @@ -61,4 +61,12 @@ describe('author display utils', () => { 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(); + }); }); diff --git a/src/lib/utils/author-display-utils.ts b/src/lib/utils/author-display-utils.ts index f41546b3..0f9711b7 100644 --- a/src/lib/utils/author-display-utils.ts +++ b/src/lib/utils/author-display-utils.ts @@ -51,3 +51,12 @@ export const getAuthorBadge = ({ address, role }: { address?: string; role?: str 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; +};