[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 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'; import { AccountListItem } from './index';
@ -26,12 +28,30 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {}; export const Default: Story = {};
export const FollowsYou: Story = {
parameters: {
state: {
relationships: {
'1': relationshipsFactory({
followed_by: true,
}),
},
},
},
};
export const WithCustomStats: Story = { export const WithCustomStats: Story = {
args: { args: {
stats: ['posts', 'last-active'], stats: ['posts', 'last-active'],
}, },
}; };
export const WithCustomBadge: Story = {
args: {
badge: <PendingBadge />,
},
};
export const WithBorder: Story = { export const WithBorder: Story = {
args: { args: {
withBorder: true, withBorder: true,

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import classNames from 'classnames';
import type { OnAttributeHandler } from '@/flavours/glitch/utils/html'; import type { OnAttributeHandler } from '@/flavours/glitch/utils/html';
import AdminIcon from '@/images/icons/icon_admin.svg?react'; 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 IconVerified from '@/images/icons/icon_verified.svg?react';
import BlockIcon from '@/material-icons/400-24px/block.svg?react'; import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; import GroupsIcon from '@/material-icons/400-24px/group.svg?react';
@ -32,6 +34,11 @@ interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
| 'danger'; | 'danger';
} }
type PresetBadgeProps = Omit<
BadgeProps,
'label' | 'icon' | 'domain' | 'roleId'
>;
export const Badge: FC<BadgeProps> = ({ export const Badge: FC<BadgeProps> = ({
icon = <PersonIcon />, icon = <PersonIcon />,
variant = 'default', 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 <Badge
icon={<SmartToyIcon />} icon={<SmartToyIcon />}
label={ label={
<FormattedMessage id='account.badges.bot' defaultMessage='Automated' /> <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 { .badge {
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 13px; font-size: 13px;
font-weight: 400;
display: inline-flex; display: inline-flex;
max-width: 100%; max-width: 100%;
padding: 4px; padding: 4px;
@ -61,7 +62,6 @@
.warning { .warning {
background-color: var(--color-bg-warning-softest); background-color: var(--color-bg-warning-softest);
color: var(--color-text-warning);
} }
.danger { .danger {

View File

@ -8,14 +8,13 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay'; import Overlay from 'react-overlays/esm/Overlay';
import { showAlert } from '@/flavours/glitch/actions/alerts'; 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 { Button } from '@/flavours/glitch/components/button';
import { DisplayName } from '@/flavours/glitch/components/display_name'; import { DisplayName } from '@/flavours/glitch/components/display_name';
import { Icon } from '@/flavours/glitch/components/icon'; import { Icon } from '@/flavours/glitch/components/icon';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship'; import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; 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 AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react'; import HelpIcon from '@/material-icons/400-24px/help.svg?react';
@ -60,17 +59,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
<h1> <h1>
<DisplayName account={account} variant='simple' /> <DisplayName account={account} variant='simple' />
</h1> </h1>
{relationship?.followed_by && ( {relationship?.followed_by && <FollowsYouBadge />}
<Badge
icon={<FollowerIcon className={classes.followerBadgeIcon} />}
label={
<FormattedMessage
id='account.follows_you'
defaultMessage='Follows you'
/>
}
/>
)}
</div> </div>
<AccountNameHelp <AccountNameHelp

View File

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