Redesign: Hide mobile compose button on pages that aren't primarily post feeds (#40698)

This commit is contained in:
diondiondion
2026-09-25 12:01:30 +00:00
committed by GitHub
parent 6511e595ec
commit 87de690f9b
4 changed files with 110 additions and 31 deletions

View File

@@ -3,7 +3,6 @@ import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { ArrowsOutSimpleIcon, MinusIcon, XIcon } from '@phosphor-icons/react';
import { ReadCvLogoIcon } from '@phosphor-icons/react/dist/ssr';
import { IconButton } from '@/mastodon/components/button/redesign';
import {
@@ -17,8 +16,6 @@ import {
useAppSelector,
} from '@/mastodon/store';
import { useBreakpoint } from '../../ui/hooks/useBreakpoint';
import { selectComposeType } from './selectors';
import classes from './styles.module.scss';
@@ -73,27 +70,18 @@ export const ComposeFormHeader: React.FC<{
noMinimize?: boolean;
}> = ({ id, noMinimize }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const titleMessage = useAppSelector(selectComposeFormTitle);
const isMinimized = useAppSelector(selectIsMinimized);
const dispatch = useAppDispatch();
const onClose = useCallback(() => {
dispatch(closeComposer());
}, [dispatch]);
const onMinimize = useCallback(() => {
dispatch(minimizeComposerToggle());
}, [dispatch]);
const isMobile = useBreakpoint('openable');
if (isMobile && isMinimized && !noMinimize) {
return (
<IconButton icon={ReadCvLogoIcon} onClick={onMinimize} size='lg'>
<FormattedMessage id='compose.expand' defaultMessage='Show composer' />
</IconButton>
);
}
return (
<header className={classes.header}>
<h2 id={id}>{intl.formatMessage(titleMessage)}</h2>

View File

@@ -1,7 +1,13 @@
@use '@/styles/mastodon/variables';
.button {
&:not(.buttonInline) {
.buttonWrapper {
position: relative;
@media (prefers-reduced-motion: no-preference) {
transition: all 200ms ease-out;
}
&:not(.buttonWrapperInline) {
position: fixed;
bottom: var(--space-4);
right: var(--space-4);
@@ -9,6 +15,12 @@
}
}
.buttonWrapperHidden {
transform: translateX(calc(100% + var(--space-4)));
opacity: 0;
transition-timing-function: ease-in;
}
.composer,
.composerMinimized {
position: fixed;

View File

@@ -4,13 +4,16 @@ import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useRouteMatch } from 'react-router';
import {
ChatCircleDotsIcon,
NewspaperIcon,
PenNibIcon,
ReadCvLogoIcon,
} from '@phosphor-icons/react';
import type { IconButtonProps } from '@/mastodon/components/button/redesign';
import { IconButton } from '@/mastodon/components/button/redesign';
import {
Menu,
@@ -20,10 +23,15 @@ import {
} from '@/mastodon/components/menu';
import { MenuCard } from '@/mastodon/components/menu/card';
import { useIdentity } from '@/mastodon/identity_context';
import { openNewComposer } from '@/mastodon/reducers/slices/composer';
import {
minimizeComposerToggle,
openNewComposer,
} from '@/mastodon/reducers/slices/composer';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import { useBreakpoint } from '../../ui/hooks/useBreakpoint';
import { ComposeFormHeader } from './header';
import classes from './trigger.module.scss';
@@ -38,8 +46,13 @@ export const ComposeRedesignButton: React.FC<{
* Render the button in regular document flow instead of fixed positioning for mobile layout
*/
inline?: boolean;
}> = ({ inline }) => {
}> = ({ inline = false }) => {
const displayState = useAppSelector((state) => state.composer.displayState);
const isMobile = useBreakpoint('openable');
const hasMobileFloatingActionButton = useHasMobileFloatingActionButton({
isMobile,
});
// Update viewport based on visual size in order to account for the virtual keyboard.
const [viewportHeight, setViewportHeight] = useState<null | number>(null);
@@ -69,14 +82,32 @@ export const ComposeRedesignButton: React.FC<{
[dispatch],
);
const toggleMinimize = useCallback(() => {
dispatch(minimizeComposerToggle());
}, [dispatch]);
const { signedIn } = useIdentity();
if (!isRedesignEnabled() || !signedIn) {
return null;
}
const floatingButtonProps = {
inline,
hidden: !hasMobileFloatingActionButton,
} as const;
if (displayState === 'minimized') {
return (
return isMobile ? (
<FloatingActionButton
icon={ReadCvLogoIcon}
onClick={toggleMinimize}
{...floatingButtonProps}
hidden={false} // never hide minimized composer button
>
<FormattedMessage id='compose.expand' defaultMessage='Show composer' />
</FloatingActionButton>
) : (
<MenuCard className={classes.composerMinimized} elevation={2}>
<ComposeFormHeader />
</MenuCard>
@@ -91,21 +122,16 @@ export const ComposeRedesignButton: React.FC<{
return (
<Suspense
fallback={
<IconButton
<FloatingActionButton
loading
icon={PenNibIcon}
className={classNames(
classes.button,
inline && classes.buttonInline,
)}
variant='solid'
size='lg'
{...floatingButtonProps}
>
<FormattedMessage
id='compose.new'
defaultMessage='Write a new post or messsage'
/>
</IconButton>
</FloatingActionButton>
}
>
<ComposeLazyForm autoFocus className={classes.composer} style={style} />
@@ -116,11 +142,9 @@ export const ComposeRedesignButton: React.FC<{
return (
<Menu>
<MenuTrigger
as={IconButton}
as={FloatingActionButton}
icon={PenNibIcon}
variant='solid'
className={classNames(classes.button, inline && classes.buttonInline)}
size='lg'
{...floatingButtonProps}
>
<FormattedMessage
id='compose.new'
@@ -148,3 +172,57 @@ export const ComposeRedesignButton: React.FC<{
</Menu>
);
};
const FloatingActionButton: React.FC<
{
inline: boolean;
hidden: boolean;
} & IconButtonProps
> = ({ inline, hidden, ...otherProps }) => {
return (
// This component uses a wrapper element to prevent its
// CSS transitions from messing with the button's own transitions
<div
className={classNames(
classes.buttonWrapper,
inline && classes.buttonWrapperInline,
hidden && classes.buttonWrapperHidden,
)}
inert={hidden}
>
<IconButton variant='solid' size='lg' {...otherProps} />
</div>
);
};
function includeMultiColumnPaths(paths: string[]) {
return [...paths, ...paths.map((path) => `/deck${path}`)];
}
const MOBILE_COMPOSE_BUTTON_ALLOW_ROUTES = includeMultiColumnPaths([
'/home',
'/public',
'/lists',
'/tags',
]);
const MOBILE_COMPOSE_BUTTON_BLOCK_ROUTES = includeMultiColumnPaths([
'/lists/new',
]);
function useHasMobileFloatingActionButton({ isMobile }: { isMobile: boolean }) {
const isRouteWithMobileComposeButton = !!useRouteMatch({
path: MOBILE_COMPOSE_BUTTON_ALLOW_ROUTES,
exact: false,
});
const isRouteWithoutMobileComposeButton = !!useRouteMatch({
path: MOBILE_COMPOSE_BUTTON_BLOCK_ROUTES,
exact: true,
});
const shouldHideMobileComposeButton =
isMobile &&
(!isRouteWithMobileComposeButton || isRouteWithoutMobileComposeButton);
return !shouldHideMobileComposeButton;
}

View File

@@ -247,6 +247,7 @@ const SlideOutNavigation: React.FC = () => {
className={classes.slideOutWrapper}
data-is-open={isOpen}
ref={overlayRef}
inert={!isOpen}
>
<animated.div className={classes.slideOut} {...bind()} style={{ x }}>
<RedesignNavigationPanel mode='slide-out' />