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 = {
|
||||
author?: {
|
||||
address?: string;
|
||||
displayName?: string;
|
||||
shortAddress?: string;
|
||||
};
|
||||
cid?: string;
|
||||
@@ -479,6 +480,24 @@ describe('post community address compatibility', () => {
|
||||
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 () => {
|
||||
testState.hasMoreReplies = true;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type TestComment = {
|
||||
author?: {
|
||||
address?: string;
|
||||
displayName?: string;
|
||||
shortAddress?: string;
|
||||
};
|
||||
cid: string;
|
||||
content?: string;
|
||||
@@ -379,6 +380,44 @@ describe('CatalogRow', () => {
|
||||
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 () => {
|
||||
testState.directories = [{ address: 'music-posting.bso', features: { requirePostLinkIsMedia: true }, title: '/mu/ - Music' }];
|
||||
testState.linkCount = 3;
|
||||
|
||||
@@ -24,6 +24,7 @@ import styles from './catalog-row.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||
import { getCommentCommunityAddress, withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||
|
||||
interface CatalogPostMediaProps {
|
||||
cid: string;
|
||||
@@ -194,14 +195,16 @@ const CatalogPost = memo(
|
||||
const { replies } = useReplies({ comment: showPortal ? resolvedPost : undefined, flat: true });
|
||||
const lastReply = replies?.length > 0 ? replies[replies.length - 1] : null;
|
||||
|
||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
const { commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
communityAddress: communityAddress ?? '',
|
||||
});
|
||||
const { isCommentAuthorMod: isLastReplyAuthorMod, commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||
const { commentAuthorRole: lastReplyAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: lastReply?.author?.address,
|
||||
communityAddress: communityAddress ?? '',
|
||||
});
|
||||
const catalogPostAuthorBadge = getAuthorBadge({ address: author?.address, role: catalogPostAuthorRole });
|
||||
const lastReplyAuthorBadge = getAuthorBadge({ address: lastReply?.author?.address, role: lastReplyAuthorRole });
|
||||
|
||||
const postContent = (
|
||||
<div className={`${styles.teaser} ${hidden && styles.hidden}`}>
|
||||
@@ -300,18 +303,22 @@ const CatalogPost = memo(
|
||||
) : (
|
||||
t('posted_by')
|
||||
)}{' '}
|
||||
<span className={`${styles.postAuthor} ${isCatalogPostAuthorMod && styles.capcode}`}>
|
||||
<span className={`${styles.postAuthor} ${catalogPostAuthorBadge ? styles.capcode : ''}`}>
|
||||
{author?.displayName || capitalize(t('anonymous'))}
|
||||
{isCatalogPostAuthorMod && <span className='capitalize'>{` ## Board ${catalogPostAuthorRole}`}</span>}
|
||||
{catalogPostAuthorBadge && (
|
||||
<span className={catalogPostAuthorBadge.capitalizeLabel ? 'capitalize' : undefined}>{` ## ${catalogPostAuthorBadge.label}`}</span>
|
||||
)}
|
||||
</span>
|
||||
{(isInAllView || isInSubscriptionsView) && communityAddress && ` to p/${getShortAddress(communityAddress)}`}
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(timestamp)}</span>
|
||||
{replyCount > 0 && (
|
||||
<div className={styles.postLast}>
|
||||
{t('last_reply_by')}{' '}
|
||||
<span className={`${styles.postAuthor} ${isLastReplyAuthorMod && styles.capcode}`}>
|
||||
<span className={`${styles.postAuthor} ${lastReplyAuthorBadge ? styles.capcode : ''}`}>
|
||||
{lastReply?.author?.displayName || capitalize(t('anonymous'))}
|
||||
{isLastReplyAuthorMod && ` ## Board ${lastReplyAuthorRole}`}
|
||||
{lastReplyAuthorBadge && (
|
||||
<span className={lastReplyAuthorBadge.capitalizeLabel ? 'capitalize' : undefined}>{` ## ${lastReplyAuthorBadge.label}`}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.postAgo}> {getFormattedTimeAgo(lastReply?.timestamp)}</span>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,7 @@ import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
||||
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
@@ -233,7 +234,7 @@ const PostInfo = ({
|
||||
const title = post?.title?.trim();
|
||||
const { address, shortAddress } = author || {};
|
||||
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 isReply = parentCid;
|
||||
const { showOmittedReplies } = useShowOmittedReplies();
|
||||
@@ -345,7 +346,9 @@ const PostInfo = ({
|
||||
</Tooltip>
|
||||
))}
|
||||
<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 ? (
|
||||
capitalize(t('deleted'))
|
||||
) : removed ? (
|
||||
@@ -363,13 +366,13 @@ const PostInfo = ({
|
||||
) : (
|
||||
capitalize(t('anonymous'))
|
||||
)}
|
||||
{!(deleted || removed || purged) && authorRole && (
|
||||
<span className='capitalize'>
|
||||
{!(deleted || removed || purged) && authorBadge && (
|
||||
<span className={authorBadge.capitalizeLabel ? 'capitalize' : undefined}>
|
||||
{' '}
|
||||
## Board {authorRole}{' '}
|
||||
## {authorBadge.label}{' '}
|
||||
<span
|
||||
className={`${styles.capcodeIcon} ${authorRole === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||
title={authorRole === 'mod' ? t('moderator_of_this_board') : t('administrator_of_this_board')}
|
||||
className={`${styles.capcodeIcon} ${authorBadge.icon === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||
title={authorBadge.title === '5chan Dev' ? authorBadge.title : t(authorBadge.title)}
|
||||
/>
|
||||
</span>
|
||||
)}{' '}
|
||||
|
||||
@@ -57,6 +57,7 @@ import useDeleteFailedPost from '../../hooks/use-delete-failed-post';
|
||||
import { getThreadPostCountsByAuthor } from '../../lib/utils/author-post-counts';
|
||||
import { withResolvedCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||
import { getFeedPostHeightEstimate, getReplyHeightEstimates, reportReplyHeightAuditSample } from '../../lib/utils/pretext-height-estimates';
|
||||
import { getAuthorBadge } from '../../lib/utils/author-display-utils';
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
@@ -91,7 +92,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
||||
const title = post?.title?.trim();
|
||||
const { address, shortAddress } = author || {};
|
||||
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 location = useLocation();
|
||||
@@ -269,7 +270,9 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
||||
<PostMenuMobile postMenu={postMenuProps} editMenuPost={resolvedPost} />
|
||||
<span className={(hidden || ((removed || deleted || purged) && !reason)) && parentCid ? styles.postDesktopHidden : ''}>
|
||||
<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 ? (
|
||||
capitalize(t('removed'))
|
||||
) : deleted ? (
|
||||
@@ -287,14 +290,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber, posts
|
||||
) : (
|
||||
capitalize(t('anonymous'))
|
||||
)}{' '}
|
||||
{!(deleted || removed || purged) && authorRole && (
|
||||
<span className='capitalize'>
|
||||
{!(deleted || removed || purged) && authorBadge && (
|
||||
<span className={authorBadge.capitalizeLabel ? 'capitalize' : undefined}>
|
||||
{' '}
|
||||
## Board {authorRole}{' '}
|
||||
## {authorBadge.label}{' '}
|
||||
<span className={styles.capcodeIconMobileWrapper}>
|
||||
<span
|
||||
className={`${styles.capcodeIconMobile} ${authorRole === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||
title={authorRole === 'mod' ? t('moderator_of_this_board') : t('administrator_of_this_board')}
|
||||
className={`${styles.capcodeIconMobile} ${authorBadge.icon === 'mod' ? styles.capcodeModIcon : styles.capcodeAdminIcon}`}
|
||||
title={authorBadge.title === '5chan Dev' ? authorBadge.title : t(authorBadge.title)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user