From 8da2fae6ccd50158171a7caa52a809da2e146c26 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 2 Apr 2026 14:33:32 +0200 Subject: [PATCH] Profile redesign: Account featured tab (#38529) Co-authored-by: diondiondion --- app/javascript/images/elephant_ui_dark.svg | 1 + app/javascript/images/elephant_ui_light.svg | 1 + .../empty_state/empty_state.module.scss | 7 + .../mastodon/components/empty_state/index.tsx | 17 ++- .../components/empty_message.tsx | 120 ++++++++++++++---- .../components/featured_tag.tsx | 52 -------- .../features/account_featured/index.tsx | 48 +------ .../account_timeline/components/tabs.tsx | 11 +- app/javascript/mastodon/hooks/useTheme.ts | 23 ++++ app/javascript/mastodon/locales/en.json | 13 +- 10 files changed, 158 insertions(+), 135 deletions(-) create mode 100644 app/javascript/images/elephant_ui_dark.svg create mode 100644 app/javascript/images/elephant_ui_light.svg delete mode 100644 app/javascript/mastodon/features/account_featured/components/featured_tag.tsx create mode 100644 app/javascript/mastodon/hooks/useTheme.ts diff --git a/app/javascript/images/elephant_ui_dark.svg b/app/javascript/images/elephant_ui_dark.svg new file mode 100644 index 0000000000..aa91b704c7 --- /dev/null +++ b/app/javascript/images/elephant_ui_dark.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/elephant_ui_light.svg b/app/javascript/images/elephant_ui_light.svg new file mode 100644 index 0000000000..1eeab31cf4 --- /dev/null +++ b/app/javascript/images/elephant_ui_light.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/mastodon/components/empty_state/empty_state.module.scss b/app/javascript/mastodon/components/empty_state/empty_state.module.scss index 1707b3bc08..bdab2f2732 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.module.scss +++ b/app/javascript/mastodon/components/empty_state/empty_state.module.scss @@ -10,6 +10,13 @@ } .content { + svg, + img { + width: 240px; + max-width: 100%; + margin-bottom: 16px; + } + h3 { font-size: 17px; font-weight: 500; diff --git a/app/javascript/mastodon/components/empty_state/index.tsx b/app/javascript/mastodon/components/empty_state/index.tsx index 93f034f3e9..59210bbc87 100644 --- a/app/javascript/mastodon/components/empty_state/index.tsx +++ b/app/javascript/mastodon/components/empty_state/index.tsx @@ -9,10 +9,12 @@ import classes from './empty_state.module.scss'; */ export const EmptyState: React.FC<{ - title?: string | React.ReactElement; - message?: string | React.ReactElement; + image?: React.ReactNode; + title?: React.ReactNode; + message?: React.ReactNode; children?: React.ReactNode; }> = ({ + image, title = ( ), @@ -21,10 +23,13 @@ export const EmptyState: React.FC<{ }) => { return (
-
-

{title}

- {!!message &&

{message}

} -
+ {(title || message || image) && ( +
+ {image} + {!!title &&

{title}

} + {!!message &&

{message}

} +
+ )} {children}
diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx index 5a60780a09..a5bb5878c9 100644 --- a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx +++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx @@ -1,9 +1,15 @@ import { FormattedMessage } from 'react-intl'; import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; -import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint'; -import { me } from 'mastodon/initial_state'; +import ElephantDarkImage from '@/images/elephant_ui_dark.svg?react'; +import ElephantLightImage from '@/images/elephant_ui_light.svg?react'; +import { EmptyState } from '@/mastodon/components/empty_state'; +import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint'; +import { areCollectionsEnabled } from '@/mastodon/features/collections/utils'; +import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; +import { useTheme } from '@/mastodon/hooks/useTheme'; interface EmptyMessageProps { suspended: boolean; @@ -19,21 +25,59 @@ export const EmptyMessage: React.FC = ({ blockedBy, }) => { const { acct } = useParams<{ acct?: string }>(); + const me = useCurrentAccountId(); + const theme = useTheme(); + const ElephantImage = + theme === 'dark' ? ElephantDarkImage : ElephantLightImage; + if (!accountId) { return null; } + let title: React.ReactNode = null; let message: React.ReactNode = null; + const hasCollections = areCollectionsEnabled(); + + const image = ; + if (me === accountId) { - message = ( - - ); + if (hasCollections) { + // Return only here to insert the "Create a collection" button as the action for the empty state. + return ( + + } + > + + + + + ); + } else { + title = ( + + ); + message = ( + + ); + } } else if (suspended) { - message = ( + title = ( = ({ } else if (hidden) { message = ; } else if (blockedBy) { - message = ( + title = ( ); - } else if (acct) { - message = ( - - ); } else { - message = ( + // Standard other account empty state. + title = ( ); + if (hasCollections) { + if (acct) { + message = ( + + ); + } else { + message = ( + + ); + } + } else { + if (acct) { + message = ( + + ); + } else { + message = ( + + ); + } + } } - return ( -
- {message} -
- ); + return ; }; diff --git a/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx deleted file mode 100644 index b9a79ce25e..0000000000 --- a/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { defineMessages, useIntl } from 'react-intl'; - -import type { Map as ImmutableMap } from 'immutable'; - -import { Hashtag } from 'mastodon/components/hashtag'; - -export type TagMap = ImmutableMap< - 'id' | 'name' | 'url' | 'statuses_count' | 'last_status_at' | 'accountId', - string | null ->; - -interface FeaturedTagProps { - tag: TagMap; - account: string; -} - -const messages = defineMessages({ - lastStatusAt: { - id: 'account.featured_tags.last_status_at', - defaultMessage: 'Last post on {date}', - }, - empty: { - id: 'account.featured_tags.last_status_never', - defaultMessage: 'No posts', - }, -}); - -export const FeaturedTag: React.FC = ({ tag, account }) => { - const intl = useIntl(); - const name = tag.get('name') ?? ''; - const count = Number.parseInt(tag.get('statuses_count') ?? ''); - return ( - 0 - ? intl.formatMessage(messages.lastStatusAt, { - date: intl.formatDate(tag.get('last_status_at') ?? '', { - month: 'short', - day: '2-digit', - year: 'numeric', - }), - }) - : intl.formatMessage(messages.empty) - } - /> - ); -}; diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index 937a200e1c..244302f209 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -8,7 +8,6 @@ import { List as ImmutableList } from 'immutable'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { fetchEndorsedAccounts } from 'mastodon/actions/accounts'; -import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { Account } from 'mastodon/components/account'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; @@ -33,8 +32,6 @@ import { CollectionListItem } from '../collections/detail/collection_list_item'; import { areCollectionsEnabled } from '../collections/utils'; import { EmptyMessage } from './components/empty_message'; -import { FeaturedTag } from './components/featured_tag'; -import type { TagMap } from './components/featured_tag'; const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ multiColumn, @@ -55,26 +52,15 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ useEffect(() => { if (accountId) { - void dispatch(fetchFeaturedTags({ accountId })); void dispatch(fetchEndorsedAccounts({ accountId })); + if (areCollectionsEnabled()) { void dispatch(fetchAccountCollections({ accountId })); } } }, [accountId, dispatch]); - const isLoading = useAppSelector( - (state) => - !accountId || - !!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']), - ); - const featuredTags = useAppSelector( - (state) => - state.user_lists.getIn( - ['featured_tags', accountId, 'items'], - ImmutableList(), - ) as ImmutableList, - ); + const isLoading = !accountId; const featuredAccountIds = useAppSelector( (state) => state.user_lists.getIn( @@ -106,13 +92,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ ); } - const noTags = featuredTags.isEmpty(); - - if ( - noTags && - featuredAccountIds.isEmpty() && - listedCollections.length === 0 - ) { + if (featuredAccountIds.isEmpty() && listedCollections.length === 0) { return ( = ({ )} - {!noTags && ( - <> -

- -

- - {featuredTags.map((tag, index) => ( -
- -
- ))} -
- - )} {!featuredAccountIds.isEmpty() && ( <>

diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx index b2ad9a6065..05dbdb5a80 100644 --- a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -8,6 +8,8 @@ import { NavLink } from 'react-router-dom'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccountId } from '@/mastodon/hooks/useAccountId'; +import { areCollectionsEnabled } from '../../collections/utils'; + import classes from './redesign.module.scss'; const isActive: Required['isActive'] = (match, location) => @@ -39,7 +41,14 @@ export const AccountTabs: FC = () => { )} {show_featured && ( - + {areCollectionsEnabled() ? ( + + ) : ( + + )} )} diff --git a/app/javascript/mastodon/hooks/useTheme.ts b/app/javascript/mastodon/hooks/useTheme.ts new file mode 100644 index 0000000000..9c67efaf58 --- /dev/null +++ b/app/javascript/mastodon/hooks/useTheme.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; + +import { isDarkMode } from '../utils/theme'; + +export function useTheme() { + const [darkMode, setDarkMode] = useState(() => isDarkMode()); + + useEffect(() => { + const mutationObserver = new MutationObserver(() => { + setDarkMode(isDarkMode()); + }); + mutationObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-color-scheme'], + }); + + return () => { + mutationObserver.disconnect(); + }; + }, []); + + return darkMode ? 'dark' : 'light'; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index daf7d28f91..c0b5b53ed8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -45,9 +45,6 @@ "account.featured": "Featured", "account.featured.accounts": "Profiles", "account.featured.collections": "Collections", - "account.featured.hashtags": "Hashtags", - "account.featured_tags.last_status_at": "Last post on {date}", - "account.featured_tags.last_status_never": "No posts", "account.field_overflow": "Show full content", "account.filters.all": "All activity", "account.filters.boosts_toggle": "Show boosts", @@ -606,9 +603,15 @@ "emoji_button.search_results": "Search results", "emoji_button.symbols": "Symbols", "emoji_button.travel": "Travel & Places", - "empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?", "empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?", - "empty_column.account_featured_other.unknown": "This account has not featured anything yet.", + "empty_column.account_featured_other.no_collections_desc": "{acct} hasn’t created any collections yet.", + "empty_column.account_featured_other.title": "Nothing to see here", + "empty_column.account_featured_self.no_collections": "No collections yet", + "empty_column.account_featured_self.no_collections_button": "Create a collection", + "empty_column.account_featured_self.pre_collections": "Stay tuned for Collections", + "empty_column.account_featured_self.pre_collections_desc": "Collections (coming in Mastodon 4.6) allows you to create your own curated lists of accounts to recommend to others.", + "empty_column.account_featured_unknown.no_collections_desc": "This account hasn’t created any collections yet.", + "empty_column.account_featured_unknown.other": "This account hasn’t featured anything yet.", "empty_column.account_hides_collections": "This user has chosen to not make this information available", "empty_column.account_suspended": "Account suspended", "empty_column.account_timeline": "No posts here!",