[Glitch] Add "Follows you" badge to AccountListItem component

Port ffd7160980a98111323c8608a3e557b7fba00c59 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-04-28 17:18:02 +02:00 committed by Claire
parent 11f3393825
commit 20e9618e6d
7 changed files with 103 additions and 49 deletions

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,26 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Account } from 'flavours/glitch/components/account';
import { VerifiedBadge } from 'flavours/glitch/components/badge';
import {
FollowsYouBadge,
VerifiedBadge,
} from 'flavours/glitch/components/badge';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { useRelationship } from 'flavours/glitch/hooks/useRelationship';
import { domain } from 'flavours/glitch/initial_state';
import type { Relationship } from 'flavours/glitch/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 +37,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 +57,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 +74,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

@ -6,6 +6,8 @@ import classNames from 'classnames';
import type { OnAttributeHandler } from '@/flavours/glitch/utils/html';
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 BlockIcon from '@/material-icons/400-24px/block.svg?react';
import GroupsIcon from '@/material-icons/400-24px/group.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

@ -8,14 +8,13 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay';
import { showAlert } from '@/flavours/glitch/actions/alerts';
import { Badge } from '@/flavours/glitch/components/badge';
import { FollowsYouBadge } from '@/flavours/glitch/components/badge';
import { Button } from '@/flavours/glitch/components/button';
import { DisplayName } from '@/flavours/glitch/components/display_name';
import { Icon } from '@/flavours/glitch/components/icon';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
@ -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;
}