Redesign: Change mobile account menu (#40428)

This commit is contained in:
diondiondion
2026-09-08 13:23:41 +00:00
committed by GitHub
parent 867206c683
commit 5dd2361cdd
7 changed files with 264 additions and 157 deletions

View File

@@ -22,3 +22,10 @@
white-space: normal;
}
}
.profileMenuItem {
--list-item-padding: 0;
--list-item-gap: 8px;
color: inherit;
}

View File

@@ -8,6 +8,7 @@ import {
GearIcon,
CirclesFourIcon,
HeartIcon,
BookmarkSimpleIcon,
UsersThreeIcon,
ProhibitIcon,
GavelIcon,
@@ -17,7 +18,14 @@ import {
import { openModal } from '@/mastodon/actions/modal';
import { Account } from '@/mastodon/components/account';
import { Avatar } from '@/mastodon/components/avatar';
import { IconButton } from '@/mastodon/components/button/redesign';
import { DisplayName } from '@/mastodon/components/display_name';
import { useAccountHandle } from '@/mastodon/components/display_name/default';
import {
ListItemContent,
ListItemWrapper,
} from '@/mastodon/components/list_item';
import {
Menu,
MenuItem,
@@ -37,6 +45,44 @@ import { useAppDispatch } from '@/mastodon/store';
import classes from './account_card_and_menu.module.scss';
export const NavigationAccountCardAndMenu: React.FC = () => {
const { accountId } = useIdentity();
if (!accountId) {
return null;
}
return (
<div className={classes.root}>
<Account
id={accountId}
minimal
withBorder={false}
withMenu={false}
size={32}
/>
<Menu type='navigation'>
<MenuTrigger
as={IconButton}
icon={DotsThreeIcon}
variant='ghost'
size='sm'
>
<FormattedMessage
id='tabs_bar.account_settings'
defaultMessage='Account settings'
/>
</MenuTrigger>
<MenuList placement='top' offset={8}>
<AccountMenuItems />
</MenuList>
</Menu>
</div>
);
};
export const AccountMenuItems: React.FC<{
context?: 'default' | 'mobile';
}> = ({ context = 'default' }) => {
const dispatch = useAppDispatch();
const { accountId, permissions } = useIdentity();
const account = useAccount(accountId);
@@ -55,109 +101,121 @@ export const NavigationAccountCardAndMenu: React.FC = () => {
const accountBasePath = `/@${account?.acct}`;
return (
<div className={classes.root}>
<Account
id={accountId}
minimal
withBorder={false}
withMenu={false}
size={32}
/>
<Menu type='navigation'>
<MenuTrigger
as={IconButton}
icon={DotsThreeIcon}
variant='ghost'
size='sm'
>
<FormattedMessage id='tabs_bar.more' defaultMessage='More' />
</MenuTrigger>
<MenuList placement='top' offset={8}>
<MenuItemLink to='/profile/edit' icon={UserIcon}>
<FormattedMessage
id='account.edit_profile'
defaultMessage='Edit profile'
/>
</MenuItemLink>
<MenuItemLink as='a' href='/settings/preferences' icon={GearIcon}>
<FormattedMessage
id='tabs_bar.settings'
defaultMessage='Settings'
/>
</MenuItemLink>
<>
{context === 'mobile' && <ProfileMenuItem />}
<MenuItemLink to='/profile/edit' icon={UserIcon}>
<FormattedMessage
id='account.edit_profile'
defaultMessage='Edit profile'
/>
</MenuItemLink>
<MenuItemLink as='a' href='/settings/preferences' icon={GearIcon}>
<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />
</MenuItemLink>
<MenuItemDivider />
<MenuItemLink
to={`${accountBasePath}/collections`}
icon={CirclesFourIcon}
>
<FormattedMessage
id='navigation_bar.collections'
defaultMessage='Collections'
/>
</MenuItemLink>
<MenuItemLink to='/favourites' icon={HeartIcon}>
<FormattedMessage
id='navigation_bar.liked_posts'
defaultMessage='Liked Posts'
/>
</MenuItemLink>
{context === 'mobile' && (
<MenuItemLink to='/bookmarks' icon={BookmarkSimpleIcon}>
<FormattedMessage
id='navigation_bar.saved_posts'
defaultMessage='Saved Posts'
/>
</MenuItemLink>
)}
<MenuItemDivider />
<MenuItemLink as='a' href='/relationships' icon={UsersThreeIcon}>
<FormattedMessage
id='navigation_bar.followers_and_following'
defaultMessage='Followers & Following'
/>
</MenuItemLink>
<MenuItemLink to='/blocks' icon={ProhibitIcon}>
<FormattedMessage
id='navigation_bar.blocked_accounts'
defaultMessage='Blocked accounts'
/>
</MenuItemLink>
{(isManager || isAdmin) && (
<>
<MenuItemDivider />
<MenuItemLink
to={`${accountBasePath}/collections`}
icon={CirclesFourIcon}
>
<FormattedMessage
id='navigation_bar.collections'
defaultMessage='Collections'
/>
</MenuItemLink>
<MenuItemLink to='/favourites' icon={HeartIcon}>
<FormattedMessage
id='navigation_bar.liked_posts'
defaultMessage='Liked Posts'
/>
</MenuItemLink>
<MenuItemDivider />
<MenuItemLink as='a' href='/relationships' icon={UsersThreeIcon}>
<FormattedMessage
id='navigation_bar.followers_and_following'
defaultMessage='Followers & Following'
/>
</MenuItemLink>
<MenuItemLink to='/blocks' icon={ProhibitIcon}>
<FormattedMessage
id='navigation_bar.blocked_accounts'
defaultMessage='Blocked accounts'
/>
</MenuItemLink>
{(isManager || isAdmin) && (
<>
<MenuItemDivider />
{isAdmin && (
<MenuItemLink as='a' href='/admin/dashboard' icon={GavelIcon}>
<FormattedMessage
id='navigation_bar.administration'
defaultMessage='Administration'
/>
</MenuItemLink>
)}
{isManager && (
<MenuItemLink
as='a'
href='/admin/reports'
icon={ShieldStarIcon}
>
<FormattedMessage
id='navigation_bar.moderation'
defaultMessage='Moderation'
/>
</MenuItemLink>
)}
</>
{isAdmin && (
<MenuItemLink as='a' href='/admin/dashboard' icon={GavelIcon}>
<FormattedMessage
id='navigation_bar.administration'
defaultMessage='Administration'
/>
</MenuItemLink>
)}
<MenuItemDivider />
{isManager && (
<MenuItemLink as='a' href='/admin/reports' icon={ShieldStarIcon}>
<FormattedMessage
id='navigation_bar.moderation'
defaultMessage='Moderation'
/>
</MenuItemLink>
)}
</>
)}
<MenuItem onClick={confirmLogout} icon={SignOutIcon}>
<FormattedMessage
id='navigation_bar.sign_out'
defaultMessage='Sign out'
/>
</MenuItem>
</MenuList>
</Menu>
</div>
<MenuItemDivider />
<MenuItem onClick={confirmLogout} icon={SignOutIcon}>
<FormattedMessage
id='navigation_bar.sign_out'
defaultMessage='Sign out'
/>
</MenuItem>
</>
);
};
const ProfileMenuItem: React.FC = () => {
const { accountId } = useIdentity();
const account = useAccount(accountId);
const handle = useAccountHandle(account);
if (!accountId) {
return null;
}
const accountBasePath = `/@${account?.acct}`;
return (
<MenuItemLink to={accountBasePath}>
<ListItemWrapper
icon={<Avatar account={account} size={40} />}
className={classes.profileMenuItem}
>
<ListItemContent subtitle={handle}>
<DisplayName variant='simple' account={account} />
</ListItemContent>
</ListItemWrapper>
</MenuItemLink>
);
};

View File

@@ -72,6 +72,10 @@ function useFollowedHashtags() {
export const RedesignNavigationPanel: React.FC<{
siteName?: string;
/**
* In 'slide-out' mode (used on smaller viewport sizes), some
* menu items are hidden and the design is tweaked slightly
*/
mode?: 'static' | 'slide-out';
}> = ({ siteName, mode = 'static' }) => {
const intl = useIntl();
@@ -204,38 +208,45 @@ export const RedesignNavigationPanel: React.FC<{
)}
</ul>
<footer className={classes.footer} data-stuck={!isScrolledToBottom}>
<ul className={classes.footerNav}>
<NavigationLink
stacked
to='/notifications'
iconComponent={BellIcon}
badgeCount={notificationsCount}
>
<FormattedMessage
id='tabs_bar.notifications'
defaultMessage='Notifications'
/>
</NavigationLink>
<NavigationLink
stacked
to='/conversations'
iconComponent={ChatCircleIcon}
>
<FormattedMessage
id='tabs_bar.messages'
defaultMessage='Messages'
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
/>
</NavigationLink>
<NavigationLink
stacked
to='/bookmarks'
iconComponent={BookmarkSimpleIcon}
>
<FormattedMessage id='tabs_bar.saved' defaultMessage='Saved' />
</NavigationLink>
</ul>
<NavigationAccountCardAndMenu />
{mode !== 'slide-out' && (
<>
<ul className={classes.footerNav}>
<NavigationLink
stacked
to='/notifications'
iconComponent={BellIcon}
badgeCount={notificationsCount}
>
<FormattedMessage
id='tabs_bar.notifications'
defaultMessage='Notifications'
/>
</NavigationLink>
<NavigationLink
stacked
to='/conversations'
iconComponent={ChatCircleIcon}
>
<FormattedMessage
id='tabs_bar.messages'
defaultMessage='Messages'
description='Message refers to a direct message. For languages where this is confusing, "chat" or "direct message" can be used.'
/>
</NavigationLink>
<NavigationLink
stacked
to='/bookmarks'
iconComponent={BookmarkSimpleIcon}
>
<FormattedMessage
id='tabs_bar.saved'
defaultMessage='Saved'
/>
</NavigationLink>
</ul>
<NavigationAccountCardAndMenu />
</>
)}
<NavigationFooterLinks siteName={siteName} />
</footer>
</>

View File

@@ -15,6 +15,7 @@ import { useDrag } from '@use-gesture/react';
import { closeNavigation, openNavigation } from '@/mastodon/actions/navigation';
import { Avatar } from '@/mastodon/components/avatar';
import { Menu, MenuList, MenuTrigger } from '@/mastodon/components/menu';
import { FOCUS_TARGET } from '@/mastodon/components/navigation_focus_target';
import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger';
import { useAccount } from '@/mastodon/hooks/useAccount';
@@ -23,8 +24,9 @@ import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifi
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { RedesignNavigationPanel } from '.';
import { AccountMenuItems } from './account_card_and_menu';
import classes from './mobile_nav.module.scss';
import { MobileNavLink } from './navigation_link';
import { MobileNavLink, MobileNavProfileButton } from './navigation_link';
export const RedesignMobileNavigation: React.FC = () => {
const { accountId, signedIn } = useIdentity();
@@ -71,14 +73,26 @@ export const RedesignMobileNavigation: React.FC = () => {
defaultMessage='Notifications'
/>
</MobileNavLink>
<MobileNavLink
to={`/@${account?.acct}`}
customIcon={
<Avatar size={24} account={account} className={classes.avatar} />
}
>
<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />
</MobileNavLink>
<Menu>
<MenuTrigger
as={MobileNavProfileButton}
avatar={
<Avatar
size={24}
account={account}
className={classes.avatar}
/>
}
>
<FormattedMessage
id='tabs_bar.account_settings'
defaultMessage='Account settings'
/>
</MenuTrigger>
<MenuList placement='top-end' offset={8}>
<AccountMenuItems context='mobile' />
</MenuList>
</Menu>
</ul>
<ComposeRedesignButton inline />
</nav>

View File

@@ -76,6 +76,14 @@
justify-content: center;
gap: 0;
padding-block: var(--space-sm);
transition:
background-color 200ms,
color 200ms;
&[aria-expanded='true'] {
color: var(--color-text-inverted);
background-color: var(--color-bg-inverted);
}
}
.icon {

View File

@@ -48,6 +48,7 @@ export const NavigationLink: React.FC<NavigationLinkProps> = ({
>
<Comp
{...otherProps}
type={as === 'button' ? 'button' : undefined}
className={classNames(classes.link, stacked && classes.linkStacked)}
>
{IconComp && (
@@ -74,20 +75,11 @@ export const NavigationLink: React.FC<NavigationLinkProps> = ({
type MobileNavLink = NavLinkProps & {
withDot?: boolean;
children: ReactNode;
} & (
| {
iconComponent: Icon | React.FC<SVGProps<SVGSVGElement>>;
customIcon?: never;
}
| {
customIcon: ReactNode;
iconComponent?: never;
}
);
iconComponent: Icon | React.FC<SVGProps<SVGSVGElement>>;
};
export const MobileNavLink: React.FC<MobileNavLink> = ({
iconComponent: IconComp,
customIcon,
withDot,
children,
...otherProps
@@ -105,14 +97,31 @@ export const MobileNavLink: React.FC<MobileNavLink> = ({
<span
className={classNames(classes.icon, withDot && classes.iconWithDot)}
>
{IconComp ? (
<IconComp size={24} weight={isActive ? 'fill' : undefined} />
) : (
customIcon
)}
<IconComp size={24} weight={isActive ? 'fill' : undefined} />
</span>
<span className='sr-only'>{children}</span>
</NavLink>
</li>
);
};
export const MobileNavProfileButton: React.FC<
{ avatar?: ReactNode } & Pick<MobileNavLink, 'children' | 'withDot'>
> = ({ children, avatar, withDot, ...otherProps }) => {
return (
<li>
<button
type='button'
{...otherProps}
className={classNames(classes.link, classes.linkMobile)}
>
<span
className={classNames(classes.icon, withDot && classes.iconWithDot)}
>
{avatar}
</span>
<span className='sr-only'>{children}</span>
</button>
</li>
);
};

View File

@@ -1040,6 +1040,7 @@
"navigation_bar.powered_by_mastodon": "powered by {logo}Mastodon",
"navigation_bar.preferences": "Preferences",
"navigation_bar.privacy_and_reach": "Privacy and reach",
"navigation_bar.saved_posts": "Saved Posts",
"navigation_bar.search": "Search",
"navigation_bar.search_trends": "Search / Trending",
"navigation_bar.sign_out": "Sign out",
@@ -1445,6 +1446,7 @@
"subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}",
"tab_bar.messages": "Messages",
"tabs_bar.account_settings": "Account settings",
"tabs_bar.create_custom_feed": "Create",
"tabs_bar.custom_feeds": "Custom Feeds",
"tabs_bar.custom_feeds_empty": "You have no custom feeds yet.",
@@ -1455,9 +1457,7 @@
"tabs_bar.home": "Home",
"tabs_bar.menu": "Menu",
"tabs_bar.messages": "Messages",
"tabs_bar.more": "More",
"tabs_bar.notifications": "Notifications",
"tabs_bar.profile": "Profile",
"tabs_bar.publish": "New Post",
"tabs_bar.quick_links": "Quick links",
"tabs_bar.saved": "Saved",