mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(author badges): add 5chan developer badges
This commit is contained in:
@@ -12,6 +12,7 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
|||||||
type TestComment = {
|
type TestComment = {
|
||||||
author?: {
|
author?: {
|
||||||
address?: string;
|
address?: string;
|
||||||
|
displayName?: string;
|
||||||
shortAddress?: string;
|
shortAddress?: string;
|
||||||
};
|
};
|
||||||
cid?: string;
|
cid?: string;
|
||||||
@@ -479,6 +480,24 @@ describe('post community address compatibility', () => {
|
|||||||
expect(container.textContent).toContain('reply-1');
|
expect(container.textContent).toContain('reply-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders known developer badges and keeps anonymous as the default name on desktop and mobile', async () => {
|
||||||
|
const post = {
|
||||||
|
...makeLegacyThread(),
|
||||||
|
author: { address: 'plebeius.bso', shortAddress: 'plebeius.bso' },
|
||||||
|
};
|
||||||
|
const roles = { 'plebeius.bso': { role: 'owner' } };
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostDesktop, { post, roles } as any));
|
||||||
|
expect(container.textContent).toContain('Anonymous');
|
||||||
|
expect(container.textContent).toContain('## 5chan Dev');
|
||||||
|
expect(container.querySelector('.capcodeAdminIcon')).toBeTruthy();
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostMobile, { post, roles } as any));
|
||||||
|
expect(container.textContent).toContain('Anonymous');
|
||||||
|
expect(container.textContent).toContain('## 5chan Dev');
|
||||||
|
expect(container.querySelector('.capcodeAdminIcon')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('forwards Pretext-backed reply estimates into Virtuoso for desktop and mobile thread views', async () => {
|
it('forwards Pretext-backed reply estimates into Virtuoso for desktop and mobile thread views', async () => {
|
||||||
testState.hasMoreReplies = true;
|
testState.hasMoreReplies = true;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type TestComment = {
|
|||||||
author?: {
|
author?: {
|
||||||
address?: string;
|
address?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
shortAddress?: string;
|
||||||
};
|
};
|
||||||
cid: string;
|
cid: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -379,6 +380,44 @@ describe('CatalogRow', () => {
|
|||||||
expect(document.body.textContent).toContain('ago:200');
|
expect(document.body.textContent).toContain('ago:200');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses developer badges and keeps anonymous as the default name in hover previews', async () => {
|
||||||
|
testState.mediaInfoByLink['https://example.com/dev.png'] = { type: 'image', url: 'https://example.com/dev.png' };
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
author: { address: 'rinse12.bso', shortAddress: 'rinse12.bso' },
|
||||||
|
cid: 'reply-dev',
|
||||||
|
timestamp: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testState.roleByAddress = {
|
||||||
|
'plebeius.bso': { commentAuthorRole: 'owner', isCommentAuthorMod: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const post: TestComment = {
|
||||||
|
author: { address: 'plebeius.bso', shortAddress: 'plebeius.bso' },
|
||||||
|
cid: 'post-dev',
|
||||||
|
content: 'Developer post',
|
||||||
|
link: 'https://example.com/dev.png',
|
||||||
|
replyCount: 1,
|
||||||
|
communityAddress: 'music-posting.eth',
|
||||||
|
timestamp: 100,
|
||||||
|
title: 'Dev thread',
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderWithRouter(createElement(CatalogRow, { row: [post] }), '/all/catalog');
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const previewTrigger = document.body.querySelector('a[href="/mu/thread/post-dev"] > div');
|
||||||
|
await act(async () => {
|
||||||
|
previewTrigger?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||||
|
vi.advanceTimersByTime(260);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.body.textContent).toContain('Dev thread by Anonymous ## 5chan Dev');
|
||||||
|
expect(document.body.textContent).toContain('last_reply_by Anonymous ## 5chan Dev');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses alias-aware board features when deciding whether reply links are media', async () => {
|
it('uses alias-aware board features when deciding whether reply links are media', async () => {
|
||||||
testState.directories = [{ address: 'music-posting.bso', features: { requirePostLinkIsMedia: true }, title: '/mu/ - Music' }];
|
testState.directories = [{ address: 'music-posting.bso', features: { requirePostLinkIsMedia: true }, title: '/mu/ - Music' }];
|
||||||
testState.linkCount = 3;
|
testState.linkCount = 3;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import styles from './catalog-row.module.css';
|
|||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
import { getCommentCommunityAddress, withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { getCommentCommunityAddress, withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
|
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||||
|
|
||||||
interface CatalogPostMediaProps {
|
interface CatalogPostMediaProps {
|
||||||
cid: string;
|
cid: string;
|
||||||
@@ -194,14 +195,16 @@ const CatalogPost = memo(
|
|||||||
const { replies } = useReplies({ comment: showPortal ? resolvedPost : undefined, flat: true });
|
const { replies } = useReplies({ comment: showPortal ? resolvedPost : undefined, flat: true });
|
||||||
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
||||||
|
|
||||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
const { commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||||
commentAuthorAddress: author?.address,
|
commentAuthorAddress: author?.address,
|
||||||
communityAddress: communityAddress ?? '',
|
communityAddress: communityAddress ?? '',
|
||||||
});
|
});
|
||||||
const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
const { commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||||
commentAuthorAddress: lastReply?.author?.address,
|
commentAuthorAddress: lastReply?.author?.address,
|
||||||
communityAddress: communityAddress ?? '',
|
communityAddress: communityAddress ?? '',
|
||||||
});
|
});
|
||||||
|
const catalogPostAuthorBadge = getAuthorBadge({ address: author?.address, role: catalogPostAuthorRole });
|
||||||
|
const lastReplyAuthorBadge = getAuthorBadge({ address: lastReply?.author?.address, role: lastReplyAuthorRole });
|
||||||
|
|
||||||
const postContent = (
|
const postContent = (
|
||||||
<div className={`${styles.teaser} ${hidden && styles.hidden}`}>
|
<div className={`${styles.teaser} ${hidden && styles.hidden}`}>
|
||||||
@@ -300,18 +303,22 @@ const CatalogPost = memo(
|
|||||||
) : (
|
) : (
|
||||||
t('posted_by')
|
t('posted_by')
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<span className={`${styles.postAuthor} ${isCatalogPostAuthorMod && styles.capcode}`}>
|
<span className={`${styles.postAuthor} ${catalogPostAuthorBadge ? styles.capcode : ''}`}>
|
||||||
{author?.displayName || capitalize(t('anonymous'))}
|
{author?.displayName || capitalize(t('anonymous'))}
|
||||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
{catalogPostAuthorBadge && (
|
||||||
|
<span className={catalogPostAuthorBadge.capitalizeLabel ? 'capitalize' : undefined}>{` ## ${catalogPostAuthorBadge.label}`}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{(isInAllView || isInSubscriptionsView) && communityAddress && ` to p/${getShortAddress(communityAddress)}`}
|
{(isInAllView || isInSubscriptionsView) && communityAddress && ` to p/${getShortAddress(communityAddress)}`}
|
||||||
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
||||||
{replyCount > 0 && (
|
{replyCount > 0 && (
|
||||||
<div className={styles.postLast}>
|
<div className={styles.postLast}>
|
||||||
{t('last_reply_by')}{' '}
|
{t('last_reply_by')}{' '}
|
||||||
<span className={`${styles.postAuthor} ${isLastReplyAuthorMod && styles.capcode}`}>
|
<span className={`${styles.postAuthor} ${lastReplyAuthorBadge ? styles.capcode : ''}`}>
|
||||||
{lastReply?.author?.displayName || capitalize(t('anonymous'))}
|
{lastReply?.author?.displayName || capitalize(t('anonymous'))}
|
||||||
{isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`}
|
{lastReplyAuthorBadge && (
|
||||||
|
<span className={lastReplyAuthorBadge.capitalizeLabel ? 'capitalize' : undefined}>{` ## ${lastReplyAuthorBadge.label}`}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
|||||||
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
||||||
|
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -233,7 +234,7 @@ const PostInfo = ({
|
|||||||
const title = post?.title?.trim();
|
const title = post?.title?.trim();
|
||||||
const { address, shortAddress } = author || {};
|
const { address, shortAddress } = author || {};
|
||||||
const displayName = author?.displayName?.trim();
|
const displayName = author?.displayName?.trim();
|
||||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
const authorBadge = getAuthorBadge({ address, role: roles?.[address]?.role });
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
const isReply = parentCid;
|
const isReply = parentCid;
|
||||||
const { showOmittedReplies } = useShowOmittedReplies();
|
const { showOmittedReplies } = useShowOmittedReplies();
|
||||||
@@ -345,7 +346,9 @@ const PostInfo = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
<span className={styles.nameBlock}>
|
<span className={styles.nameBlock}>
|
||||||
<span className={`${styles.name} ${authorRole && !(deleted || removed || purged) && (authorRole === 'mod' ? styles.capcodeMod : styles.capcodeAdmin)}`}>
|
<span
|
||||||
|
className={`${styles.name} ${authorBadge && !(deleted || removed || purged) ? (authorBadge.icon === 'mod' ? styles.capcodeMod : styles.capcodeAdmin) : ''}`}
|
||||||
|
>
|
||||||
{deleted ? (
|
{deleted ? (
|
||||||
capitalize(t('deleted'))
|
capitalize(t('deleted'))
|
||||||
) : removed ? (
|
) : removed ? (
|
||||||
@@ -363,13 +366,13 @@ const PostInfo = ({
|
|||||||
) : (
|
) : (
|
||||||
capitalize(t('anonymous'))
|
capitalize(t('anonymous'))
|
||||||
)}
|
)}
|
||||||
{!(deleted || removed || purged) && authorRole && (
|
{!(deleted || removed || purged) && authorBadge && (
|
||||||
<span className='capitalize'>
|
<span className={authorBadge.capitalizeLabel ? 'capitalize' : undefined}>
|
||||||
{' '}
|
{' '}
|
||||||
## Board {authorRole}{' '}
|
## {authorBadge.label}{' '}
|
||||||
<span
|
<span
|
||||||
className={`${styles.capcodeIcon} ${authorRole === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
className={`${styles.capcodeIcon} ${authorBadge.icon === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||||
title={authorRole === 'mod' ? t('moderator_of_this_board') : t('administrator_of_this_board')}
|
title={authorBadge.title === '5chan Dev' ? authorBadge.title : t(authorBadge.title)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
|||||||
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
||||||
|
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
|||||||
const title = post?.title?.trim();
|
const title = post?.title?.trim();
|
||||||
const { address, shortAddress } = author || {};
|
const { address, shortAddress } = author || {};
|
||||||
const displayName = author?.displayName?.trim();
|
const displayName = author?.displayName?.trim();
|
||||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
const authorBadge = getAuthorBadge({ address, role: roles?.[address]?.role });
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -269,7 +270,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
|||||||
<PostMenuMobile postMenu={postMenuProps} editMenuPost={resolvedPost} />
|
<PostMenuMobile postMenu={postMenuProps} editMenuPost={resolvedPost} />
|
||||||
<span className={(hidden || ((removed || deleted || purged) && !reason)) && parentCid ? styles.postDesktopHidden : ''}>
|
<span className={(hidden || ((removed || deleted || purged) && !reason)) && parentCid ? styles.postDesktopHidden : ''}>
|
||||||
<span className={styles.nameBlock}>
|
<span className={styles.nameBlock}>
|
||||||
<span className={`${styles.name} ${authorRole && !(deleted || removed || purged) && (authorRole === 'mod' ? styles.capcodeMod : styles.capcodeAdmin)}`}>
|
<span
|
||||||
|
className={`${styles.name} ${authorBadge && !(deleted || removed || purged) ? (authorBadge.icon === 'mod' ? styles.capcodeMod : styles.capcodeAdmin) : ''}`}
|
||||||
|
>
|
||||||
{removed ? (
|
{removed ? (
|
||||||
capitalize(t('removed'))
|
capitalize(t('removed'))
|
||||||
) : deleted ? (
|
) : deleted ? (
|
||||||
@@ -287,14 +290,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
|||||||
) : (
|
) : (
|
||||||
capitalize(t('anonymous'))
|
capitalize(t('anonymous'))
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{!(deleted || removed || purged) && authorRole && (
|
{!(deleted || removed || purged) && authorBadge && (
|
||||||
<span className='capitalize'>
|
<span className={authorBadge.capitalizeLabel ? 'capitalize' : undefined}>
|
||||||
{' '}
|
{' '}
|
||||||
## Board {authorRole}{' '}
|
## {authorBadge.label}{' '}
|
||||||
<span className={styles.capcodeIconMobileWrapper}>
|
<span className={styles.capcodeIconMobileWrapper}>
|
||||||
<span
|
<span
|
||||||
className={`${styles.capcodeIconMobile} ${authorRole === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
className={`${styles.capcodeIconMobile} ${authorBadge.icon === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||||
title={authorRole === 'mod' ? t('moderator_of_this_board') : t('administrator_of_this_board')}
|
title={authorBadge.title === '5chan Dev' ? authorBadge.title : t(authorBadge.title)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "estebanabaroa.bso",
|
||||||
|
"name": "Esteban Abaroa",
|
||||||
|
"githubProfileUrl": "https://github.com/estebanabaroa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "rinse12.bso",
|
||||||
|
"name": "Rinse",
|
||||||
|
"githubProfileUrl": "https://github.com/rinse12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "plebeius.bso",
|
||||||
|
"name": "Tommaso Casaburi",
|
||||||
|
"githubProfileUrl": "https://github.com/tomcasaburi"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { KNOWN_5CHAN_DEVELOPER_ENTRIES, getAuthorBadge, isKnown5chanDeveloper } from '../author-display-utils';
|
||||||
|
|
||||||
|
describe('author display utils', () => {
|
||||||
|
it('recognizes the hardcoded 5chan developer addresses', () => {
|
||||||
|
expect(isKnown5chanDeveloper('estebanabaroa.bso')).toBe(true);
|
||||||
|
expect(isKnown5chanDeveloper('rinse12.bso')).toBe(true);
|
||||||
|
expect(isKnown5chanDeveloper('plebeius.bso')).toBe(true);
|
||||||
|
expect(isKnown5chanDeveloper('someone-else.bso')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps optional developer profile metadata next to the hardcoded address', () => {
|
||||||
|
expect(KNOWN_5CHAN_DEVELOPER_ENTRIES).toContainEqual({
|
||||||
|
address: 'rinse12.bso',
|
||||||
|
githubProfileUrl: 'https://github.com/rinse12',
|
||||||
|
name: 'Rinse',
|
||||||
|
});
|
||||||
|
expect(KNOWN_5CHAN_DEVELOPER_ENTRIES).toContainEqual({
|
||||||
|
address: 'plebeius.bso',
|
||||||
|
githubProfileUrl: 'https://github.com/tomcasaburi',
|
||||||
|
name: 'Tommaso Casaburi',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('labels known developers with the admin icon style', () => {
|
||||||
|
expect(getAuthorBadge({ address: 'rinse12.bso' })).toEqual({
|
||||||
|
icon: 'admin',
|
||||||
|
label: '5chan Dev',
|
||||||
|
title: '5chan Dev',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the developer label even when the account has a board role', () => {
|
||||||
|
expect(getAuthorBadge({ address: 'plebeius.bso', role: 'owner' })).toEqual({
|
||||||
|
icon: 'admin',
|
||||||
|
label: '5chan Dev',
|
||||||
|
title: '5chan Dev',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps board role labels for non-developers', () => {
|
||||||
|
expect(getAuthorBadge({ address: 'other.bso', role: 'moderator' })).toEqual({
|
||||||
|
capitalizeLabel: true,
|
||||||
|
icon: 'mod',
|
||||||
|
label: 'Board mod',
|
||||||
|
title: 'moderator_of_this_board',
|
||||||
|
});
|
||||||
|
expect(getAuthorBadge({ address: 'other.bso', role: 'owner' })).toEqual({
|
||||||
|
capitalizeLabel: true,
|
||||||
|
icon: 'admin',
|
||||||
|
label: 'Board owner',
|
||||||
|
title: 'administrator_of_this_board',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import known5chanDeveloperEntries from '../../data/known-5chan-developers.json';
|
||||||
|
|
||||||
|
interface Known5chanDeveloperEntry {
|
||||||
|
address: string;
|
||||||
|
githubProfileUrl?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KNOWN_5CHAN_DEVELOPER_ENTRIES = known5chanDeveloperEntries as readonly Known5chanDeveloperEntry[];
|
||||||
|
export const KNOWN_5CHAN_DEVELOPER_ADDRESSES = KNOWN_5CHAN_DEVELOPER_ENTRIES.map(({ address }) => address);
|
||||||
|
|
||||||
|
type AuthorBadgeIcon = 'admin' | 'mod';
|
||||||
|
|
||||||
|
interface AuthorBadge {
|
||||||
|
capitalizeLabel?: boolean;
|
||||||
|
icon: AuthorBadgeIcon;
|
||||||
|
label: string;
|
||||||
|
title: '5chan Dev' | 'administrator_of_this_board' | 'moderator_of_this_board';
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBoardRole = (role?: string): string | undefined => {
|
||||||
|
const normalizedRole = role?.trim();
|
||||||
|
if (!normalizedRole) return undefined;
|
||||||
|
return normalizedRole.toLowerCase() === 'moderator' ? 'mod' : normalizedRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isKnown5chanDeveloper = (address?: string): boolean =>
|
||||||
|
typeof address === 'string' && KNOWN_5CHAN_DEVELOPER_ENTRIES.some((developer) => developer.address === address);
|
||||||
|
|
||||||
|
export const getAuthorBadge = ({ address, role }: { address?: string; role?: string }): AuthorBadge | undefined => {
|
||||||
|
const boardRole = normalizeBoardRole(role);
|
||||||
|
const isDeveloper = isKnown5chanDeveloper(address);
|
||||||
|
|
||||||
|
if (isDeveloper) {
|
||||||
|
return {
|
||||||
|
icon: 'admin',
|
||||||
|
label: '5chan Dev',
|
||||||
|
title: '5chan Dev',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boardRole) return undefined;
|
||||||
|
|
||||||
|
const isMod = boardRole?.toLowerCase() === 'mod';
|
||||||
|
return {
|
||||||
|
capitalizeLabel: true,
|
||||||
|
icon: isMod ? 'mod' : 'admin',
|
||||||
|
label: `Board ${boardRole}`,
|
||||||
|
title: isMod ? 'moderator_of_this_board' : 'administrator_of_this_board',
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user