From 047338e684a3b8fb89080b3ecbbd2f7c831e9c16 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 16 Jan 2026 13:44:49 +0100 Subject: [PATCH] Profile fields redesign (#37513) --- .../mastodon/components/mini_card/list.tsx | 36 ++++--- .../mini_card/mini_card.stories.tsx | 35 ++++--- .../components/mini_card/styles.module.css | 14 +-- .../features/account_timeline/common.ts | 5 + .../components/account_header.tsx | 33 +------ .../account_timeline/components/fields.tsx | 97 +++++++++++++++++++ .../components/fields_modal.tsx | 80 +++++++++++++++ .../account_timeline/components/links.tsx | 58 ----------- .../components/number_fields.tsx | 94 ++++++++++++++++++ .../components/redesign.module.scss | 47 +++++++++ .../features/ui/components/modal_root.jsx | 1 + app/javascript/mastodon/locales/en.json | 5 +- 12 files changed, 386 insertions(+), 119 deletions(-) create mode 100644 app/javascript/mastodon/features/account_timeline/common.ts create mode 100644 app/javascript/mastodon/features/account_timeline/components/fields.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx delete mode 100644 app/javascript/mastodon/features/account_timeline/components/links.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/number_fields.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/redesign.module.scss diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx index b5b8fbc2c8..f775e70aac 100644 --- a/app/javascript/mastodon/components/mini_card/list.tsx +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -11,11 +11,13 @@ import classes from './styles.module.css'; interface MiniCardListProps { cards?: (Pick & { key?: Key })[]; + className?: string; onOverflowClick?: MouseEventHandler; } export const MiniCardList: FC = ({ cards = [], + className, onOverflowClick, }) => { const { @@ -27,29 +29,37 @@ export const MiniCardList: FC = ({ maxWidth, } = useOverflow(); + if (!cards.length) { + return null; + } + return ( -
+
{cards.map((card, index) => (
- + {cards.length > 1 && ( +
+ +
+ )}
); }; diff --git a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx index ada76011b2..60534f05f6 100644 --- a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx +++ b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx @@ -7,18 +7,6 @@ const meta = { title: 'Components/MiniCard', component: MiniCardList, args: { - cards: [ - { label: 'Pronouns', value: 'they/them' }, - { - label: 'Website', - value: bowie-the-db.meow, - }, - { - label: 'Free playlists', - value: soundcloud.com, - }, - { label: 'Location', value: 'Purris, France' }, - ], onOverflowClick: action('Overflow clicked'), }, render(args) { @@ -43,7 +31,22 @@ export default meta; type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + args: { + cards: [ + { label: 'Pronouns', value: 'they/them' }, + { + label: 'Website', + value: bowie-the-db.meow, + }, + { + label: 'Free playlists', + value: soundcloud.com, + }, + { label: 'Location', value: 'Purris, France' }, + ], + }, +}; export const LongValue: Story = { args: { @@ -60,3 +63,9 @@ export const LongValue: Story = { ], }, }; + +export const OneCard: Story = { + args: { + cards: [{ label: 'Pronouns', value: 'they/them' }], + }, +}; diff --git a/app/javascript/mastodon/components/mini_card/styles.module.css b/app/javascript/mastodon/components/mini_card/styles.module.css index d912f1e5cf..642c08c5fa 100644 --- a/app/javascript/mastodon/components/mini_card/styles.module.css +++ b/app/javascript/mastodon/components/mini_card/styles.module.css @@ -6,7 +6,6 @@ } .list { - min-width: 0; display: flex; gap: 4px; overflow: hidden; @@ -21,16 +20,19 @@ flex-shrink: 0; } -.card { - max-width: 20vw; - overflow: hidden; -} - .more { color: var(--color-text-secondary); font-weight: 600; appearance: none; background: none; + aspect-ratio: 1; + height: 100%; + transition: all 300ms linear; +} + +.more:hover { + background-color: var(--color-bg-brand-softer); + color: var(--color-text-primary); } .hidden { diff --git a/app/javascript/mastodon/features/account_timeline/common.ts b/app/javascript/mastodon/features/account_timeline/common.ts new file mode 100644 index 0000000000..ee76c25bb7 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/common.ts @@ -0,0 +1,5 @@ +import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; + +export function isRedesignEnabled() { + return isClientFeatureEnabled('profile_redesign'); +} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 4244d7f702..8fc89d206b 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -1,18 +1,16 @@ import { useCallback } from 'react'; -import { useIntl, FormattedMessage } from 'react-intl'; +import { useIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { AccountBio } from '@/mastodon/components/account_bio'; -import { AccountFields } from '@/mastodon/components/account_fields'; import { DisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import { openModal } from 'mastodon/actions/modal'; import { Avatar } from 'mastodon/components/avatar'; -import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { Icon } from 'mastodon/components/icon'; import { AccountNote } from 'mastodon/features/account/components/account_note'; import { DomainPill } from 'mastodon/features/account/components/domain_pill'; @@ -25,10 +23,11 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { AccountBadges } from './badges'; import { AccountButtons } from './buttons'; import { FamiliarFollowers } from './familiar_followers'; +import { AccountHeaderFields } from './fields'; import { AccountInfo } from './info'; -import { AccountLinks } from './links'; import { MemorialNote } from './memorial_note'; import { MovedNote } from './moved_note'; +import { AccountNumberFields } from './number_fields'; import { AccountTabs } from './tabs'; const titleFromAccount = (account: Account) => { @@ -192,32 +191,10 @@ export const AccountHeader: React.FC<{ className='account__header__content' /> -
-
-
- -
-
- -
-
- - -
+
- + )} diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx new file mode 100644 index 0000000000..a73d92c1b6 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -0,0 +1,97 @@ +import { useCallback, useMemo } from 'react'; +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { openModal } from '@/mastodon/actions/modal'; +import { AccountFields } from '@/mastodon/components/account_fields'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { MiniCardList } from '@/mastodon/components/mini_card/list'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { Account } from '@/mastodon/models/account'; +import { useAppDispatch } from '@/mastodon/store'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountHeaderFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const account = useAccount(accountId); + + if (!account) { + return null; + } + + if (isRedesignEnabled()) { + return ; + } + + return ( +
+
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { + const htmlHandlers = useElementHandledLink(); + const cards = useMemo( + () => + account.fields.toArray().map(({ value_emojified, name_emojified }) => ({ + label: ( + + ), + value: ( + + ), + })), + [account.emojis, account.fields, htmlHandlers], + ); + + const dispatch = useAppDispatch(); + const handleOverflowClick = useCallback(() => { + dispatch( + openModal({ + modalType: 'ACCOUNT_FIELDS', + modalProps: { accountId: account.id }, + }), + ); + }, [account.id, dispatch]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx new file mode 100644 index 0000000000..715f6097f4 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx @@ -0,0 +1,80 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import { DisplayName } from '@/mastodon/components/display_name'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +import classes from './redesign.module.scss'; + +export const AccountFieldsModal: FC<{ + accountId: string; + onClose: () => void; +}> = ({ accountId, onClose }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const htmlHandlers = useElementHandledLink(); + + if (!account) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + , + }} + /> + +
+
+ +
+ {account.fields.map((field, index) => ( +
+ + +
+ ))} +
+
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/links.tsx b/app/javascript/mastodon/features/account_timeline/components/links.tsx deleted file mode 100644 index 2e056e4e57..0000000000 --- a/app/javascript/mastodon/features/account_timeline/components/links.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { FC } from 'react'; - -import { useIntl } from 'react-intl'; - -import { NavLink } from 'react-router-dom'; - -import { - FollowersCounter, - FollowingCounter, - StatusesCounter, -} from '@/mastodon/components/counters'; -import { ShortNumber } from '@/mastodon/components/short_number'; -import { useAccount } from '@/mastodon/hooks/useAccount'; - -export const AccountLinks: FC<{ accountId: string }> = ({ accountId }) => { - const intl = useIntl(); - const account = useAccount(accountId); - - if (!account) { - return null; - } - - return ( -
- - - - - - - - - - - -
- ); -}; diff --git a/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx new file mode 100644 index 0000000000..20df6a0125 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; +import { NavLink } from 'react-router-dom'; + +import { + FollowersCounter, + FollowingCounter, + StatusesCounter, +} from '@/mastodon/components/counters'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { ShortNumber } from '@/mastodon/components/short_number'; +import { useAccount } from '@/mastodon/hooks/useAccount'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountNumberFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const intl = useIntl(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + return ( +
+ {!isRedesignEnabled() && ( + + + + )} + + + + + + + + + + {isRedesignEnabled() && ( + + + + + ), + }} + /> + + )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss new file mode 100644 index 0000000000..dd09f199e5 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -0,0 +1,47 @@ +.fieldList { + margin-top: 16px; +} + +.fieldNumbersWrapper { + a { + font-weight: unset; + } +} + +.modalCloseButton { + padding: 8px; + border-radius: 50%; + border: 1px solid var(--color-border-primary); +} + +.modalTitle { + flex-grow: 1; + text-align: center; +} + +.modalFieldsList { + padding: 24px; +} + +.modalFieldItem { + &:not(:first-child) { + padding-top: 12px; + } + + &:not(:last-child)::after { + content: ''; + display: block; + border-bottom: 1px solid var(--color-border-primary); + margin-top: 12px; + } + + dt { + color: var(--color-text-secondary); + font-size: 13px; + } + + dd { + font-weight: 600; + font-size: 15px; + } +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 6f9b23042d..0e718747d9 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -85,6 +85,7 @@ export const MODAL_COMPONENTS = { 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, 'ANNUAL_REPORT': AnnualReportModal, 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), + 'ACCOUNT_FIELDS': () => import('mastodon/features/account_timeline/components/fields_modal.tsx').then(module => ({ default: module.AccountFieldsModal })), }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 77dd953887..afccb03857 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -90,6 +91,8 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_fields_modal.close": "Close", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", - "minicard.more_items": "+ {count} more", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options",