From 8cf246e4d3305a71e647c2f374468141b8bc846c Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 11 Jun 2025 13:12:39 +0200 Subject: [PATCH 01/10] Prevent two composers from being shown (#35006) --- .../features/ui/components/columns_area.jsx | 2 +- .../features/ui/components/compose_panel.jsx | 64 ------------------- .../features/ui/components/compose_panel.tsx | 56 ++++++++++++++++ app/javascript/mastodon/hooks/useLayout.ts | 13 ++++ 4 files changed, 70 insertions(+), 65 deletions(-) delete mode 100644 app/javascript/mastodon/features/ui/components/compose_panel.jsx create mode 100644 app/javascript/mastodon/features/ui/components/compose_panel.tsx create mode 100644 app/javascript/mastodon/hooks/useLayout.ts diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 97b54e50d3..4901ee2182 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -23,7 +23,7 @@ import { useColumnsContext } from '../util/columns_context'; import BundleColumnError from './bundle_column_error'; import { ColumnLoading } from './column_loading'; -import ComposePanel from './compose_panel'; +import { ComposePanel } from './compose_panel'; import DrawerLoading from './drawer_loading'; import NavigationPanel from './navigation_panel'; diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.jsx b/app/javascript/mastodon/features/ui/components/compose_panel.jsx deleted file mode 100644 index e622c8859a..0000000000 --- a/app/javascript/mastodon/features/ui/components/compose_panel.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { connect } from 'react-redux'; - -import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose'; -import ServerBanner from 'mastodon/components/server_banner'; -import { Search } from 'mastodon/features/compose/components/search'; -import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; -import { LinkFooter } from 'mastodon/features/ui/components/link_footer'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; - -class ComposePanel extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - dispatch: PropTypes.func.isRequired, - }; - - onFocus = () => { - const { dispatch } = this.props; - dispatch(changeComposing(true)); - }; - - onBlur = () => { - const { dispatch } = this.props; - dispatch(changeComposing(false)); - }; - - componentDidMount () { - const { dispatch } = this.props; - dispatch(mountCompose()); - } - - componentWillUnmount () { - const { dispatch } = this.props; - dispatch(unmountCompose()); - } - - render() { - const { signedIn } = this.props.identity; - - return ( -
- - - {!signedIn && ( - <> - -
- - )} - - {signedIn && ( - - )} - - -
- ); - } - -} - -export default connect()(withIdentity(ComposePanel)); diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.tsx b/app/javascript/mastodon/features/ui/components/compose_panel.tsx new file mode 100644 index 0000000000..aa15520309 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/compose_panel.tsx @@ -0,0 +1,56 @@ +import { useCallback, useEffect } from 'react'; + +import { useLayout } from '@/mastodon/hooks/useLayout'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { + changeComposing, + mountCompose, + unmountCompose, +} from 'mastodon/actions/compose'; +import ServerBanner from 'mastodon/components/server_banner'; +import { Search } from 'mastodon/features/compose/components/search'; +import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; +import { LinkFooter } from 'mastodon/features/ui/components/link_footer'; +import { useIdentity } from 'mastodon/identity_context'; + +export const ComposePanel: React.FC = () => { + const dispatch = useAppDispatch(); + const handleFocus = useCallback(() => { + dispatch(changeComposing(true)); + }, [dispatch]); + const { signedIn } = useIdentity(); + const hideComposer = useAppSelector((state) => { + const mounted = state.compose.get('mounted'); + if (typeof mounted === 'number') { + return mounted > 1; + } + return false; + }); + + useEffect(() => { + dispatch(mountCompose()); + return () => { + dispatch(unmountCompose()); + }; + }, [dispatch]); + + const { singleColumn } = useLayout(); + + return ( +
+ + + {!signedIn && ( + <> + +
+ + )} + + {signedIn && !hideComposer && } + {signedIn && hideComposer &&
} + + +
+ ); +}; diff --git a/app/javascript/mastodon/hooks/useLayout.ts b/app/javascript/mastodon/hooks/useLayout.ts new file mode 100644 index 0000000000..fc1cf136bf --- /dev/null +++ b/app/javascript/mastodon/hooks/useLayout.ts @@ -0,0 +1,13 @@ +import type { LayoutType } from '../is_mobile'; +import { useAppSelector } from '../store'; + +export const useLayout = () => { + const layout = useAppSelector( + (state) => state.meta.get('layout') as LayoutType, + ); + + return { + singleColumn: layout === 'single-column' || layout === 'mobile', + layout, + }; +}; From a13b33d85131e3e65cfc71894672cb2b15c89c51 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 11 Jun 2025 13:55:43 +0200 Subject: [PATCH 02/10] Change navigation layout on small screens in web UI (#34910) --- app/javascript/mastodon/actions/navigation.ts | 7 + .../mastodon/components/column_header.tsx | 8 +- .../mastodon/components/icon_button.tsx | 3 + .../mastodon/components/icon_with_badge.tsx | 2 +- ...{navigation_bar.jsx => navigation_bar.tsx} | 27 +- .../mastodon/features/compose/index.jsx | 138 ----- .../mastodon/features/compose/index.tsx | 200 +++++++ .../mastodon/features/explore/index.tsx | 5 +- .../features/getting_started/index.jsx | 2 +- .../mastodon/features/home_timeline/index.jsx | 9 +- .../features/ui/components/column_link.jsx | 49 -- .../features/ui/components/column_link.tsx | 86 +++ .../features/ui/components/columns_area.jsx | 8 +- .../features/ui/components/header.jsx | 121 ----- .../features/ui/components/list_panel.jsx | 41 -- .../features/ui/components/list_panel.tsx | 92 ++++ .../features/ui/components/navigation_bar.tsx | 204 ++++++++ .../ui/components/navigation_panel.jsx | 206 -------- .../ui/components/navigation_panel.tsx | 495 ++++++++++++++++++ .../features/ui/hooks/useBreakpoint.tsx | 53 ++ app/javascript/mastodon/features/ui/index.jsx | 5 +- app/javascript/mastodon/locales/en.json | 6 +- app/javascript/mastodon/reducers/index.ts | 2 + .../mastodon/reducers/navigation.ts | 28 + app/javascript/mastodon/selectors/lists.ts | 23 +- .../400-24px/arrow_left-fill.svg | 1 + .../material-icons/400-24px/arrow_left.svg | 1 + .../400-24px/edit_square-fill.svg | 1 + .../material-icons/400-24px/edit_square.svg | 1 + .../400-24px/unfold_less-fill.svg | 1 + .../material-icons/400-24px/unfold_less.svg | 1 + .../400-24px/unfold_more-fill.svg | 1 + .../material-icons/400-24px/unfold_more.svg | 1 + .../styles/mastodon/components.scss | 244 +++++---- 34 files changed, 1390 insertions(+), 682 deletions(-) create mode 100644 app/javascript/mastodon/actions/navigation.ts rename app/javascript/mastodon/features/compose/components/{navigation_bar.jsx => navigation_bar.tsx} (59%) delete mode 100644 app/javascript/mastodon/features/compose/index.jsx create mode 100644 app/javascript/mastodon/features/compose/index.tsx delete mode 100644 app/javascript/mastodon/features/ui/components/column_link.jsx create mode 100644 app/javascript/mastodon/features/ui/components/column_link.tsx delete mode 100644 app/javascript/mastodon/features/ui/components/header.jsx delete mode 100644 app/javascript/mastodon/features/ui/components/list_panel.jsx create mode 100644 app/javascript/mastodon/features/ui/components/list_panel.tsx create mode 100644 app/javascript/mastodon/features/ui/components/navigation_bar.tsx delete mode 100644 app/javascript/mastodon/features/ui/components/navigation_panel.jsx create mode 100644 app/javascript/mastodon/features/ui/components/navigation_panel.tsx create mode 100644 app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx create mode 100644 app/javascript/mastodon/reducers/navigation.ts create mode 100644 app/javascript/material-icons/400-24px/arrow_left-fill.svg create mode 100644 app/javascript/material-icons/400-24px/arrow_left.svg create mode 100644 app/javascript/material-icons/400-24px/edit_square-fill.svg create mode 100644 app/javascript/material-icons/400-24px/edit_square.svg create mode 100644 app/javascript/material-icons/400-24px/unfold_less-fill.svg create mode 100644 app/javascript/material-icons/400-24px/unfold_less.svg create mode 100644 app/javascript/material-icons/400-24px/unfold_more-fill.svg create mode 100644 app/javascript/material-icons/400-24px/unfold_more.svg diff --git a/app/javascript/mastodon/actions/navigation.ts b/app/javascript/mastodon/actions/navigation.ts new file mode 100644 index 0000000000..663a1c1bce --- /dev/null +++ b/app/javascript/mastodon/actions/navigation.ts @@ -0,0 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; + +export const openNavigation = createAction('navigation/open'); + +export const closeNavigation = createAction('navigation/close'); + +export const toggleNavigation = createAction('navigation/toggle'); diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index ec946cab3e..3a8d245b2a 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -9,7 +9,8 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; +import UnfoldLessIcon from '@/material-icons/400-24px/unfold_less.svg?react'; +import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react'; import type { IconProp } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon'; import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; @@ -238,7 +239,10 @@ export const ColumnHeader: React.FC = ({ onClick={handleToggleClick} > - + {collapseIssues && } diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx index 8ec665bbd8..cd0234e39a 100644 --- a/app/javascript/mastodon/components/icon_button.tsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -27,6 +27,7 @@ interface Props { counter?: number; href?: string; ariaHidden?: boolean; + ariaControls?: string; } export const IconButton = forwardRef( @@ -52,6 +53,7 @@ export const IconButton = forwardRef( overlay = false, tabIndex = 0, ariaHidden = false, + ariaControls, }, buttonRef, ) => { @@ -153,6 +155,7 @@ export const IconButton = forwardRef( aria-label={title} aria-expanded={expanded} aria-hidden={ariaHidden} + aria-controls={ariaControls} title={title} className={classes} onClick={handleClick} diff --git a/app/javascript/mastodon/components/icon_with_badge.tsx b/app/javascript/mastodon/components/icon_with_badge.tsx index c6ab34479c..3469fec338 100644 --- a/app/javascript/mastodon/components/icon_with_badge.tsx +++ b/app/javascript/mastodon/components/icon_with_badge.tsx @@ -7,7 +7,7 @@ interface Props { id: string; icon: IconProp; count: number; - issueBadge: boolean; + issueBadge?: boolean; className: string; } export const IconWithBadge: React.FC = ({ diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx b/app/javascript/mastodon/features/compose/components/navigation_bar.tsx similarity index 59% rename from app/javascript/mastodon/features/compose/components/navigation_bar.jsx rename to app/javascript/mastodon/features/compose/components/navigation_bar.tsx index 38382b0ca0..df1c0a129d 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.tsx @@ -2,34 +2,47 @@ import { useCallback } from 'react'; import { useIntl, defineMessages } from 'react-intl'; -import { useSelector, useDispatch } from 'react-redux'; - import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import { cancelReplyCompose } from 'mastodon/actions/compose'; import { Account } from 'mastodon/components/account'; import { IconButton } from 'mastodon/components/icon_button'; import { me } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { ActionBar } from './action_bar'; - const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, }); -export const NavigationBar = () => { - const dispatch = useDispatch(); +export const NavigationBar: React.FC = () => { + const dispatch = useAppDispatch(); const intl = useIntl(); - const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to'])); + const isReplying = useAppSelector( + (state) => !!state.compose.get('in_reply_to'), + ); const handleCancelClick = useCallback(() => { dispatch(cancelReplyCompose()); }, [dispatch]); + if (!me) { + return null; + } + return (
- {isReplying ? : } + {isReplying ? ( + + ) : ( + + )}
); }; diff --git a/app/javascript/mastodon/features/compose/index.jsx b/app/javascript/mastodon/features/compose/index.jsx deleted file mode 100644 index 660f08615b..0000000000 --- a/app/javascript/mastodon/features/compose/index.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import { Helmet } from 'react-helmet'; -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; - -import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; -import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; -import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; -import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; -import PublicIcon from '@/material-icons/400-24px/public.svg?react'; -import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; -import { openModal } from 'mastodon/actions/modal'; -import Column from 'mastodon/components/column'; -import { Icon } from 'mastodon/components/icon'; - -import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; -import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose'; -import { mascot } from '../../initial_state'; -import { isMobile } from '../../is_mobile'; - -import { Search } from './components/search'; -import ComposeFormContainer from './containers/compose_form_container'; - -const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, -}); - -const mapStateToProps = (state) => ({ - columns: state.getIn(['settings', 'columns']), -}); - -class Compose extends PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columns: ImmutablePropTypes.list.isRequired, - multiColumn: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - const { dispatch } = this.props; - dispatch(mountCompose()); - } - - componentWillUnmount () { - const { dispatch } = this.props; - dispatch(unmountCompose()); - } - - handleLogoutClick = e => { - const { dispatch } = this.props; - - e.preventDefault(); - e.stopPropagation(); - - dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' })); - - return false; - }; - - onFocus = () => { - this.props.dispatch(changeComposing(true)); - }; - - onBlur = () => { - this.props.dispatch(changeComposing(false)); - }; - - render () { - const { multiColumn, intl } = this.props; - - if (multiColumn) { - const { columns } = this.props; - - return ( -
- - - {multiColumn && } - -
-
- - -
- -
-
-
-
- ); - } - - return ( - - - - - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Compose)); diff --git a/app/javascript/mastodon/features/compose/index.tsx b/app/javascript/mastodon/features/compose/index.tsx new file mode 100644 index 0000000000..54776c98ff --- /dev/null +++ b/app/javascript/mastodon/features/compose/index.tsx @@ -0,0 +1,200 @@ +import { useEffect, useCallback } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import { Helmet } from 'react-helmet'; +import { Link } from 'react-router-dom'; + +import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +import elephantUIPlane from '@/images/elephant_ui_plane.svg'; +import EditIcon from '@/material-icons/400-24px/edit_square.svg?react'; +import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; +import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; +import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; +import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; +import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; +import PublicIcon from '@/material-icons/400-24px/public.svg?react'; +import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; +import { mountCompose, unmountCompose } from 'mastodon/actions/compose'; +import { openModal } from 'mastodon/actions/modal'; +import { Column } from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import { Icon } from 'mastodon/components/icon'; +import { mascot } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import { Search } from './components/search'; +import ComposeFormContainer from './containers/compose_form_container'; + +const messages = defineMessages({ + start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + notifications: { + id: 'tabs_bar.notifications', + defaultMessage: 'Notifications', + }, + public: { + id: 'navigation_bar.public_timeline', + defaultMessage: 'Federated timeline', + }, + community: { + id: 'navigation_bar.community_timeline', + defaultMessage: 'Local timeline', + }, + preferences: { + id: 'navigation_bar.preferences', + defaultMessage: 'Preferences', + }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, +}); + +type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>; + +const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const columns = useAppSelector( + (state) => + (state.settings as ImmutableMap).get( + 'columns', + ) as ImmutableList, + ); + + useEffect(() => { + dispatch(mountCompose()); + + return () => { + dispatch(unmountCompose()); + }; + }, [dispatch]); + + const handleLogoutClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} })); + + return false; + }, + [dispatch], + ); + + if (multiColumn) { + return ( +
+ + + + +
+
+ + +
+ +
+
+
+
+ ); + } + + return ( + + + +
+ +
+ + + + +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default Compose; diff --git a/app/javascript/mastodon/features/explore/index.tsx b/app/javascript/mastodon/features/explore/index.tsx index 671d92d6b4..c6f65a09ac 100644 --- a/app/javascript/mastodon/features/explore/index.tsx +++ b/app/javascript/mastodon/features/explore/index.tsx @@ -9,7 +9,9 @@ import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import { Column } from 'mastodon/components/column'; import type { ColumnRef } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; +import { SymbolLogo } from 'mastodon/components/logo'; import { Search } from 'mastodon/features/compose/components/search'; +import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint'; import { useIdentity } from 'mastodon/identity_context'; import Links from './links'; @@ -25,6 +27,7 @@ const Explore: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { const { signedIn } = useIdentity(); const intl = useIntl(); const columnRef = useRef(null); + const logoRequired = useBreakpoint('full'); const handleHeaderClick = useCallback(() => { columnRef.current?.scrollTop(); @@ -38,7 +41,7 @@ const Explore: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { > { @@ -121,7 +124,7 @@ class HomeTimeline extends PureComponent { }; render () { - const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; + const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, matchesBreakpoint } = this.props; const pinned = !!columnId; const { signedIn } = this.props.identity; const banners = []; @@ -150,7 +153,7 @@ class HomeTimeline extends PureComponent { { - const match = useRouteMatch(to); - const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--optional': optional }); - const badgeElement = typeof badge !== 'undefined' ? {badge} : null; - const iconElement = (typeof icon === 'string' || iconComponent) ? : icon; - const activeIconElement = activeIcon ?? (activeIconComponent ? : iconElement); - const active = match?.isExact; - - if (href) { - return ( - - {active ? activeIconElement : iconElement} - {text} - {badgeElement} - - ); - } else { - return ( - - {active ? activeIconElement : iconElement} - {text} - {badgeElement} - - ); - } -}; - -ColumnLink.propTypes = { - icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - iconComponent: PropTypes.func, - activeIcon: PropTypes.node, - activeIconComponent: PropTypes.func, - text: PropTypes.string.isRequired, - to: PropTypes.string, - href: PropTypes.string, - method: PropTypes.string, - badge: PropTypes.node, - transparent: PropTypes.bool, - optional: PropTypes.bool, -}; - -export default ColumnLink; diff --git a/app/javascript/mastodon/features/ui/components/column_link.tsx b/app/javascript/mastodon/features/ui/components/column_link.tsx new file mode 100644 index 0000000000..d322c2e4c4 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/column_link.tsx @@ -0,0 +1,86 @@ +import classNames from 'classnames'; +import { useRouteMatch, NavLink } from 'react-router-dom'; + +import { Icon } from 'mastodon/components/icon'; +import type { IconProp } from 'mastodon/components/icon'; + +export const ColumnLink: React.FC<{ + icon: React.ReactNode; + iconComponent?: IconProp; + activeIcon?: React.ReactNode; + activeIconComponent?: IconProp; + isActive?: (match: unknown, location: { pathname: string }) => boolean; + text: string; + to?: string; + href?: string; + method?: string; + badge?: React.ReactNode; + transparent?: boolean; + optional?: boolean; + className?: string; + id?: string; +}> = ({ + icon, + activeIcon, + iconComponent, + activeIconComponent, + text, + to, + href, + method, + badge, + transparent, + optional, + ...other +}) => { + const match = useRouteMatch(to ?? ''); + const className = classNames('column-link', { + 'column-link--transparent': transparent, + 'column-link--optional': optional, + }); + const badgeElement = + typeof badge !== 'undefined' ? ( + {badge} + ) : null; + const iconElement = iconComponent ? ( + + ) : ( + icon + ); + const activeIconElement = + activeIcon ?? + (activeIconComponent ? ( + + ) : ( + iconElement + )); + const active = !!match; + + if (href) { + return ( + + {active ? activeIconElement : iconElement} + {text} + {badgeElement} + + ); + } else if (to) { + return ( + + {active ? activeIconElement : iconElement} + {text} + {badgeElement} + + ); + } else { + return null; + } +}; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 4901ee2182..5d596b7bcb 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -25,7 +25,7 @@ import BundleColumnError from './bundle_column_error'; import { ColumnLoading } from './column_loading'; import { ComposePanel } from './compose_panel'; import DrawerLoading from './drawer_loading'; -import NavigationPanel from './navigation_panel'; +import { NavigationPanel } from './navigation_panel'; const componentMap = { 'COMPOSE': Compose, @@ -132,11 +132,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
{children}
-
-
- -
-
+
); } diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx deleted file mode 100644 index 19c76c722b..0000000000 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; - -import { Link, withRouter } from 'react-router-dom'; - -import { connect } from 'react-redux'; - -import SearchIcon from '@/material-icons/400-24px/search.svg?react'; -import { openModal } from 'mastodon/actions/modal'; -import { fetchServer } from 'mastodon/actions/server'; -import { Avatar } from 'mastodon/components/avatar'; -import { Icon } from 'mastodon/components/icon'; -import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { registrationsOpen, me, sso_redirect } from 'mastodon/initial_state'; - -const Account = connect(state => ({ - account: state.getIn(['accounts', me]), -}))(({ account }) => ( - - - -)); - -const messages = defineMessages({ - search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, -}); - -const mapStateToProps = (state) => ({ - signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up', -}); - -const mapDispatchToProps = (dispatch) => ({ - openClosedRegistrationsModal() { - dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); - }, - dispatchServer() { - dispatch(fetchServer()); - } -}); - -class Header extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - openClosedRegistrationsModal: PropTypes.func, - location: PropTypes.object, - signupUrl: PropTypes.string.isRequired, - dispatchServer: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - const { dispatchServer } = this.props; - dispatchServer(); - } - - render () { - const { signedIn } = this.props.identity; - const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props; - - let content; - - if (signedIn) { - content = ( - <> - {location.pathname !== '/search' && } - {location.pathname !== '/publish' && } - - - ); - } else { - - if (sso_redirect) { - content = ( - - ); - } else { - let signupButton; - - if (registrationsOpen) { - signupButton = ( - - - - ); - } else { - signupButton = ( - - ); - } - - content = ( - <> - {signupButton} - - - ); - } - } - - return ( -
- - - - - -
- {content} -
-
- ); - } - -} - -export default injectIntl(withRouter(withIdentity(connect(mapStateToProps, mapDispatchToProps)(Header)))); diff --git a/app/javascript/mastodon/features/ui/components/list_panel.jsx b/app/javascript/mastodon/features/ui/components/list_panel.jsx deleted file mode 100644 index 03c8fce9e8..0000000000 --- a/app/javascript/mastodon/features/ui/components/list_panel.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect } from 'react'; - -import { createSelector } from '@reduxjs/toolkit'; -import { useDispatch, useSelector } from 'react-redux'; - -import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react'; -import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; -import { fetchLists } from 'mastodon/actions/lists'; - -import ColumnLink from './column_link'; - -const getOrderedLists = createSelector([state => state.get('lists')], lists => { - if (!lists) { - return lists; - } - - return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4); -}); - -export const ListPanel = () => { - const dispatch = useDispatch(); - const lists = useSelector(state => getOrderedLists(state)); - - useEffect(() => { - dispatch(fetchLists()); - }, [dispatch]); - - if (!lists || lists.isEmpty()) { - return null; - } - - return ( -
-
- - {lists.map(list => ( - - ))} -
- ); -}; diff --git a/app/javascript/mastodon/features/ui/components/list_panel.tsx b/app/javascript/mastodon/features/ui/components/list_panel.tsx new file mode 100644 index 0000000000..ef4cdad2cc --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/list_panel.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState, useCallback, useId } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react'; +import ArrowLeftIcon from '@/material-icons/400-24px/arrow_left.svg?react'; +import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react'; +import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; +import { fetchLists } from 'mastodon/actions/lists'; +import { IconButton } from 'mastodon/components/icon_button'; +import { getOrderedLists } from 'mastodon/selectors/lists'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import { ColumnLink } from './column_link'; + +const messages = defineMessages({ + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + expand: { + id: 'navigation_panel.expand_lists', + defaultMessage: 'Expand list menu', + }, + collapse: { + id: 'navigation_panel.collapse_lists', + defaultMessage: 'Collapse list menu', + }, +}); + +export const ListPanel: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const lists = useAppSelector((state) => getOrderedLists(state)); + const [expanded, setExpanded] = useState(false); + const accessibilityId = useId(); + + useEffect(() => { + dispatch(fetchLists()); + }, [dispatch]); + + const handleClick = useCallback(() => { + setExpanded((value) => !value); + }, [setExpanded]); + + return ( +
+
+ + + {lists.length > 0 && ( + + )} +
+ + {lists.length > 0 && expanded && ( +
+ {lists.map((list) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/navigation_bar.tsx b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx new file mode 100644 index 0000000000..dbb70f9ec8 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx @@ -0,0 +1,204 @@ +import { useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; +import { NavLink, useRouteMatch } from 'react-router-dom'; + +import AddIcon from '@/material-icons/400-24px/add.svg?react'; +import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; +import HomeIcon from '@/material-icons/400-24px/home.svg?react'; +import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; +import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; +import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; +import SearchIcon from '@/material-icons/400-24px/search.svg?react'; +import { openModal } from 'mastodon/actions/modal'; +import { toggleNavigation } from 'mastodon/actions/navigation'; +import { fetchServer } from 'mastodon/actions/server'; +import { Icon } from 'mastodon/components/icon'; +import { IconWithBadge } from 'mastodon/components/icon_with_badge'; +import { useIdentity } from 'mastodon/identity_context'; +import { registrationsOpen, sso_redirect } from 'mastodon/initial_state'; +import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + search: { id: 'tabs_bar.search', defaultMessage: 'Search' }, + publish: { id: 'tabs_bar.publish', defaultMessage: 'New Post' }, + notifications: { + id: 'tabs_bar.notifications', + defaultMessage: 'Notifications', + }, + menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' }, +}); + +const IconLabelButton: React.FC<{ + to: string; + icon?: React.ReactNode; + activeIcon?: React.ReactNode; + title: string; +}> = ({ to, icon, activeIcon, title }) => { + const match = useRouteMatch(to); + + return ( + + {match && activeIcon ? activeIcon : icon} + + ); +}; + +const NotificationsButton = () => { + const count = useAppSelector(selectUnreadNotificationGroupsCount); + const intl = useIntl(); + + return ( + + } + activeIcon={ + + } + title={intl.formatMessage(messages.notifications)} + /> + ); +}; + +const LoginOrSignUp: React.FC = () => { + const dispatch = useAppDispatch(); + const signupUrl = useAppSelector( + (state) => + (state.server.getIn(['server', 'registrations', 'url'], null) as + | string + | null) ?? '/auth/sign_up', + ); + + const openClosedRegistrationsModal = useCallback(() => { + dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS', modalProps: {} })); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchServer()); + }, [dispatch]); + + if (sso_redirect) { + return ( +
+ + + +
+ ); + } else { + let signupButton; + + if (registrationsOpen) { + signupButton = ( + + + + ); + } else { + signupButton = ( + + ); + } + + return ( +
+ {signupButton} + + + +
+ ); + } +}; + +export const NavigationBar: React.FC = () => { + const { signedIn } = useIdentity(); + const dispatch = useAppDispatch(); + const open = useAppSelector((state) => state.navigation.open); + const intl = useIntl(); + + const handleClick = useCallback(() => { + dispatch(toggleNavigation()); + }, [dispatch]); + + return ( +
+ {!signedIn && } + +
+ {signedIn && ( + <> + } + activeIcon={} + /> + } + /> + } + /> + + + )} + + +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx deleted file mode 100644 index 8fa20a554d..0000000000 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ /dev/null @@ -1,206 +0,0 @@ -import PropTypes from 'prop-types'; -import { Component, useEffect } from 'react'; - -import { defineMessages, injectIntl, useIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useSelector, useDispatch } from 'react-redux'; - -import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; -import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react'; -import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react'; -import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; -import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react'; -import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; -import HomeIcon from '@/material-icons/400-24px/home.svg?react'; -import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react'; -import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; -import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react'; -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; -import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; -import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; -import PublicIcon from '@/material-icons/400-24px/public.svg?react'; -import SearchIcon from '@/material-icons/400-24px/search.svg?react'; -import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; -import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react'; -import StarIcon from '@/material-icons/400-24px/star.svg?react'; -import { fetchFollowRequests } from 'mastodon/actions/accounts'; -import { IconWithBadge } from 'mastodon/components/icon_with_badge'; -import { WordmarkLogo } from 'mastodon/components/logo'; -import { NavigationPortal } from 'mastodon/components/navigation_portal'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { timelinePreview, trendsEnabled } from 'mastodon/initial_state'; -import { transientSingleColumn } from 'mastodon/is_mobile'; -import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; -import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications'; - -import ColumnLink from './column_link'; -import DisabledAccountBanner from './disabled_account_banner'; -import { ListPanel } from './list_panel'; -import SignInBanner from './sign_in_banner'; - -const messages = defineMessages({ - home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - explore: { id: 'explore.title', defaultMessage: 'Explore' }, - firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, - direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, - bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - administration: { id: 'navigation_bar.administration', defaultMessage: 'Administration' }, - moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' }, - followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' }, - about: { id: 'navigation_bar.about', defaultMessage: 'About' }, - search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, - advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' }, - openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' }, - followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, -}); - -const NotificationsLink = () => { - - const count = useSelector(selectUnreadNotificationGroupsCount); - const intl = useIntl(); - - return ( - } - activeIcon={} - text={intl.formatMessage(messages.notifications)} - /> - ); -}; - -const FollowRequestsLink = () => { - const count = useSelector(state => state.getIn(['user_lists', 'follow_requests', 'items'])?.size ?? 0); - const intl = useIntl(); - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(fetchFollowRequests()); - }, [dispatch]); - - if (count === 0) { - return null; - } - - return ( - } - activeIcon={} - text={intl.formatMessage(messages.followRequests)} - /> - ); -}; - -class NavigationPanel extends Component { - static propTypes = { - identity: identityContextPropShape, - intl: PropTypes.object.isRequired, - }; - - isFirehoseActive = (match, location) => { - return match || location.pathname.startsWith('/public'); - }; - - render () { - const { intl } = this.props; - const { signedIn, disabledAccountId, permissions } = this.props.identity; - - let banner = undefined; - - if (transientSingleColumn) { - banner = ( -
- {intl.formatMessage(messages.openedInClassicInterface)} - {" "} - - {intl.formatMessage(messages.advancedInterface)} - -
- ); - } - - return ( -
-
- -
- - {banner && -
- {banner} -
- } - -
- {signedIn && ( - <> - - - - - )} - - {trendsEnabled ? ( - - ) : ( - - )} - - {(signedIn || timelinePreview) && ( - - )} - - {!signedIn && ( -
-
- { disabledAccountId ? : } -
- )} - - {signedIn && ( - <> - - - - - - - -
- - - - {canManageReports(permissions) && } - {canViewAdminDashboard(permissions) && } - - )} - -
-
- -
-
- -
- - -
- ); - } - -} - -export default injectIntl(withIdentity(NavigationPanel)); diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.tsx b/app/javascript/mastodon/features/ui/components/navigation_panel.tsx new file mode 100644 index 0000000000..61e4f2c1b1 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.tsx @@ -0,0 +1,495 @@ +import { useEffect, useCallback, useRef } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; +import { Link, useLocation } from 'react-router-dom'; + +import type { Map as ImmutableMap } from 'immutable'; + +import { animated, useSpring } from '@react-spring/web'; +import { useDrag } from '@use-gesture/react'; + +import AddIcon from '@/material-icons/400-24px/add.svg?react'; +import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; +import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; +import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react'; +import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react'; +import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; +import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react'; +import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; +import HomeIcon from '@/material-icons/400-24px/home.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; +import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react'; +import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; +import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; +import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; +import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; +import PublicIcon from '@/material-icons/400-24px/public.svg?react'; +import SearchIcon from '@/material-icons/400-24px/search.svg?react'; +import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; +import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react'; +import StarIcon from '@/material-icons/400-24px/star.svg?react'; +import { fetchFollowRequests } from 'mastodon/actions/accounts'; +import { openModal } from 'mastodon/actions/modal'; +import { openNavigation, closeNavigation } from 'mastodon/actions/navigation'; +import { Account } from 'mastodon/components/account'; +import { IconButton } from 'mastodon/components/icon_button'; +import { IconWithBadge } from 'mastodon/components/icon_with_badge'; +import { WordmarkLogo } from 'mastodon/components/logo'; +import { NavigationPortal } from 'mastodon/components/navigation_portal'; +import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint'; +import { useIdentity } from 'mastodon/identity_context'; +import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state'; +import { transientSingleColumn } from 'mastodon/is_mobile'; +import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; +import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import { ColumnLink } from './column_link'; +import DisabledAccountBanner from './disabled_account_banner'; +import { ListPanel } from './list_panel'; +import SignInBanner from './sign_in_banner'; + +const messages = defineMessages({ + home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + notifications: { + id: 'tabs_bar.notifications', + defaultMessage: 'Notifications', + }, + explore: { id: 'explore.title', defaultMessage: 'Explore' }, + firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, + direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, + preferences: { + id: 'navigation_bar.preferences', + defaultMessage: 'Preferences', + }, + administration: { + id: 'navigation_bar.administration', + defaultMessage: 'Administration', + }, + moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' }, + followsAndFollowers: { + id: 'navigation_bar.follows_and_followers', + defaultMessage: 'Follows and followers', + }, + about: { id: 'navigation_bar.about', defaultMessage: 'About' }, + search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, + advancedInterface: { + id: 'navigation_bar.advanced_interface', + defaultMessage: 'Open in advanced web interface', + }, + openedInClassicInterface: { + id: 'navigation_bar.opened_in_classic_interface', + defaultMessage: + 'Posts, accounts, and other specific pages are opened by default in the classic web interface.', + }, + followRequests: { + id: 'navigation_bar.follow_requests', + defaultMessage: 'Follow requests', + }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + compose: { id: 'tabs_bar.publish', defaultMessage: 'New Post' }, +}); + +const NotificationsLink = () => { + const count = useAppSelector(selectUnreadNotificationGroupsCount); + const intl = useIntl(); + + return ( + + } + activeIcon={ + + } + text={intl.formatMessage(messages.notifications)} + /> + ); +}; + +const FollowRequestsLink: React.FC = () => { + const intl = useIntl(); + const count = useAppSelector( + (state) => + ( + state.user_lists.getIn(['follow_requests', 'items']) as + | ImmutableMap + | undefined + )?.size ?? 0, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(fetchFollowRequests()); + }, [dispatch]); + + if (count === 0) { + return null; + } + + return ( + + } + activeIcon={ + + } + text={intl.formatMessage(messages.followRequests)} + /> + ); +}; + +const SearchLink: React.FC = () => { + const intl = useIntl(); + const showAsSearch = useBreakpoint('full'); + + if (!trendsEnabled || showAsSearch) { + return ( + + ); + } + + return ( + + ); +}; + +const ProfileCard: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const handleLogoutClick = useCallback(() => { + dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} })); + }, [dispatch]); + + if (!me) { + return null; + } + + return ( +
+ + +
+ ); +}; + +const MENU_WIDTH = 284; + +export const NavigationPanel: React.FC = () => { + const intl = useIntl(); + const { signedIn, disabledAccountId, permissions } = useIdentity(); + const open = useAppSelector((state) => state.navigation.open); + const dispatch = useAppDispatch(); + const openable = useBreakpoint('openable'); + const location = useLocation(); + const overlayRef = useRef(null); + + useEffect(() => { + dispatch(closeNavigation()); + }, [dispatch, location]); + + useEffect(() => { + const handleDocumentClick = (e: MouseEvent) => { + if (overlayRef.current && e.target === overlayRef.current) { + dispatch(closeNavigation()); + } + }; + + const handleDocumentKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + dispatch(closeNavigation()); + } + }; + + document.addEventListener('click', handleDocumentClick); + document.addEventListener('keyup', handleDocumentKeyUp); + + return () => { + document.removeEventListener('click', handleDocumentClick); + document.removeEventListener('keyup', handleDocumentKeyUp); + }; + }, [dispatch]); + + const [{ x }, spring] = useSpring( + () => ({ + x: open ? 0 : MENU_WIDTH, + onRest: { + x({ value }: { value: number }) { + if (value === 0) { + dispatch(openNavigation()); + } else if (value > 0) { + dispatch(closeNavigation()); + } + }, + }, + }), + [open], + ); + + const bind = useDrag( + ({ last, offset: [ox], velocity: [vx], direction: [dx], cancel }) => { + if (ox < -70) { + cancel(); + } + + if (last) { + if (ox > MENU_WIDTH / 2 || (vx > 0.5 && dx > 0)) { + void spring.start({ x: MENU_WIDTH }); + } else { + void spring.start({ x: 0 }); + } + } else { + void spring.start({ x: ox, immediate: true }); + } + }, + { + from: () => [x.get(), 0], + filterTaps: true, + bounds: { left: 0 }, + rubberband: true, + }, + ); + + const isFirehoseActive = useCallback( + (match: unknown, location: { pathname: string }): boolean => { + return !!match || location.pathname.startsWith('/public'); + }, + [], + ); + + const previouslyFocusedElementRef = useRef(); + + useEffect(() => { + if (open) { + const firstLink = document.querySelector( + '.navigation-panel__menu .column-link', + ); + previouslyFocusedElementRef.current = + document.activeElement as HTMLElement; + firstLink?.focus(); + } else { + previouslyFocusedElementRef.current?.focus(); + } + }, [open]); + + let banner = undefined; + + if (transientSingleColumn) { + banner = ( +
+ {intl.formatMessage(messages.openedInClassicInterface)}{' '} + + {intl.formatMessage(messages.advancedInterface)} + +
+ ); + } + + const showOverlay = openable && open; + + return ( +
+ +
+
+ + + +
+ + + + {banner &&
{banner}
} + +
+ {signedIn && ( + <> + + + + + + )} + + + + {(signedIn || timelinePreview) && ( + + )} + + {!signedIn && ( +
+
+ {disabledAccountId ? ( + + ) : ( + + )} +
+ )} + + {signedIn && ( + <> + + + + + + +
+ + + + {canManageReports(permissions) && ( + + )} + {canViewAdminDashboard(permissions) && ( + + )} + + )} + +
+
+ +
+
+ +
+ + +
+ +
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx b/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx new file mode 100644 index 0000000000..af96ab3766 --- /dev/null +++ b/app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; + +const breakpoints = { + openable: 759, // Device width at which the sidebar becomes an openable hamburger menu + full: 1174, // Device width at which all 3 columns can be displayed +}; + +type Breakpoint = 'openable' | 'full'; + +export const useBreakpoint = (breakpoint: Breakpoint) => { + const [isMatching, setIsMatching] = useState(false); + + useEffect(() => { + const mediaWatcher = window.matchMedia( + `(max-width: ${breakpoints[breakpoint]}px)`, + ); + + setIsMatching(mediaWatcher.matches); + + const handleChange = (e: MediaQueryListEvent) => { + setIsMatching(e.matches); + }; + + mediaWatcher.addEventListener('change', handleChange); + + return () => { + mediaWatcher.removeEventListener('change', handleChange); + }; + }, [breakpoint, setIsMatching]); + + return isMatching; +}; + +interface WithBreakpointType { + matchesBreakpoint: boolean; +} + +export function withBreakpoint

( + Component: React.ComponentType

, + breakpoint: Breakpoint = 'full', +) { + const displayName = `withMobileLayout(${Component.displayName ?? Component.name})`; + + const ComponentWithBreakpoint = (props: P) => { + const matchesBreakpoint = useBreakpoint(breakpoint); + + return ; + }; + + ComponentWithBreakpoint.displayName = displayName; + + return ComponentWithBreakpoint; +} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 7c4f45721d..4297d750c5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -29,7 +29,7 @@ import { expandHomeTimeline } from '../../actions/timelines'; import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; -import Header from './components/header'; +import { NavigationBar } from './components/navigation_bar'; import { UploadArea } from './components/upload_area'; import { HashtagMenuController } from './components/hashtag_menu_controller'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -603,12 +603,11 @@ class UI extends PureComponent { return (

-
- {children} + {layout !== 'mobile' && } {!disableHoverCards && } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 589aae2e55..ab7ad7cfdd 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -207,7 +207,6 @@ "compose_form.poll.switch_to_single": "Change poll to allow for a single choice", "compose_form.poll.type": "Style", "compose_form.publish": "Post", - "compose_form.publish_form": "New post", "compose_form.reply": "Reply", "compose_form.save_changes": "Update", "compose_form.spoiler.marked": "Remove content warning", @@ -579,6 +578,8 @@ "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.search": "Search", "navigation_bar.security": "Security", + "navigation_panel.collapse_lists": "Collapse list menu", + "navigation_panel.expand_lists": "Expand list menu", "not_signed_in_indicator.not_signed_in": "You need to login to access this resource.", "notification.admin.report": "{name} reported {target}", "notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}", @@ -907,7 +908,10 @@ "subscribed_languages.save": "Save changes", "subscribed_languages.target": "Change subscribed languages for {target}", "tabs_bar.home": "Home", + "tabs_bar.menu": "Menu", "tabs_bar.notifications": "Notifications", + "tabs_bar.publish": "New Post", + "tabs_bar.search": "Search", "terms_of_service.effective_as_of": "Effective as of {date}", "terms_of_service.title": "Terms of Service", "terms_of_service.upcoming_changes_on": "Upcoming changes on {date}", diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index d35d166115..9c3583c7a3 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -21,6 +21,7 @@ import { markersReducer } from './markers'; import media_attachments from './media_attachments'; import meta from './meta'; import { modalReducer } from './modal'; +import { navigationReducer } from './navigation'; import { notificationGroupsReducer } from './notification_groups'; import { notificationPolicyReducer } from './notification_policy'; import { notificationRequestsReducer } from './notification_requests'; @@ -76,6 +77,7 @@ const reducers = { history, notificationPolicy: notificationPolicyReducer, notificationRequests: notificationRequestsReducer, + navigation: navigationReducer, }; // We want the root state to be an ImmutableRecord, which is an object with a defined list of keys, diff --git a/app/javascript/mastodon/reducers/navigation.ts b/app/javascript/mastodon/reducers/navigation.ts new file mode 100644 index 0000000000..3f245603a1 --- /dev/null +++ b/app/javascript/mastodon/reducers/navigation.ts @@ -0,0 +1,28 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { + openNavigation, + closeNavigation, + toggleNavigation, +} from 'mastodon/actions/navigation'; + +interface State { + open: boolean; +} + +const initialState: State = { + open: false, +}; + +export const navigationReducer = createReducer(initialState, (builder) => { + builder + .addCase(openNavigation, (state) => { + state.open = true; + }) + .addCase(closeNavigation, (state) => { + state.open = false; + }) + .addCase(toggleNavigation, (state) => { + state.open = !state.open; + }); +}); diff --git a/app/javascript/mastodon/selectors/lists.ts b/app/javascript/mastodon/selectors/lists.ts index f93e90ce68..9b79a880a9 100644 --- a/app/javascript/mastodon/selectors/lists.ts +++ b/app/javascript/mastodon/selectors/lists.ts @@ -1,15 +1,16 @@ -import { createSelector } from '@reduxjs/toolkit'; -import type { Map as ImmutableMap } from 'immutable'; +import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import type { List } from 'mastodon/models/list'; -import type { RootState } from 'mastodon/store'; +import { createAppSelector } from 'mastodon/store'; -export const getOrderedLists = createSelector( - [(state: RootState) => state.lists], - (lists: ImmutableMap) => - lists - .toList() - .filter((item: List | null) => !!item) - .sort((a: List, b: List) => a.title.localeCompare(b.title)) - .toArray(), +const getLists = createAppSelector( + [(state) => state.lists], + (lists: ImmutableMap): ImmutableList => + lists.toList().filter((item: List | null): item is List => !!item), +); + +export const getOrderedLists = createAppSelector( + [(state) => getLists(state)], + (lists) => + lists.sort((a: List, b: List) => a.title.localeCompare(b.title)).toArray(), ); diff --git a/app/javascript/material-icons/400-24px/arrow_left-fill.svg b/app/javascript/material-icons/400-24px/arrow_left-fill.svg new file mode 100644 index 0000000000..bf9b2aef3f --- /dev/null +++ b/app/javascript/material-icons/400-24px/arrow_left-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/arrow_left.svg b/app/javascript/material-icons/400-24px/arrow_left.svg new file mode 100644 index 0000000000..bf9b2aef3f --- /dev/null +++ b/app/javascript/material-icons/400-24px/arrow_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/edit_square-fill.svg b/app/javascript/material-icons/400-24px/edit_square-fill.svg new file mode 100644 index 0000000000..4f931de0f2 --- /dev/null +++ b/app/javascript/material-icons/400-24px/edit_square-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/edit_square.svg b/app/javascript/material-icons/400-24px/edit_square.svg new file mode 100644 index 0000000000..dccfaa9f3c --- /dev/null +++ b/app/javascript/material-icons/400-24px/edit_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/unfold_less-fill.svg b/app/javascript/material-icons/400-24px/unfold_less-fill.svg new file mode 100644 index 0000000000..8136d615b2 --- /dev/null +++ b/app/javascript/material-icons/400-24px/unfold_less-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/unfold_less.svg b/app/javascript/material-icons/400-24px/unfold_less.svg new file mode 100644 index 0000000000..8136d615b2 --- /dev/null +++ b/app/javascript/material-icons/400-24px/unfold_less.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/unfold_more-fill.svg b/app/javascript/material-icons/400-24px/unfold_more-fill.svg new file mode 100644 index 0000000000..3e245d2090 --- /dev/null +++ b/app/javascript/material-icons/400-24px/unfold_more-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/unfold_more.svg b/app/javascript/material-icons/400-24px/unfold_more.svg new file mode 100644 index 0000000000..3e245d2090 --- /dev/null +++ b/app/javascript/material-icons/400-24px/unfold_more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f732f85922..4eeef5c8f7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2644,15 +2644,13 @@ a.account__display-name { min-width: 0; &__display-name { - font-size: 16px; - line-height: 24px; - letter-spacing: 0.15px; + font-size: 14px; + line-height: 20px; font-weight: 500; .display-name__account { font-size: 14px; - line-height: 20px; - letter-spacing: 0.1px; + font-weight: 400; } } } @@ -2889,67 +2887,69 @@ a.account__display-name { } } -$ui-header-height: 55px; -$ui-header-logo-wordmark-width: 99px; - -.ui__header { - display: none; - box-sizing: border-box; - height: $ui-header-height; +.ui__navigation-bar { position: sticky; - top: 0; - z-index: 3; - justify-content: space-between; - align-items: center; + bottom: 0; + background: var(--background-color); backdrop-filter: var(--background-filter); + border-top: 1px solid var(--background-border-color); + z-index: 3; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding-bottom: env(safe-area-inset-bottom); - &__logo { - display: inline-flex; - padding: 15px; - flex-grow: 1; - flex-shrink: 1; - overflow: hidden; - container: header-logo / inline-size; + .layout-multiple-columns & { + display: none; + } - .logo { - height: $ui-header-height - 30px; - width: auto; - } + &__items { + display: grid; + grid-auto-columns: minmax(0, 1fr); + grid-auto-flow: column; + padding: 0 16px; - .logo--wordmark { - display: none; - } - - @container header-logo (min-width: #{$ui-header-logo-wordmark-width}) { - .logo--wordmark { - display: block; - } - - .logo--icon { - display: none; - } + &.active { + flex: 1; + padding: 0; } } - &__links { + &__sign-up { display: flex; align-items: center; - gap: 10px; - padding: 0 10px; - overflow: hidden; - flex-shrink: 0; + gap: 4px; + padding-inline-start: 16px; + } - .button { - flex: 0 0 auto; + &__item { + display: flex; + flex-direction: column; + align-items: center; + background: transparent; + border: none; + gap: 8px; + font-size: 12px; + font-weight: 500; + line-height: 16px; + padding-top: 11px; + padding-bottom: 15px; + border-top: 4px solid transparent; + text-decoration: none; + color: inherit; + + &.active { + color: $highlight-text-color; } - .button-tertiary { - flex-shrink: 1; + &:focus { + outline: 0; } - .icon { - width: 22px; - height: 22px; + &:focus-visible { + border-top-color: $ui-button-focus-outline-color; + border-radius: 0; } } } @@ -2958,13 +2958,12 @@ $ui-header-logo-wordmark-width: 99px; background: var(--background-color); backdrop-filter: var(--background-filter); position: sticky; - top: $ui-header-height; + top: 0; z-index: 2; padding-top: 0; @media screen and (min-width: $no-gap-breakpoint) { padding-top: 10px; - top: 0; } } @@ -3133,8 +3132,10 @@ $ui-header-logo-wordmark-width: 99px; display: none; } - .navigation-panel__legal { - display: none; + .navigation-panel__legal, + .navigation-panel__compose-button, + .navigation-panel .navigation-bar { + display: none !important; } } @@ -3146,7 +3147,7 @@ $ui-header-logo-wordmark-width: 99px; } .columns-area__panels { - min-height: calc(100vh - $ui-header-height); + min-height: 100vh; gap: 0; } @@ -3164,24 +3165,14 @@ $ui-header-logo-wordmark-width: 99px; } .navigation-panel__sign-in-banner, - .navigation-panel__logo, .navigation-panel__banner, - .getting-started__trends { + .getting-started__trends, + .navigation-panel__logo { display: none; } - - .column-link__icon { - font-size: 18px; - } } .layout-single-column { - .ui__header { - display: flex; - background: var(--background-color); - border-bottom: 1px solid var(--background-border-color); - } - .column > .scrollable, .tabs-bar__wrapper .column-header, .tabs-bar__wrapper .column-back-button, @@ -3205,30 +3196,64 @@ $ui-header-logo-wordmark-width: 99px; } } -@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) { - $sidebar-width: 55px; - +@media screen and (width <= 759px) { .columns-area__panels__main { - width: calc(100% - $sidebar-width); + width: 100%; } .columns-area__panels__pane--navigational { - min-width: $sidebar-width; + position: fixed; + inset-inline-end: 0; + width: 100%; + height: 100%; + pointer-events: none; + } - .columns-area__panels__pane__inner { - width: $sidebar-width; - } + .columns-area__panels__pane--navigational .columns-area__panels__pane__inner { + pointer-events: auto; + background: var(--background-color); + position: fixed; + width: 284px + 70px; + inset-inline-end: -70px; + touch-action: pan-y; - .column-link span { - display: none; - } + .navigation-panel { + width: 284px; + overflow-y: auto; - .list-panel { - display: none; + &__menu { + flex-shrink: 0; + min-height: none; + overflow: hidden; + padding-bottom: calc(65px + env(safe-area-inset-bottom)); + } + + &__logo { + display: none; + } } } } +.columns-area__panels__pane--navigational { + transition: background 500ms; +} + +.columns-area__panels__pane--overlay { + pointer-events: auto; + background: rgba($base-overlay-background, 0.5); + + .columns-area__panels__pane__inner { + box-shadow: var(--dropdown-shadow); + } +} + +@media screen and (width >= 760px) { + .ui__navigation-bar { + display: none; + } +} + .explore__suggestions__card { padding: 12px 16px; gap: 8px; @@ -3455,6 +3480,49 @@ $ui-header-logo-wordmark-width: 99px; overflow-y: auto; } + &__list-panel { + &__header { + display: flex; + align-items: center; + padding-inline-end: 12px; + + .column-link { + flex: 1 1 auto; + } + } + + &__items { + padding-inline-start: 24px + 5px; + + .icon { + display: none; + } + } + } + + &__compose-button { + display: flex; + justify-content: flex-start; + padding-top: 10px; + padding-bottom: 10px; + padding-inline-start: 13px - 7px; + padding-inline-end: 13px; + gap: 5px; + margin: 12px; + margin-bottom: 4px; + border-radius: 6px; + + .icon { + width: 24px; + height: 24px; + } + } + + .navigation-bar { + padding: 16px; + border-bottom: 1px solid var(--background-border-color); + } + .logo { height: 30px; width: auto; @@ -3487,12 +3555,6 @@ $ui-header-logo-wordmark-width: 99px; display: none; } } - - @media screen and (height <= 1040px) { - .list-panel { - display: none; - } - } } .navigation-panel, @@ -4336,6 +4398,10 @@ a.status-card { &:focus-visible { outline: $ui-button-icon-focus-outline; } + + .logo { + height: 24px; + } } .column-header__back-button + &__title { @@ -4419,10 +4485,6 @@ a.status-card { &:hover { color: $primary-text-color; } - - .icon-sliders { - transform: rotate(60deg); - } } &:disabled { From 0cdf11d6ad0d1ceefae0c4a82523a8b6f61072b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:12:53 +0200 Subject: [PATCH 03/10] fix(deps): update dependency postcss-preset-env to v10.2.3 (#35009) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 72c8f94439..93e6e26551 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1416,15 +1416,15 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-is-pseudo-class@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-is-pseudo-class@npm:5.0.2" +"@csstools/postcss-is-pseudo-class@npm:^5.0.3": + version: 5.0.3 + resolution: "@csstools/postcss-is-pseudo-class@npm:5.0.3" dependencies: "@csstools/selector-specificity": "npm:^5.0.0" postcss-selector-parser: "npm:^7.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/79b4149b9718799c240b72680f5a37dd4b04a50d8b4a05b3fb35b921d03b2338a7ffa1324019681101c1023076bb0d4950cec1da49cd305e9ab154aad199ff92 + checksum: 10c0/7980f1cabf32850bac72552e4e9de47412359e36e259a92b9b9af25dae4cce42bbcc5fdca8f384a589565bf383ecb23dec3af9f084d8df18b82552318b2841b6 languageName: node linkType: hard @@ -10260,8 +10260,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.1.5": - version: 10.2.2 - resolution: "postcss-preset-env@npm:10.2.2" + version: 10.2.3 + resolution: "postcss-preset-env@npm:10.2.3" dependencies: "@csstools/postcss-cascade-layers": "npm:^5.0.1" "@csstools/postcss-color-function": "npm:^4.0.10" @@ -10275,7 +10275,7 @@ __metadata: "@csstools/postcss-hwb-function": "npm:^4.0.10" "@csstools/postcss-ic-unit": "npm:^4.0.2" "@csstools/postcss-initial": "npm:^2.0.1" - "@csstools/postcss-is-pseudo-class": "npm:^5.0.2" + "@csstools/postcss-is-pseudo-class": "npm:^5.0.3" "@csstools/postcss-light-dark-function": "npm:^2.0.9" "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" "@csstools/postcss-logical-overflow": "npm:^2.0.0" @@ -10329,7 +10329,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/314774ed0cfb2880d82d0056e1a26ab5c04b9e5584e5c41045231374a7e080eda7ed6e5506fbc1c1c6bc7a05700576bfb0b5f334ae25f125bcbb772e2aa337e8 + checksum: 10c0/f3d2ea8b95083acad2cf74aca93904dd3158639bf692d1d471598b538e0c6b4447ae306e7bc1c2426dd465e7c9715373678855b7e211e194b507ef8184e83f99 languageName: node linkType: hard From 933ee420c311b04243d1ce20afe7a544889a57bc Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 11 Jun 2025 15:17:07 +0200 Subject: [PATCH 04/10] Remove some unused CSS classes (#35012) --- .../styles/mastodon-light/diff.scss | 37 ------------- app/javascript/styles/mastodon/admin.scss | 10 ---- .../styles/mastodon/components.scss | 55 ------------------- 3 files changed, 102 deletions(-) diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index c6e3f42799..ebaac66a5e 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -283,36 +283,6 @@ } } -.activity-stream { - border: 1px solid var(--background-border-color); - - &--under-tabs { - border-top: 0; - } - - .entry { - background: $white; - - .detailed-status.light, - .more.light, - .status.light { - border-bottom-color: lighten($ui-base-color, 8%); - } - } - - .status.light { - .status__content { - color: $primary-text-color; - } - - .display-name { - strong { - color: $primary-text-color; - } - } - } -} - .accounts-grid { .account-grid-card { .controls { @@ -416,13 +386,6 @@ } } -.mute-modal select { - border: 1px solid var(--background-border-color); - background: $simple-background-color - url("data:image/svg+xml;utf8,") - no-repeat right 8px center / auto 16px; -} - .status__wrapper-direct { background-color: rgba($ui-highlight-color, 0.1); diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index ef57fbbd71..add030626d 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -585,16 +585,6 @@ body, .account-status { display: flex; margin-bottom: 10px; - - .activity-stream { - flex: 2 0 0; - margin-inline-end: 20px; - max-width: calc(100% - 60px); - - .entry { - border-radius: 4px; - } - } } .report-status__actions, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4eeef5c8f7..8e4c721ed7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1458,43 +1458,6 @@ body > [data-popper-placement] { margin-top: 16px; } - &.light { - .status__relative-time, - .status__visibility-icon { - color: $light-text-color; - } - - .status__display-name { - color: $inverted-text-color; - } - - .display-name { - color: $light-text-color; - - strong { - color: $inverted-text-color; - } - } - - .status__content { - color: $inverted-text-color; - - a { - color: $highlight-text-color; - } - - &__spoiler-link { - color: $primary-text-color; - background: $ui-primary-color; - - &:hover, - &:focus { - background: lighten($ui-primary-color, 8%); - } - } - } - } - &--is-quote { border: none; } @@ -6710,24 +6673,6 @@ a.status-card { } } } - - select { - appearance: none; - box-sizing: border-box; - font-size: 14px; - color: $inverted-text-color; - display: inline-block; - width: auto; - outline: 0; - font-family: inherit; - background: $simple-background-color - url("data:image/svg+xml;utf8,") - no-repeat right 8px center / auto 16px; - border: 1px solid darken($simple-background-color, 14%); - border-radius: 4px; - padding: 6px 10px; - padding-inline-end: 30px; - } } .report-modal__target { From 722fb1ff55c5c3c94294cbc1c39c7084696de6f8 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 11 Jun 2025 15:17:42 +0200 Subject: [PATCH 05/10] Fix quoted posts appearing between text and media (#35011) --- app/javascript/mastodon/components/status.jsx | 4 ++-- .../mastodon/features/status/components/detailed_status.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 39b6e89902..0f9d751eb3 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -588,10 +588,10 @@ class Status extends ImmutablePureComponent { {...statusContentProps} /> - {children} - {media} {hashtagBar} + + {children} )} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index 650e439348..ec6aa003e2 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -377,12 +377,12 @@ export const DetailedStatus: React.FC<{ {...(statusContentProps as any)} /> + {media} + {hashtagBar} + {status.get('quote') && ( )} - - {media} - {hashtagBar} )} From 1623d54ec01c0ec779a8fe900ffa07fa29908ad1 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 11 Jun 2025 15:37:59 +0200 Subject: [PATCH 06/10] Start local prometheus_exporter server only in puma/sidekiq startup (#35005) --- config/initializers/prometheus_exporter.rb | 10 +++------- config/initializers/sidekiq.rb | 6 ++++++ config/puma.rb | 6 ++++++ .../prometheus_exporter/local_server.rb | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 lib/mastodon/prometheus_exporter/local_server.rb diff --git a/config/initializers/prometheus_exporter.rb b/config/initializers/prometheus_exporter.rb index fdfee59dc8..16b408977d 100644 --- a/config/initializers/prometheus_exporter.rb +++ b/config/initializers/prometheus_exporter.rb @@ -5,16 +5,12 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' require 'prometheus_exporter/middleware' if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' - require 'prometheus_exporter/server' - require 'prometheus_exporter/client' + require 'mastodon/prometheus_exporter/local_server' # bind is the address, on which the webserver will listen # port is the port that will provide the /metrics route - server = PrometheusExporter::Server::WebServer.new bind: ENV.fetch('MASTODON_PROMETHEUS_EXPORTER_HOST', 'localhost'), port: ENV.fetch('MASTODON_PROMETHEUS_EXPORTER_PORT', '9394').to_i - server.start - - # wire up a default local client - PrometheusExporter::Client.default = PrometheusExporter::LocalClient.new(collector: server.collector) + Mastodon::PrometheusExporter::LocalServer.bind = ENV.fetch('MASTODON_PROMETHEUS_EXPORTER_HOST', 'localhost') + Mastodon::PrometheusExporter::LocalServer.port = ENV.fetch('MASTODON_PROMETHEUS_EXPORTER_PORT', '9394').to_i end if ENV['MASTODON_PROMETHEUS_EXPORTER_WEB_DETAILED_METRICS'] == 'true' diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index b2ebdbf078..3c2f12780c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -26,6 +26,12 @@ Sidekiq.configure_server do |config| require 'prometheus_exporter' require 'prometheus_exporter/instrumentation' + if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' + config.on :startup do + Mastodon::PrometheusExporter::LocalServer.setup! + end + end + config.on :startup do # Ruby process metrics (memory, GC, etc) PrometheusExporter::Instrumentation::Process.start type: 'sidekiq' diff --git a/config/puma.rb b/config/puma.rb index 4fe8f802b9..16c481a2ae 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -21,6 +21,12 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' require 'prometheus_exporter' require 'prometheus_exporter/instrumentation' + if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' + before_fork do + Mastodon::PrometheusExporter::LocalServer.setup! + end + end + on_worker_boot do # Ruby process metrics (memory, GC, etc) PrometheusExporter::Instrumentation::Process.start(type: 'puma') diff --git a/lib/mastodon/prometheus_exporter/local_server.rb b/lib/mastodon/prometheus_exporter/local_server.rb new file mode 100644 index 0000000000..31954a473b --- /dev/null +++ b/lib/mastodon/prometheus_exporter/local_server.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'prometheus_exporter/server' +require 'prometheus_exporter/client' + +module Mastodon::PrometheusExporter + module LocalServer + mattr_accessor :bind, :port + + def self.setup! + server = PrometheusExporter::Server::WebServer.new(bind:, port:) + server.start + + # wire up a default local client + PrometheusExporter::Client.default = PrometheusExporter::LocalClient.new(collector: server.collector) + end + end +end From 2c828748a3fd7dd1d5e63d73547b38babf425e0a Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 11 Jun 2025 17:15:12 +0200 Subject: [PATCH 07/10] fix: Fix cramped layout of follower recommendations on small viewports (#34967) --- .../mastodon/components/account.tsx | 117 +++++++++-------- app/javascript/mastodon/components/avatar.tsx | 4 +- .../features/explore/components/card.jsx | 75 ----------- .../features/explore/components/card.tsx | 124 ++++++++++++++++++ .../mastodon/features/onboarding/follows.tsx | 2 +- .../styles/mastodon/components.scss | 114 ++++++++++------ 6 files changed, 266 insertions(+), 170 deletions(-) delete mode 100644 app/javascript/mastodon/features/explore/components/card.jsx create mode 100644 app/javascript/mastodon/features/explore/components/card.tsx diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx index 375f759f42..8397695a44 100644 --- a/app/javascript/mastodon/components/account.tsx +++ b/app/javascript/mastodon/components/account.tsx @@ -71,6 +71,7 @@ interface AccountProps { minimal?: boolean; defaultAction?: 'block' | 'mute'; withBio?: boolean; + withMenu?: boolean; } export const Account: React.FC = ({ @@ -80,6 +81,7 @@ export const Account: React.FC = ({ minimal, defaultAction, withBio, + withMenu = true, }) => { const intl = useIntl(); const { signedIn } = useIdentity(); @@ -225,9 +227,10 @@ export const Account: React.FC = ({ ); } - let button: React.ReactNode, dropdown: React.ReactNode; + let button: React.ReactNode; + let dropdown: React.ReactNode; - if (menu.length > 0) { + if (menu.length > 0 && withMenu) { dropdown = ( = ({ } return ( -
-
- -
- {account ? ( - +
+
+
+ +
+ {account ? ( + + ) : ( + + )} +
+ +
+ + + {!minimal && ( +
+ {account ? ( + <> + {' '} + {verification} {muteTimeRemaining} + + ) : ( + + )} +
+ )} +
+ + + {account && + withBio && + (account.note.length > 0 ? ( +
) : ( - - )} -
- -
- - - {!minimal && ( -
- {account ? ( - <> - {' '} - {verification} {muteTimeRemaining} - - ) : ( - - )} +
+
- )} -
- + ))} +
{!minimal && (
@@ -323,22 +352,6 @@ export const Account: React.FC = ({
)}
- - {account && - withBio && - (account.note.length > 0 ? ( -
- ) : ( -
- -
- ))}
); }; diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index fb331813a9..2ae66ffa11 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -18,6 +18,7 @@ interface Props { withLink?: boolean; counter?: number | string; counterBorderColor?: string; + className?: string; } export const Avatar: React.FC = ({ @@ -27,6 +28,7 @@ export const Avatar: React.FC = ({ inline = false, withLink = false, style: styleFromParent, + className, counter, counterBorderColor, }) => { @@ -52,7 +54,7 @@ export const Avatar: React.FC = ({ const avatar = (
{ - const intl = useIntl(); - const account = useSelector(state => state.getIn(['accounts', id])); - const dispatch = useDispatch(); - - const handleDismiss = useCallback(() => { - dispatch(dismissSuggestion({ accountId: id })); - }, [id, dispatch]); - - let label; - - switch (source) { - case 'friends_of_friends': - label = ; - break; - case 'similar_to_recently_followed': - label = ; - break; - case 'featured': - label = ; - break; - case 'most_followed': - label = ; - break; - case 'most_interactions': - label = ; - break; - } - - return ( -
-
- {label} -
- -
- - -
-
- - - -
-
-
-
- ); -}; - -Card.propTypes = { - id: PropTypes.string.isRequired, - source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']), -}; diff --git a/app/javascript/mastodon/features/explore/components/card.tsx b/app/javascript/mastodon/features/explore/components/card.tsx new file mode 100644 index 0000000000..9cf128100e --- /dev/null +++ b/app/javascript/mastodon/features/explore/components/card.tsx @@ -0,0 +1,124 @@ +import { useCallback } from 'react'; + +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { dismissSuggestion } from 'mastodon/actions/suggestions'; +import { Avatar } from 'mastodon/components/avatar'; +import { DisplayName } from 'mastodon/components/display_name'; +import { FollowButton } from 'mastodon/components/follow_button'; +import { IconButton } from 'mastodon/components/icon_button'; +import { domain } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + dismiss: { + id: 'follow_suggestions.dismiss', + defaultMessage: "Don't show again", + }, +}); + +type SuggestionSource = + | 'friends_of_friends' + | 'similar_to_recently_followed' + | 'featured' + | 'most_followed' + | 'most_interactions'; + +export const Card: React.FC<{ id: string; source: SuggestionSource }> = ({ + id, + source, +}) => { + const intl = useIntl(); + const account = useAppSelector((state) => state.accounts.get(id)); + const dispatch = useAppDispatch(); + + const handleDismiss = useCallback(() => { + void dispatch(dismissSuggestion({ accountId: id })); + }, [id, dispatch]); + + let label; + + switch (source) { + case 'friends_of_friends': + label = ( + + ); + break; + case 'similar_to_recently_followed': + label = ( + + ); + break; + case 'featured': + label = ( + + ); + break; + case 'most_followed': + label = ( + + ); + break; + case 'most_interactions': + label = ( + + ); + break; + } + + if (!account) { + return null; + } + + return ( +
+
{label}
+ +
+ + + + +
+ + +
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/onboarding/follows.tsx b/app/javascript/mastodon/features/onboarding/follows.tsx index 703a4449be..d30834d0b6 100644 --- a/app/javascript/mastodon/features/onboarding/follows.tsx +++ b/app/javascript/mastodon/features/onboarding/follows.tsx @@ -170,7 +170,7 @@ export const Follows: React.FC<{ } > {displayedAccountIds.map((accountId) => ( - + ))} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8e4c721ed7..c56fbd6365 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2133,6 +2133,16 @@ body > [data-popper-placement] { display: flex; gap: 10px; align-items: center; + justify-content: end; +} + +.account__wrapper--with-bio { + align-items: start; +} + +.account__info-wrapper { + flex: 1 1 auto; + min-width: 0; } .account__avatar { @@ -2141,6 +2151,11 @@ body > [data-popper-placement] { border-radius: var(--avatar-border-radius); background: var(--surface-background-color); + @container (width < 360px) { + width: 35px !important; + height: 35px !important; + } + img { width: 100%; height: 100%; @@ -2266,7 +2281,7 @@ a .account__avatar { } .account__relationship, -.explore__suggestions__card { +.explore-suggestions-card { .icon-button { border: 1px solid var(--background-border-color); border-radius: 4px; @@ -3217,7 +3232,7 @@ a.account__display-name { } } -.explore__suggestions__card { +.explore-suggestions-card { padding: 12px 16px; gap: 8px; display: flex; @@ -3229,60 +3244,77 @@ a.account__display-name { } &__source { - padding-inline-start: 60px; font-size: 13px; line-height: 16px; color: $dark-text-color; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + + @container (width >= 400px) { + padding-inline-start: 60px; + } } &__body { display: flex; gap: 12px; align-items: center; + justify-content: end; + } - &__main { - flex: 1 1 auto; - display: flex; - flex-direction: column; - gap: 8px; - min-width: 0; + &__avatar { + flex-shrink: 0; - &__name-button { - display: flex; - align-items: center; - gap: 8px; + @container (width < 360px) { + width: 35px !important; + height: 35px !important; + } + } - &__name { - display: block; - color: inherit; - text-decoration: none; - flex: 1 1 auto; - min-width: 0; - } + &__link { + flex: 1 1 auto; + display: flex; + gap: 12px; + align-items: center; + text-decoration: none; + min-width: 0; - .button { - min-width: 80px; - } - - .display-name { - font-size: 15px; - line-height: 20px; - color: $secondary-text-color; - - strong { - font-weight: 700; - } - - &__account { - color: $darker-text-color; - display: block; - } - } + &:hover, + &:focus-visible { + .display-name__html { + text-decoration: underline; } } + + .display-name { + font-size: 15px; + line-height: 20px; + color: $secondary-text-color; + + strong { + font-weight: 700; + } + + &__account { + color: $darker-text-color; + display: block; + } + } + } + + &__actions { + display: flex; + align-items: center; + gap: 8px; + justify-content: end; + + .button { + min-width: 80px; + } + } + + &__dismiss-button { + @container (width < 400px) { + display: none; + } } } From 9896bed85feff10c23490dcaba145a1166f4c4a9 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 11 Jun 2025 17:17:14 +0200 Subject: [PATCH 08/10] fix: Fix direction of media gallery arrows (#35014) --- .../mastodon/features/ui/components/media_modal.jsx | 4 ++-- app/javascript/styles/mastodon/components.scss | 5 +++-- app/javascript/styles/mastodon/css_variables.scss | 9 +++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx index ed782aefe6..0190148555 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx @@ -168,8 +168,8 @@ class MediaModal extends ImmutablePureComponent { const index = this.getIndex(); - const leftNav = media.size > 1 && ; - const rightNav = media.size > 1 && ; + const leftNav = media.size > 1 && ; + const rightNav = media.size > 1 && ; const content = media.map((image, idx) => { const width = image.getIn(['meta', 'original', 'width']) || null; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c56fbd6365..89f71ef938 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5850,6 +5850,7 @@ a.status-card { position: absolute; top: 0; bottom: 0; + transform: scaleX(var(--text-x-direction)); &:hover, &:focus, @@ -5858,11 +5859,11 @@ a.status-card { } } -.media-modal__nav--left { +.media-modal__nav--prev { inset-inline-start: 0; } -.media-modal__nav--right { +.media-modal__nav--next { inset-inline-end: 0; } diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index 413efca3f6..4390a917bf 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -35,3 +35,12 @@ --input-background-color: var(--surface-variant-background-color); --on-input-color: #{$secondary-text-color}; } + +body { + // Variable for easily inverting directional UI elements, + --text-x-direction: 1; + + &.rtl { + --text-x-direction: -1; + } +} From f53bb4cd7d9eca4bc2b770cb6e71ecc29cb8868b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 11 Jun 2025 18:12:04 +0200 Subject: [PATCH 09/10] Add "More" to the sidebar menu with links to mutes, blocks, and so on (#34987) --- .../components/account_header.tsx | 51 ++----- .../compose/components/action_bar.tsx | 81 ----------- .../compose/components/navigation_bar.tsx | 7 +- .../features/ui/components/more_link.tsx | 126 ++++++++++++++++++ .../ui/components/navigation_panel.tsx | 33 +---- app/javascript/mastodon/locales/en.json | 5 + .../styles/mastodon/components.scss | 1 + spec/system/log_out_spec.rb | 4 +- 8 files changed, 151 insertions(+), 157 deletions(-) delete mode 100644 app/javascript/mastodon/features/compose/components/action_bar.tsx create mode 100644 app/javascript/mastodon/features/ui/components/more_link.tsx diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index a156e0cc36..18807ecf85 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -418,7 +418,7 @@ export const AccountHeader: React.FC<{ return arr; } - if (signedIn && account.id !== me && !account.suspended) { + if (signedIn && !account.suspended) { arr.push({ text: intl.formatMessage(messages.mention, { name: account.username, @@ -442,37 +442,7 @@ export const AccountHeader: React.FC<{ arr.push(null); } - if (account.id === me) { - arr.push({ - text: intl.formatMessage(messages.edit_profile), - href: '/settings/profile', - }); - arr.push({ - text: intl.formatMessage(messages.preferences), - href: '/settings/preferences', - }); - arr.push(null); - arr.push({ - text: intl.formatMessage(messages.follow_requests), - to: '/follow_requests', - }); - arr.push({ - text: intl.formatMessage(messages.favourites), - to: '/favourites', - }); - arr.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); - arr.push({ - text: intl.formatMessage(messages.followed_tags), - to: '/followed_tags', - }); - arr.push(null); - arr.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); - arr.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); - arr.push({ - text: intl.formatMessage(messages.domain_blocks), - to: '/domain_blocks', - }); - } else if (signedIn) { + if (signedIn) { if (relationship?.following) { if (!relationship.muting) { if (relationship.showing_reblogs) { @@ -611,8 +581,7 @@ export const AccountHeader: React.FC<{ } if ( - (account.id !== me && - (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || + (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) @@ -880,12 +849,14 @@ export const AccountHeader: React.FC<{
{!hidden && bellBtn} {!hidden && shareBtn} - + {accountId !== me && ( + + )} {!hidden && actionBtn}
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.tsx b/app/javascript/mastodon/features/compose/components/action_bar.tsx deleted file mode 100644 index 55e95fb5d8..0000000000 --- a/app/javascript/mastodon/features/compose/components/action_bar.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useMemo } from 'react'; - -import { defineMessages, useIntl } from 'react-intl'; - -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import { openModal } from 'mastodon/actions/modal'; -import { Dropdown } from 'mastodon/components/dropdown_menu'; -import { useAppDispatch } from 'mastodon/store'; - -const messages = defineMessages({ - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - preferences: { - id: 'navigation_bar.preferences', - defaultMessage: 'Preferences', - }, - follow_requests: { - id: 'navigation_bar.follow_requests', - defaultMessage: 'Follow requests', - }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - followed_tags: { - id: 'navigation_bar.followed_tags', - defaultMessage: 'Followed hashtags', - }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { - id: 'navigation_bar.domain_blocks', - defaultMessage: 'Blocked domains', - }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, -}); - -export const ActionBar: React.FC = () => { - const dispatch = useAppDispatch(); - const intl = useIntl(); - - const menu = useMemo(() => { - const handleLogoutClick = () => { - dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} })); - }; - - return [ - { - text: intl.formatMessage(messages.edit_profile), - href: '/settings/profile', - }, - { - text: intl.formatMessage(messages.preferences), - href: '/settings/preferences', - }, - null, - { - text: intl.formatMessage(messages.follow_requests), - to: '/follow_requests', - }, - { text: intl.formatMessage(messages.favourites), to: '/favourites' }, - { text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }, - { text: intl.formatMessage(messages.lists), to: '/lists' }, - { - text: intl.formatMessage(messages.followed_tags), - to: '/followed_tags', - }, - null, - { text: intl.formatMessage(messages.mutes), to: '/mutes' }, - { text: intl.formatMessage(messages.blocks), to: '/blocks' }, - { - text: intl.formatMessage(messages.domain_blocks), - to: '/domain_blocks', - }, - { text: intl.formatMessage(messages.filters), href: '/filters' }, - null, - { text: intl.formatMessage(messages.logout), action: handleLogoutClick }, - ]; - }, [intl, dispatch]); - - return ; -}; diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.tsx b/app/javascript/mastodon/features/compose/components/navigation_bar.tsx index df1c0a129d..778c7cc3a6 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.tsx +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.tsx @@ -9,8 +9,6 @@ import { IconButton } from 'mastodon/components/icon_button'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { ActionBar } from './action_bar'; - const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, }); @@ -33,15 +31,14 @@ export const NavigationBar: React.FC = () => { return (
- {isReplying ? ( + + {isReplying && ( - ) : ( - )}
); diff --git a/app/javascript/mastodon/features/ui/components/more_link.tsx b/app/javascript/mastodon/features/ui/components/more_link.tsx new file mode 100644 index 0000000000..765b059df2 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/more_link.tsx @@ -0,0 +1,126 @@ +import { useMemo } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; +import { openModal } from 'mastodon/actions/modal'; +import { Dropdown } from 'mastodon/components/dropdown_menu'; +import { Icon } from 'mastodon/components/icon'; +import { useIdentity } from 'mastodon/identity_context'; +import type { MenuItem } from 'mastodon/models/dropdown_menu'; +import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; +import { useAppDispatch } from 'mastodon/store'; + +const messages = defineMessages({ + followedTags: { + id: 'navigation_bar.followed_tags', + defaultMessage: 'Followed hashtags', + }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domainBlocks: { + id: 'navigation_bar.domain_blocks', + defaultMessage: 'Blocked domains', + }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, + filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + administration: { + id: 'navigation_bar.administration', + defaultMessage: 'Administration', + }, + moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + automatedDeletion: { + id: 'navigation_bar.automated_deletion', + defaultMessage: 'Automated post deletion', + }, + accountSettings: { + id: 'navigation_bar.account_settings', + defaultMessage: 'Password and security', + }, + importExport: { + id: 'navigation_bar.import_export', + defaultMessage: 'Import and export', + }, + privacyAndReach: { + id: 'navigation_bar.privacy_and_reach', + defaultMessage: 'Privacy and reach', + }, +}); + +export const MoreLink: React.FC = () => { + const intl = useIntl(); + const { permissions } = useIdentity(); + const dispatch = useAppDispatch(); + + const menu = useMemo(() => { + const arr: MenuItem[] = [ + { + text: intl.formatMessage(messages.followedTags), + to: '/followed_tags', + }, + null, + { text: intl.formatMessage(messages.filters), href: '/filters' }, + { text: intl.formatMessage(messages.mutes), to: '/mutes' }, + { text: intl.formatMessage(messages.blocks), to: '/blocks' }, + { + text: intl.formatMessage(messages.domainBlocks), + to: '/domain_blocks', + }, + ]; + + arr.push( + null, + { + href: '/settings/privacy', + text: intl.formatMessage(messages.privacyAndReach), + }, + { + href: '/statuses_cleanup', + text: intl.formatMessage(messages.automatedDeletion), + }, + { + href: '/auth/edit', + text: intl.formatMessage(messages.accountSettings), + }, + { + href: '/settings/export', + text: intl.formatMessage(messages.importExport), + }, + ); + + if (canManageReports(permissions)) { + arr.push(null, { + href: '/admin/reports', + text: intl.formatMessage(messages.moderation), + }); + } + + if (canViewAdminDashboard(permissions)) { + arr.push({ + href: '/admin/dashboard', + text: intl.formatMessage(messages.administration), + }); + } + + const handleLogoutClick = () => { + dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} })); + }; + + arr.push(null, { + text: intl.formatMessage(messages.logout), + action: handleLogoutClick, + }); + + return arr; + }, [intl, dispatch, permissions]); + + return ( + + + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.tsx b/app/javascript/mastodon/features/ui/components/navigation_panel.tsx index 61e4f2c1b1..b38888779d 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.tsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.tsx @@ -16,12 +16,10 @@ import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?re import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react'; import ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; -import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react'; import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home.svg?react'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; -import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react'; import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; @@ -43,13 +41,13 @@ import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint'; import { useIdentity } from 'mastodon/identity_context'; import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state'; import { transientSingleColumn } from 'mastodon/is_mobile'; -import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { ColumnLink } from './column_link'; import DisabledAccountBanner from './disabled_account_banner'; import { ListPanel } from './list_panel'; +import { MoreLink } from './more_link'; import SignInBanner from './sign_in_banner'; const messages = defineMessages({ @@ -67,11 +65,6 @@ const messages = defineMessages({ id: 'navigation_bar.preferences', defaultMessage: 'Preferences', }, - administration: { - id: 'navigation_bar.administration', - defaultMessage: 'Administration', - }, - moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' }, followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers', @@ -227,7 +220,7 @@ const MENU_WIDTH = 284; export const NavigationPanel: React.FC = () => { const intl = useIntl(); - const { signedIn, disabledAccountId, permissions } = useIdentity(); + const { signedIn, disabledAccountId } = useIdentity(); const open = useAppSelector((state) => state.navigation.open); const dispatch = useAppDispatch(); const openable = useBreakpoint('openable'); @@ -450,31 +443,13 @@ export const NavigationPanel: React.FC = () => { text={intl.formatMessage(messages.preferences)} /> - {canManageReports(permissions) && ( - - )} - {canViewAdminDashboard(permissions) && ( - - )} + )}

+ Date: Wed, 11 Jun 2025 18:51:55 +0200 Subject: [PATCH 10/10] Make React Spring respect animation preferences (#35018) --- app/javascript/mastodon/components/poll.tsx | 2 -- .../mastodon/features/alt_text_modal/index.tsx | 4 ++-- app/javascript/mastodon/features/audio/index.tsx | 10 +--------- .../features/compose/components/upload_progress.tsx | 3 +-- .../getting_started/components/announcements.jsx | 1 - .../mastodon/features/ui/components/upload_area.tsx | 4 ---- app/javascript/mastodon/features/video/index.tsx | 10 +--------- app/javascript/mastodon/main.tsx | 10 +++++++++- 8 files changed, 14 insertions(+), 30 deletions(-) diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index 6692f674d4..e9b3b2b672 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -14,7 +14,6 @@ import { fetchPoll, vote } from 'mastodon/actions/polls'; import { Icon } from 'mastodon/components/icon'; import emojify from 'mastodon/features/emoji/emoji'; import { useIdentity } from 'mastodon/identity_context'; -import { reduceMotion } from 'mastodon/initial_state'; import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import type * as Model from 'mastodon/models/poll'; import type { Status } from 'mastodon/models/status'; @@ -265,7 +264,6 @@ const PollOption: React.FC = (props) => { to: { width: `${percent}%`, }, - immediate: reduceMotion, }); return ( diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx index f24a3b6f70..8a91e14e31 100644 --- a/app/javascript/mastodon/features/alt_text_modal/index.tsx +++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx @@ -27,7 +27,7 @@ import { Audio } from 'mastodon/features/audio'; import { CharacterCounter } from 'mastodon/features/compose/components/character_counter'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import { Video, getPointerPosition } from 'mastodon/features/video'; -import { me, reduceMotion } from 'mastodon/initial_state'; +import { me } from 'mastodon/initial_state'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { assetHost } from 'mastodon/utils/config'; @@ -110,7 +110,7 @@ const Preview: React.FC<{ left: `${x * 100}%`, top: `${y * 100}%`, }, - immediate: reduceMotion || draggingRef.current, + immediate: draggingRef.current, }); const media = useAppSelector((state) => ( diff --git a/app/javascript/mastodon/features/audio/index.tsx b/app/javascript/mastodon/features/audio/index.tsx index dd6fef07d9..a6a131c0d4 100644 --- a/app/javascript/mastodon/features/audio/index.tsx +++ b/app/javascript/mastodon/features/audio/index.tsx @@ -19,11 +19,7 @@ import { SpoilerButton } from 'mastodon/components/spoiler_button'; import { formatTime, getPointerPosition } from 'mastodon/features/video'; import { useAudioContext } from 'mastodon/hooks/useAudioContext'; import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer'; -import { - displayMedia, - useBlurhash, - reduceMotion, -} from 'mastodon/initial_state'; +import { displayMedia, useBlurhash } from 'mastodon/initial_state'; import { playerSettings } from 'mastodon/settings'; const messages = defineMessages({ @@ -163,7 +159,6 @@ export const Audio: React.FC<{ } void spring.start({ volume: `${audioRef.current.volume * 100}%`, - immediate: reduceMotion, }); } }, @@ -217,7 +212,6 @@ export const Audio: React.FC<{ if (audioRef.current && audioRef.current.duration > 0) { void spring.start({ progress: `${(audioRef.current.currentTime / audioRef.current.duration) * 100}%`, - immediate: reduceMotion, config: config.stiff, }); } @@ -263,7 +257,6 @@ export const Audio: React.FC<{ if (lastTimeRange > -1) { void spring.start({ buffer: `${Math.ceil(audioRef.current.buffered.end(lastTimeRange) / audioRef.current.duration) * 100}%`, - immediate: reduceMotion, }); } }, [spring]); @@ -278,7 +271,6 @@ export const Audio: React.FC<{ void spring.start({ volume: `${audioRef.current.muted ? 0 : audioRef.current.volume * 100}%`, - immediate: reduceMotion, }); persistVolume(audioRef.current.volume, audioRef.current.muted); diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.tsx b/app/javascript/mastodon/features/compose/components/upload_progress.tsx index be15917784..e12f58d17f 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.tsx +++ b/app/javascript/mastodon/features/compose/components/upload_progress.tsx @@ -4,7 +4,6 @@ import { animated, useSpring } from '@react-spring/web'; import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react'; import { Icon } from 'mastodon/components/icon'; -import { reduceMotion } from 'mastodon/initial_state'; interface UploadProgressProps { active: boolean; @@ -20,7 +19,7 @@ export const UploadProgress: React.FC = ({ const styles = useSpring({ from: { width: '0%' }, to: { width: `${progress}%` }, - immediate: reduceMotion || !active, // If this is not active, update the UI immediately. + immediate: !active, // If this is not active, update the UI immediately. }); if (!active) { return null; diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx index f5f593860f..87d7e2a3be 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx +++ b/app/javascript/mastodon/features/getting_started/components/announcements.jsx @@ -270,7 +270,6 @@ const ReactionsBar = ({ leave: { scale: 0, }, - immediate: reduceMotion, keys: visibleReactions.map(x => x.get('name')), }); diff --git a/app/javascript/mastodon/features/ui/components/upload_area.tsx b/app/javascript/mastodon/features/ui/components/upload_area.tsx index 87ac090e7e..1919a30df3 100644 --- a/app/javascript/mastodon/features/ui/components/upload_area.tsx +++ b/app/javascript/mastodon/features/ui/components/upload_area.tsx @@ -4,8 +4,6 @@ import { FormattedMessage } from 'react-intl'; import { animated, config, useSpring } from '@react-spring/web'; -import { reduceMotion } from 'mastodon/initial_state'; - interface UploadAreaProps { active?: boolean; onClose: () => void; @@ -39,7 +37,6 @@ export const UploadArea: React.FC = ({ active, onClose }) => { opacity: 1, }, reverse: !active, - immediate: reduceMotion, }); const backgroundAnimStyles = useSpring({ from: { @@ -50,7 +47,6 @@ export const UploadArea: React.FC = ({ active, onClose }) => { }, reverse: !active, config: config.wobbly, - immediate: reduceMotion, }); return ( diff --git a/app/javascript/mastodon/features/video/index.tsx b/app/javascript/mastodon/features/video/index.tsx index e9c3cdefb6..65f26cedad 100644 --- a/app/javascript/mastodon/features/video/index.tsx +++ b/app/javascript/mastodon/features/video/index.tsx @@ -27,11 +27,7 @@ import { attachFullscreenListener, detachFullscreenListener, } from 'mastodon/features/ui/util/fullscreen'; -import { - displayMedia, - useBlurhash, - reduceMotion, -} from 'mastodon/initial_state'; +import { displayMedia, useBlurhash } from 'mastodon/initial_state'; import { playerSettings } from 'mastodon/settings'; import { HotkeyIndicator } from './components/hotkey_indicator'; @@ -260,7 +256,6 @@ export const Video: React.FC<{ setMuted(videoRef.current.muted); void api.start({ volume: `${videoRef.current.volume * 100}%`, - immediate: reduceMotion, }); } }, @@ -350,7 +345,6 @@ export const Video: React.FC<{ videoRef.current.currentTime / videoRef.current.duration; void api.start({ progress: isNaN(progress) ? '0%' : `${progress * 100}%`, - immediate: reduceMotion, config: config.stiff, }); } @@ -738,7 +732,6 @@ export const Video: React.FC<{ if (lastTimeRange > -1) { void api.start({ buffer: `${Math.ceil(videoRef.current.buffered.end(lastTimeRange) / videoRef.current.duration) * 100}%`, - immediate: reduceMotion, }); } }, [api]); @@ -753,7 +746,6 @@ export const Video: React.FC<{ void api.start({ volume: `${videoRef.current.muted ? 0 : videoRef.current.volume * 100}%`, - immediate: reduceMotion, }); persistVolume(videoRef.current.volume, videoRef.current.muted); diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index a9696ac50e..70e6391bee 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -1,8 +1,10 @@ import { createRoot } from 'react-dom/client'; +import { Globals } from '@react-spring/web'; + import { setupBrowserNotifications } from 'mastodon/actions/notifications'; import Mastodon from 'mastodon/containers/mastodon'; -import { me } from 'mastodon/initial_state'; +import { me, reduceMotion } from 'mastodon/initial_state'; import * as perf from 'mastodon/performance'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; @@ -21,6 +23,12 @@ function main() { mountNode.getAttribute('data-props') ?? '{}', ) as Record; + if (reduceMotion) { + Globals.assign({ + skipAnimation: true, + }); + } + const root = createRoot(mountNode); root.render(); store.dispatch(setupBrowserNotifications());