diff --git a/app/javascript/mastodon/components/hover_card_account.stories.tsx b/app/javascript/mastodon/components/hover_card_account.stories.tsx new file mode 100644 index 00000000000..58c9bb682bf --- /dev/null +++ b/app/javascript/mastodon/components/hover_card_account.stories.tsx @@ -0,0 +1,150 @@ +import { Map as ImmutableMap } from 'immutable'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { accountFactory, accountFactoryState } from '@/testing/factories'; + +import { HoverCardAccount } from './hover_card_account'; + +const meta = { + title: 'Components/HoverCardAccount', + component: HoverCardAccount, + argTypes: { + accountId: { + type: 'string', + description: 'ID of the account to display in the hover card', + }, + }, + args: { + accountId: '1', + }, + decorators: [ + (Story) => ( +
+

+ Hover card examples - demonstrating Issue #35623 fix for moved + accounts +

+ +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +// Mock data for different account states +const regularAccount = accountFactoryState({ + id: '1', + username: 'alice', + acct: 'alice@mastodon.social', + display_name: 'Alice Johnson', + note: 'Frontend developer who loves building amazing user interfaces. Coffee enthusiast ☕', + followers_count: 1250, + following_count: 342, + statuses_count: 1840, + avatar: 'https://picsum.photos/200/200?random=1', + header: 'https://picsum.photos/600/200?random=1', + fields: [ + { name: 'Website', value: 'https://alice.dev', verified_at: null }, + { name: 'Location', value: 'San Francisco, CA', verified_at: null }, + ], +}); + +const movedAccount = accountFactoryState({ + id: '2', + username: 'bob_old', + acct: 'bob_old@mastodon.social', + display_name: 'Bob Smith (Moved)', + note: 'I have moved to a new account. Please follow me there!', + followers_count: 890, + following_count: 156, + statuses_count: 420, + avatar: 'https://picsum.photos/200/200?random=2', + header: 'https://picsum.photos/600/200?random=2', + moved: accountFactory({ + id: '3', + username: 'bob_new', + acct: 'bob_new@social.example', + display_name: 'Bob Smith', + followers_count: 950, + following_count: 180, + statuses_count: 45, + avatar: 'https://picsum.photos/200/200?random=3', + header: 'https://picsum.photos/600/200?random=3', + }), +}); + +const newAccount = accountFactoryState({ + id: '3', + username: 'bob_new', + acct: 'bob_new@social.example', + display_name: 'Bob Smith', + note: 'This is my new account! Thanks for following me here.', + followers_count: 950, + following_count: 180, + statuses_count: 45, + avatar: 'https://picsum.photos/200/200?random=3', + header: 'https://picsum.photos/600/200?random=3', +}); + +export const RegularAccount: Story = { + args: { + accountId: '1', + }, + parameters: { + state: { + accounts: ImmutableMap({ + '1': regularAccount, + }), + relationships: ImmutableMap(), + meta: ImmutableMap({ + locale: 'en', + emoji_style: 'unicode', + }), + }, + }, +}; + +export const MovedAccount: Story = { + args: { + accountId: '2', + }, + parameters: { + state: { + accounts: ImmutableMap({ + '2': movedAccount, + '3': newAccount, + }), + relationships: ImmutableMap(), + meta: ImmutableMap({ + locale: 'en', + emoji_style: 'unicode', + }), + }, + }, +}; + +export const LoadingState: Story = { + args: { + accountId: '999', + }, + parameters: { + state: { + accounts: ImmutableMap(), + relationships: ImmutableMap(), + meta: ImmutableMap({ + locale: 'en', + emoji_style: 'unicode', + }), + }, + }, +}; diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx index a5a5e4c9575..a01907ec8af 100644 --- a/app/javascript/mastodon/components/hover_card_account.tsx +++ b/app/javascript/mastodon/components/hover_card_account.tsx @@ -32,6 +32,11 @@ export const HoverCardAccount = forwardRef< const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, ); + // Get the moved account data if this account has moved + // This allows us to show proper follow button for the new account + const movedAccount = useAppSelector((state) => + account?.moved ? state.accounts.get(account.moved) : undefined, + ); const suspended = account?.suspended; const hidden = useAppSelector((state) => accountId ? getAccountHidden(state, accountId) : undefined, @@ -98,6 +103,28 @@ export const HoverCardAccount = forwardRef< /> )} + ) : account.moved ? ( + // Issue #35623: Show moved account notice instead of regular content + // This prevents following the old account and provides a link + follow button for the new account +
+ + @{movedAccount.acct} + + ) : ( + @{account.moved} + ), + }} + /> + {/* Follow button that follows the NEW account, not the old one */} + {movedAccount && ( + + )} +
) : ( <>
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f2f23e60fa2..fccc79ca483 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -58,6 +58,7 @@ "account.media": "Media", "account.mention": "Mention @{name}", "account.moved_to": "{name} has indicated that their new account is now:", + "account.moved_to_short": "This account has moved to {acct}", "account.mute": "Mute @{name}", "account.mute_notifications_short": "Mute notifications", "account.mute_short": "Mute", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 80f8e5cb02a..1772abe987a 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -11214,6 +11214,26 @@ noscript { font-weight: 500; } + &__moved-notice { + text-align: center; + font-weight: 500; + color: $secondary-text-color; + display: flex; + flex-direction: column; + gap: 12px; + + a { + color: inherit; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + } + .display-name { font-size: 15px; line-height: 22px;