[Glitch] Profile redesign: Adds a "Follows you" badge

Port 759e97fd36c4119f599d82af5604c4122b2379ad to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo 2026-04-03 11:39:29 +02:00 committed by Claire
parent da85a16692
commit c6f9a60e1f
12 changed files with 128 additions and 119 deletions

View File

@ -2,14 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import CelebrationIcon from '@/material-icons/400-24px/celebration-fill.svg?react'; import CelebrationIcon from '@/material-icons/400-24px/celebration-fill.svg?react';
import * as badges from './badge'; import * as badges from '.';
const meta = { const meta = {
component: badges.Badge, component: badges.Badge,
title: 'Components/Badge', title: 'Components/Badge',
args: { args: {
domain: '',
label: undefined, label: undefined,
}, },
argTypes: {
domain: {
control: 'text',
},
},
} satisfies Meta<typeof badges.Badge>; } satisfies Meta<typeof badges.Badge>;
export default meta; export default meta;

View File

@ -11,28 +11,38 @@ import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react'; import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import classes from './styles.module.scss';
interface BadgeProps { interface BadgeProps {
label: ReactNode; label: ReactNode;
icon?: ReactNode; icon?: ReactNode;
className?: string; className?: string;
domain?: ReactNode; domain?: ReactNode;
roleId?: string; roleId?: string;
variant?:
| 'default'
| 'subtle'
| 'inverted'
| 'success'
| 'warning'
| 'danger';
} }
export const Badge: FC<BadgeProps> = ({ export const Badge: FC<BadgeProps> = ({
icon = <PersonIcon />, icon = <PersonIcon />,
variant = 'default',
label, label,
className, className,
domain, domain,
roleId, roleId,
}) => ( }) => (
<div <div
className={classNames('account-role', className)} className={classNames(classes.badge, classes[variant], className)}
data-account-role-id={roleId} data-account-role-id={roleId}
> >
{icon} {icon}
<span>{label}</span> <span>{label}</span>
{domain && <span className='account-role__domain'>{domain}</span>} {domain && <span className={classes.domain}>{domain}</span>}
</div> </div>
); );
@ -89,6 +99,7 @@ export const MutedBadge: FC<
return ( return (
<Badge <Badge
icon={<VolumeOffIcon />} icon={<VolumeOffIcon />}
variant='inverted'
label={ label={
label ?? label ??
(formattedDate ? ( (formattedDate ? (
@ -111,6 +122,7 @@ export const MutedBadge: FC<
export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => ( export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
<Badge <Badge
icon={<BlockIcon />} icon={<BlockIcon />}
variant='danger'
label={ label={
label ?? ( label ?? (
<FormattedMessage <FormattedMessage

View File

@ -0,0 +1,55 @@
.badge {
color: var(--color-text-primary);
font-size: 13px;
display: inline-flex;
padding: 4px;
padding-inline-end: 8px;
gap: 4px;
border-radius: 6px;
align-items: center;
> svg {
width: auto;
height: 15px;
fill: currentColor;
opacity: 0.85;
}
}
.domain {
opacity: 0.75;
letter-spacing: 0;
}
.default {
background-color: var(--color-bg-secondary);
}
.subtle {
background-color: var(--color-bg-brand-softest);
}
.feature {
background-color: var(--color-bg-brand-base);
color: var(--color-text-on-brand-base);
}
.inverted {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
}
.success {
background-color: var(--color-bg-success-softest);
color: var(--color-text-success);
}
.warning {
background-color: var(--color-bg-warning-softest);
color: var(--color-text-warning);
}
.danger {
background-color: var(--color-bg-error-base);
color: var(--color-text-on-error-base);
}

View File

@ -161,7 +161,7 @@ export const AccountHeader: React.FC<{
<div <div
className={classNames( className={classNames(
'account__header__tabs__name', 'account__header__tabs__name',
redesignClasses.nameWrapper, redesignClasses.displayNameWrapper,
)} )}
> >
<AccountName accountId={accountId} /> <AccountName accountId={accountId} />

View File

@ -7,10 +7,13 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay'; import Overlay from 'react-overlays/esm/Overlay';
import { Badge } from '@/flavours/glitch/components/badge';
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 { useAppSelector } from '@/flavours/glitch/store'; import { 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 HelpIcon from '@/material-icons/400-24px/help.svg?react'; import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react'; import DomainIcon from '@/material-icons/400-24px/language.svg?react';
@ -35,6 +38,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const localDomain = useAppSelector( const localDomain = useAppSelector(
(state) => state.meta.get('domain') as string, (state) => state.meta.get('domain') as string,
); );
const relationship = useRelationship(accountId);
if (!account) { if (!account) {
return null; return null;
@ -43,10 +47,23 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const [username = '', domain = localDomain] = account.acct.split('@'); const [username = '', domain = localDomain] = account.acct.split('@');
return ( return (
<div className={classes.name}> <div className={classes.nameWrapper}>
<h1> <div className={classes.name}>
<DisplayName account={account} variant='simple' /> <h1>
</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'
/>
}
/>
)}
</div>
<p className={classes.username}> <p className={classes.username}>
@{username}@{domain} @{username}@{domain}
<AccountNameHelp <AccountNameHelp

View File

@ -3,8 +3,6 @@ import type { FC } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { fetchRelationships } from '@/flavours/glitch/actions/accounts'; import { fetchRelationships } from '@/flavours/glitch/actions/accounts';
import { import {
AdminBadge, AdminBadge,
@ -14,13 +12,9 @@ import {
GroupBadge, GroupBadge,
MutedBadge, MutedBadge,
} from '@/flavours/glitch/components/badge'; } from '@/flavours/glitch/components/badge';
import { Icon } from '@/flavours/glitch/components/icon';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import type { AccountRole } from '@/flavours/glitch/models/account'; import type { AccountRole } from '@/flavours/glitch/models/account';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import IconPinned from '@/images/icons/icon_pinned.svg?react';
import classes from './redesign.module.scss';
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
const account = useAccount(accountId); const account = useAccount(accountId);
@ -53,7 +47,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<AdminBadge <AdminBadge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={classes.badge}
domain={`(${domain})`} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
@ -63,7 +56,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<Badge <Badge
key={role.id} key={role.id}
label={role.name} label={role.name}
className={classes.badge}
domain={`(${domain})`} domain={`(${domain})`}
roleId={role.id} roleId={role.id}
/>, />,
@ -72,25 +64,19 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
}); });
if (account.bot) { if (account.bot) {
badges.push(<AutomatedBadge key='bot-badge' className={classes.badge} />); badges.push(<AutomatedBadge key='bot-badge' />);
} }
if (account.group) { if (account.group) {
badges.push(<GroupBadge key='group-badge' className={classes.badge} />); badges.push(<GroupBadge key='group-badge' />);
} }
if (relationship) { if (relationship) {
if (relationship.blocking) { if (relationship.blocking) {
badges.push( badges.push(<BlockedBadge key='blocking' />);
<BlockedBadge
key='blocking'
className={classNames(classes.badge, classes.badgeBlocked)}
/>,
);
} }
if (relationship.domain_blocking) { if (relationship.domain_blocking) {
badges.push( badges.push(
<BlockedBadge <BlockedBadge
key='domain-blocking' key='domain-blocking'
className={classNames(classes.badge, classes.badgeBlocked)}
domain={domain} domain={domain}
label={ label={
<FormattedMessage <FormattedMessage
@ -105,7 +91,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push( badges.push(
<MutedBadge <MutedBadge
key='muted-badge' key='muted-badge'
className={classNames(classes.badge, classes.badgeMuted)}
expiresAt={relationship.muting_expires_at} expiresAt={relationship.muting_expires_at}
/>, />,
); );
@ -119,16 +104,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return <div className={'account__header__badges'}>{badges}</div>; return <div className={'account__header__badges'}>{badges}</div>;
}; };
export const PinnedBadge: FC = () => (
<Badge
className={classes.badge}
icon={<Icon id='pinned' icon={IconPinned} />}
label={
<FormattedMessage id='account.timeline.pinned' defaultMessage='Pinned' />
}
/>
);
function isAdminBadge(role: AccountRole) { function isAdminBadge(role: AccountRole) {
const name = role.name.toLowerCase(); const name = role.name.toLowerCase();
return name === 'admin' || name === 'owner'; return name === 'admin' || name === 'owner';

View File

@ -17,14 +17,20 @@
padding-top: 0; padding-top: 0;
} }
.nameWrapper { .displayNameWrapper {
display: flex; display: flex;
align-items: start; align-items: start;
gap: 16px; gap: 16px;
} }
.name { .nameWrapper {
flex-grow: 1; flex-grow: 1;
}
.name {
display: flex;
gap: 2px;
align-items: baseline;
> h1 { > h1 {
font-size: 22px; font-size: 22px;
@ -33,6 +39,10 @@
} }
} }
.followerBadgeIcon {
color: var(--color-text-secondary);
}
.username { .username {
display: flex; display: flex;
font-size: 13px; font-size: 13px;
@ -165,38 +175,6 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
font-size: 15px; font-size: 15px;
} }
.badge {
background-color: var(--color-bg-secondary);
border: none;
color: var(--color-text-secondary);
font-weight: 500;
padding: 4px;
font-size: 13px;
:global(.account__header__badges) > & {
line-height: 1;
}
> span {
font-weight: unset;
opacity: 1;
}
}
.badgeMuted {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
}
.badgeBlocked {
background-color: var(--color-bg-error-base);
color: var(--color-text-on-error-base);
}
svg.badgeIcon {
opacity: 1;
}
.note { .note {
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@ -10,6 +10,7 @@ import {
expandTimelineByKey, expandTimelineByKey,
timelineKey, timelineKey,
} from '@/flavours/glitch/actions/timelines_typed'; } from '@/flavours/glitch/actions/timelines_typed';
import { Badge } from '@/flavours/glitch/components/badge';
import { Button } from '@/flavours/glitch/components/button'; import { Button } from '@/flavours/glitch/components/button';
import { Icon } from '@/flavours/glitch/components/icon'; import { Icon } from '@/flavours/glitch/components/icon';
import { StatusHeader } from '@/flavours/glitch/components/status/header'; import { StatusHeader } from '@/flavours/glitch/components/status/header';
@ -18,8 +19,6 @@ import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import IconPinned from '@/images/icons/icon_pinned.svg?react'; import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { PinnedBadge } from '../components/badges';
import { useAccountContext } from './context'; import { useAccountContext } from './context';
import classes from './styles.module.scss'; import classes from './styles.module.scss';
@ -79,7 +78,16 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
} }
return ( return (
<StatusHeader {...args} className={classes.pinnedStatusHeader}> <StatusHeader {...args} className={classes.pinnedStatusHeader}>
<PinnedBadge /> <Badge
className={classes.pinnedBadge}
icon={<Icon id='pinned' icon={IconPinned} />}
label={
<FormattedMessage
id='account.timeline.pinned'
defaultMessage='Pinned'
/>
}
/>
</StatusHeader> </StatusHeader>
); );
}; };

View File

@ -118,8 +118,8 @@
> :global(.status__display-name) { > :global(.status__display-name) {
grid-row: span 2; grid-row: span 2;
} }
}
> :global(.account-role) {
justify-self: end; .pinnedBadge {
} justify-self: end;
} }

View File

@ -226,34 +226,6 @@
} }
} }
.account-role {
display: inline-flex;
padding: 4px;
padding-inline-end: 8px;
border: 1px solid var(--color-border-brand);
color: var(--color-text-brand);
font-weight: 500;
font-size: 12px;
letter-spacing: 0.5px;
line-height: 16px;
gap: 4px;
border-radius: 6px;
align-items: center;
svg {
width: auto;
height: 15px;
opacity: 0.85;
fill: currentColor;
}
&__domain {
font-weight: 400;
opacity: 0.75;
letter-spacing: 0;
}
}
.simple_form .not_recommended { .simple_form .not_recommended {
color: var(--color-text-error); color: var(--color-text-error);
background-color: var(--color-bg-error-softest); background-color: var(--color-bg-error-softest);

View File

@ -1124,10 +1124,6 @@ a.name-tag,
&:hover { &:hover {
color: var(--color-text-brand); color: var(--color-text-brand);
} }
.account-role {
vertical-align: middle;
}
} }
.icon { .icon {

View File

@ -8739,16 +8739,6 @@ noscript {
} }
} }
&__badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
.account-role {
line-height: unset;
}
}
&__tabs { &__tabs {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;