Add "Follows you" badge to AccountListItem component (#38828)
This commit is contained in:
parent
6c5bd4f9a8
commit
ffd7160980
3
app/javascript/images/icons/icon_clock.svg
Normal file
3
app/javascript/images/icons/icon_clock.svg
Normal 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 |
@ -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,
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
|
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 'mastodon/components/account';
|
import { FollowsYouBadge, VerifiedBadge } from 'mastodon/components/badge';
|
||||||
import { VerifiedBadge } from 'mastodon/components/badge';
|
|
||||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
import { useRelationship } from 'mastodon/hooks/useRelationship';
|
import { useRelationship } from 'mastodon/hooks/useRelationship';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
import type { Relationship } from 'mastodon/models/relationship';
|
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 { 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 +34,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 +54,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 +71,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') && (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
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 type { OnAttributeHandler } from '@/mastodon/utils/html';
|
import type { OnAttributeHandler } from '@/mastodon/utils/html';
|
||||||
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
|
import BlockIcon from '@/material-icons/400-24px/block.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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -7,9 +7,8 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import Overlay from 'react-overlays/esm/Overlay';
|
import Overlay from 'react-overlays/esm/Overlay';
|
||||||
|
|
||||||
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
|
|
||||||
import { showAlert } from '@/mastodon/actions/alerts';
|
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 { Button } from '@/mastodon/components/button';
|
||||||
import { DisplayName } from '@/mastodon/components/display_name';
|
import { DisplayName } from '@/mastodon/components/display_name';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
@ -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
|
||||||
|
|||||||
@ -37,10 +37,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.followerBadgeIcon {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badges {
|
.badges {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,6 +131,7 @@
|
|||||||
"account.note.edit_button": "Edit",
|
"account.note.edit_button": "Edit",
|
||||||
"account.note.title": "Personal note (visible only to you)",
|
"account.note.title": "Personal note (visible only to you)",
|
||||||
"account.open_original_page": "Open original page",
|
"account.open_original_page": "Open original page",
|
||||||
|
"account.pending": "Pending",
|
||||||
"account.posts": "Posts",
|
"account.posts": "Posts",
|
||||||
"account.remove_from_followers": "Remove {name} from followers",
|
"account.remove_from_followers": "Remove {name} from followers",
|
||||||
"account.report": "Report @{name}",
|
"account.report": "Report @{name}",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user