Add "Follows you" badge to AccountListItem component (#38828)

This commit is contained in:
diondiondion 2026-04-28 17:18:02 +02:00 committed by GitHub
parent 6c5bd4f9a8
commit ffd7160980
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 104 additions and 49 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M13.2502 8.00029C13.2502 5.10089 10.8996 2.75044 8.00024 2.75029C5.10075 2.75029 2.75024 5.10079 2.75024 8.00029C2.7504 10.8997 5.10084 13.2503 8.00024 13.2503C10.8995 13.2501 13.2501 10.8996 13.2502 8.00029ZM7.41663 4.50029C7.41663 4.17812 7.67808 3.91667 8.00024 3.91667C8.32228 3.91683 8.58301 4.17822 8.58301 4.50029V7.63884L10.5945 8.64458C10.8825 8.78865 10.999 9.13922 10.8551 9.42729C10.711 9.71541 10.3605 9.83276 10.0724 9.68877L7.73877 8.52153C7.54143 8.4227 7.41673 8.221 7.41663 8.00029V4.50029ZM14.4166 8.00029C14.4165 11.5439 11.5438 14.4165 8.00024 14.4167C4.45651 14.4167 1.58316 11.544 1.58301 8.00029C1.58301 4.45646 4.45642 1.58305 8.00024 1.58305C11.5439 1.58321 14.4166 4.45656 14.4166 8.00029Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 810 B

View File

@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { accountFactoryState } from '@/testing/factories';
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
import { PendingBadge } from '../badge';
import { AccountListItem } from './index';
@ -26,12 +28,30 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const FollowsYou: Story = {
parameters: {
state: {
relationships: {
'1': relationshipsFactory({
followed_by: true,
}),
},
},
},
};
export const WithCustomStats: Story = {
args: {
stats: ['posts', 'last-active'],
},
};
export const WithCustomBadge: Story = {
args: {
badge: <PendingBadge />,
},
};
export const WithBorder: Story = {
args: {
withBorder: true,

View File

@ -1,18 +1,23 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Account } from 'mastodon/components/account';
import { VerifiedBadge } from 'mastodon/components/badge';
import { FollowsYouBadge, VerifiedBadge } from 'mastodon/components/badge';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useRelationship } from 'mastodon/hooks/useRelationship';
import { domain } from 'mastodon/initial_state';
import type { Relationship } from 'mastodon/models/relationship';
import { Avatar } from '../avatar';
import { useAccountHandle } from '../display_name/default';
import { DisplayNameSimple } from '../display_name/simple';
import { EmojiHTML } from '../emoji/html';
import { FollowButton } from '../follow_button';
import { FormattedDateWrapper } from '../formatted_date';
import { ListItemLink, ListItemWrapper } from '../list_item';
import { NumberFields, NumberFieldsItem } from '../number_fields';
import { RelativeTimestamp } from '../relative_timestamp';
import { ShortNumber } from '../short_number';
@ -29,9 +34,10 @@ type Stat = 'followers' | 'following' | 'posts' | 'joined' | 'last-active';
interface Props {
accountId: string | undefined;
stats?: Stat[];
renderButton?: (options: RenderButtonOptions) => React.ReactNode;
withBio?: boolean;
withBorder?: boolean;
badge?: ReactNode;
renderButton?: (options: RenderButtonOptions) => React.ReactNode;
}
const DEFAULT_STATS: Stat[] = ['followers', 'posts', 'last-active'];
@ -48,10 +54,12 @@ export const AccountListItem: React.FC<Props> = ({
stats = DEFAULT_STATS,
withBio = true,
withBorder = true,
badge: badgeProp,
renderButton = defaultRenderButton,
}) => {
const intl = useIntl();
const account = useAccount(accountId);
const handle = useAccountHandle(account, domain);
const relationship = useRelationship(accountId);
const createdThisYear = useMemo(
@ -63,22 +71,34 @@ export const AccountListItem: React.FC<Props> = ({
return null;
}
const badge =
badgeProp ?? (relationship?.followed_by ? <FollowsYouBadge /> : null);
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
return (
<div className={classes.wrapper} data-with-border={withBorder}>
<div className={classes.header}>
<Account
id={accountId}
minimal
size={40}
withMenu={false}
withBorder={false}
className={classes.account}
/>
{renderButton({ accountId, relationship })}
</div>
<ListItemWrapper
className={classes.main}
icon={<Avatar account={account} size={40} />}
sideContent={
<span className={classes.button}>
{renderButton({ accountId, relationship })}
</span>
}
>
<ListItemLink
to={`/@${account.acct}`}
data-hover-card-account={accountId}
subtitle={handle}
>
<DisplayNameSimple
account={account}
className={classes.displayName}
/>
{badge && <span className={classes.badge}>{badge}</span>}
</ListItemLink>
</ListItemWrapper>
<NumberFields>
{stats.includes('followers') && (

View File

@ -10,22 +10,22 @@
}
}
.header {
display: flex;
align-items: start;
width: 100%;
.main {
--list-item-padding: 0;
}
.account {
--account-name-size: 15px;
--account-handle-color: var(--color-text-secondary);
--account-handle-size: 13px;
--account-bio-color: var(--color-text-primary);
--account-bio-size: 13px;
--account-outer-spacing: 0;
.displayName {
// Spacing for badge
margin-inline-end: 6px;
}
flex-grow: 1;
margin-inline-end: 16px;
.badge {
// Sort out vertical alignment next to name
vertical-align: -4px;
}
.button {
align-self: start;
}
.verifiedBadge {

View File

@ -5,6 +5,8 @@ import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import AdminIcon from '@/images/icons/icon_admin.svg?react';
import ClockIcon from '@/images/icons/icon_clock.svg?react';
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
import IconVerified from '@/images/icons/icon_verified.svg?react';
import type { OnAttributeHandler } from '@/mastodon/utils/html';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
@ -32,6 +34,11 @@ interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
| 'danger';
}
type PresetBadgeProps = Omit<
BadgeProps,
'label' | 'icon' | 'domain' | 'roleId'
>;
export const Badge: FC<BadgeProps> = ({
icon = <PersonIcon />,
variant = 'default',
@ -83,13 +90,32 @@ export const GroupBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
/>
);
export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => (
export const AutomatedBadge: FC<PresetBadgeProps> = (props) => (
<Badge
icon={<SmartToyIcon />}
label={
<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />
}
className={className}
{...props}
/>
);
export const FollowsYouBadge: FC<PresetBadgeProps> = (props) => (
<Badge
icon={<FollowerIcon />}
label={
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
}
{...props}
/>
);
export const PendingBadge: FC<PresetBadgeProps> = (props) => (
<Badge
variant='warning'
icon={<ClockIcon />}
label={<FormattedMessage id='account.pending' defaultMessage='Pending' />}
{...props}
/>
);

View File

@ -1,6 +1,7 @@
.badge {
color: var(--color-text-primary);
font-size: 13px;
font-weight: 400;
display: inline-flex;
max-width: 100%;
padding: 4px;
@ -61,7 +62,6 @@
.warning {
background-color: var(--color-bg-warning-softest);
color: var(--color-text-warning);
}
.danger {

View File

@ -7,9 +7,8 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay';
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
import { showAlert } from '@/mastodon/actions/alerts';
import { Badge } from '@/mastodon/components/badge';
import { FollowsYouBadge } from '@/mastodon/components/badge';
import { Button } from '@/mastodon/components/button';
import { DisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
@ -60,17 +59,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
<h1>
<DisplayName account={account} variant='simple' />
</h1>
{relationship?.followed_by && (
<Badge
icon={<FollowerIcon className={classes.followerBadgeIcon} />}
label={
<FormattedMessage
id='account.follows_you'
defaultMessage='Follows you'
/>
}
/>
)}
{relationship?.followed_by && <FollowsYouBadge />}
</div>
<AccountNameHelp

View File

@ -37,10 +37,6 @@
}
}
.followerBadgeIcon {
color: var(--color-text-secondary);
}
.badges {
margin-top: 8px;
}

View File

@ -131,6 +131,7 @@
"account.note.edit_button": "Edit",
"account.note.title": "Personal note (visible only to you)",
"account.open_original_page": "Open original page",
"account.pending": "Pending",
"account.posts": "Posts",
"account.remove_from_followers": "Remove {name} from followers",
"account.report": "Report @{name}",