diff --git a/app/javascript/images/icons/icon_clock.svg b/app/javascript/images/icons/icon_clock.svg new file mode 100644 index 0000000000..72557c82c5 --- /dev/null +++ b/app/javascript/images/icons/icon_clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/mastodon/components/account_list_item/account_list_item.stories.tsx b/app/javascript/mastodon/components/account_list_item/account_list_item.stories.tsx index bad1667b12..62875b02f6 100644 --- a/app/javascript/mastodon/components/account_list_item/account_list_item.stories.tsx +++ b/app/javascript/mastodon/components/account_list_item/account_list_item.stories.tsx @@ -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; 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: , + }, +}; + export const WithBorder: Story = { args: { withBorder: true, diff --git a/app/javascript/mastodon/components/account_list_item/index.tsx b/app/javascript/mastodon/components/account_list_item/index.tsx index 34ae8b0daf..04817cb33e 100644 --- a/app/javascript/mastodon/components/account_list_item/index.tsx +++ b/app/javascript/mastodon/components/account_list_item/index.tsx @@ -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 = ({ 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 = ({ return null; } + const badge = + badgeProp ?? (relationship?.followed_by ? : null); + const firstVerifiedField = account.fields.find((item) => !!item.verified_at); return ( - - - - {renderButton({ accountId, relationship })} - + } + sideContent={ + + {renderButton({ accountId, relationship })} + + } + > + + + {badge && {badge}} + + {stats.includes('followers') && ( diff --git a/app/javascript/mastodon/components/account_list_item/styles.module.scss b/app/javascript/mastodon/components/account_list_item/styles.module.scss index 61d0169bf3..69949508c5 100644 --- a/app/javascript/mastodon/components/account_list_item/styles.module.scss +++ b/app/javascript/mastodon/components/account_list_item/styles.module.scss @@ -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 { diff --git a/app/javascript/mastodon/components/badge/index.tsx b/app/javascript/mastodon/components/badge/index.tsx index 1cc4b531c7..56a6788c3e 100644 --- a/app/javascript/mastodon/components/badge/index.tsx +++ b/app/javascript/mastodon/components/badge/index.tsx @@ -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 = ({ icon = , variant = 'default', @@ -83,13 +90,32 @@ export const GroupBadge: FC> = ({ label, ...props }) => ( /> ); -export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => ( +export const AutomatedBadge: FC = (props) => ( } label={ } - className={className} + {...props} + /> +); + +export const FollowsYouBadge: FC = (props) => ( + } + label={ + + } + {...props} + /> +); + +export const PendingBadge: FC = (props) => ( + } + label={} + {...props} /> ); diff --git a/app/javascript/mastodon/components/badge/styles.module.scss b/app/javascript/mastodon/components/badge/styles.module.scss index d359bc38eb..b216e3eb20 100644 --- a/app/javascript/mastodon/components/badge/styles.module.scss +++ b/app/javascript/mastodon/components/badge/styles.module.scss @@ -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 { diff --git a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx index a1d646ac8b..9f7dc528ea 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx @@ -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 }) => { - {relationship?.followed_by && ( - } - label={ - - } - /> - )} + {relationship?.followed_by && }