mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-27 17:07:48 -05:00
Redesign: Hide mobile compose button on pages that aren't primarily post feeds (#40698)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' />
|
||||
|
||||
Reference in New Issue
Block a user