From 7c4393e719c681184b09b97df718539002f89fab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 16 Jun 2025 17:06:33 +0200 Subject: [PATCH] Change order of items in navigation panel in web UI (#35029) --- app/javascript/mastodon/actions/lists.js | 28 +--- .../mastodon/actions/lists_typed.ts | 6 +- app/javascript/mastodon/api/lists.ts | 2 + app/javascript/mastodon/api/tags.ts | 3 +- .../mastodon/components/navigation_portal.tsx | 6 - .../getting_started/components/trends.jsx | 54 ------- .../containers/trends_container.js | 15 -- .../features/getting_started/index.jsx | 4 +- .../mastodon/features/list_adder/index.tsx | 2 +- .../mastodon/features/lists/index.tsx | 2 +- .../components/collapsible_panel.tsx | 85 +++++++++++ .../components/disabled_account_banner.tsx | 88 +++++++++++ .../components/followed_tags_panel.tsx | 69 +++++++++ .../components/list_panel.tsx | 66 +++++++++ .../components/more_link.tsx | 9 -- .../components/sign_in_banner.tsx | 105 +++++++++++++ .../navigation_panel/components/trends.tsx | 57 ++++++++ .../index.tsx} | 138 +++++++++--------- .../features/ui/components/columns_area.jsx | 2 +- .../ui/components/disabled_account_banner.jsx | 85 ----------- .../features/ui/components/list_panel.tsx | 92 ------------ .../features/ui/components/sign_in_banner.jsx | 56 ------- app/javascript/mastodon/locales/en.json | 3 + app/javascript/mastodon/reducers/lists.ts | 11 +- .../400-24px/keyboard_arrow_down-fill.svg | 1 + .../400-24px/keyboard_arrow_down.svg | 1 + .../400-24px/keyboard_arrow_up-fill.svg | 1 + .../400-24px/keyboard_arrow_up.svg | 1 + .../styles/mastodon/components.scss | 85 +++++++---- 29 files changed, 625 insertions(+), 452 deletions(-) delete mode 100644 app/javascript/mastodon/components/navigation_portal.tsx delete mode 100644 app/javascript/mastodon/features/getting_started/components/trends.jsx delete mode 100644 app/javascript/mastodon/features/getting_started/containers/trends_container.js create mode 100644 app/javascript/mastodon/features/navigation_panel/components/collapsible_panel.tsx create mode 100644 app/javascript/mastodon/features/navigation_panel/components/disabled_account_banner.tsx create mode 100644 app/javascript/mastodon/features/navigation_panel/components/followed_tags_panel.tsx create mode 100644 app/javascript/mastodon/features/navigation_panel/components/list_panel.tsx rename app/javascript/mastodon/features/{ui => navigation_panel}/components/more_link.tsx (94%) create mode 100644 app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx create mode 100644 app/javascript/mastodon/features/navigation_panel/components/trends.tsx rename app/javascript/mastodon/features/{ui/components/navigation_panel.tsx => navigation_panel/index.tsx} (90%) delete mode 100644 app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx delete mode 100644 app/javascript/mastodon/features/ui/components/list_panel.tsx delete mode 100644 app/javascript/mastodon/features/ui/components/sign_in_banner.jsx create mode 100644 app/javascript/material-icons/400-24px/keyboard_arrow_down-fill.svg create mode 100644 app/javascript/material-icons/400-24px/keyboard_arrow_down.svg create mode 100644 app/javascript/material-icons/400-24px/keyboard_arrow_up-fill.svg create mode 100644 app/javascript/material-icons/400-24px/keyboard_arrow_up.svg diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index f9abc2e76..a4bbfff22 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -4,14 +4,12 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; -export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST'; -export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS'; -export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL'; - export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST'; export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS'; export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL'; +export * from './lists_typed'; + export const fetchList = id => (dispatch, getState) => { if (getState().getIn(['lists', id])) { return; @@ -40,28 +38,6 @@ export const fetchListFail = (id, error) => ({ error, }); -export const fetchLists = () => (dispatch) => { - dispatch(fetchListsRequest()); - - api().get('/api/v1/lists') - .then(({ data }) => dispatch(fetchListsSuccess(data))) - .catch(err => dispatch(fetchListsFail(err))); -}; - -export const fetchListsRequest = () => ({ - type: LISTS_FETCH_REQUEST, -}); - -export const fetchListsSuccess = lists => ({ - type: LISTS_FETCH_SUCCESS, - lists, -}); - -export const fetchListsFail = error => ({ - type: LISTS_FETCH_FAIL, - error, -}); - export const deleteList = id => (dispatch) => { dispatch(deleteListRequest(id)); diff --git a/app/javascript/mastodon/actions/lists_typed.ts b/app/javascript/mastodon/actions/lists_typed.ts index ccc5c11c8..f2c468e10 100644 --- a/app/javascript/mastodon/actions/lists_typed.ts +++ b/app/javascript/mastodon/actions/lists_typed.ts @@ -1,4 +1,4 @@ -import { apiCreate, apiUpdate } from 'mastodon/api/lists'; +import { apiCreate, apiUpdate, apiGetLists } from 'mastodon/api/lists'; import type { List } from 'mastodon/models/list'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; @@ -11,3 +11,7 @@ export const updateList = createDataLoadingThunk( 'list/update', (list: Partial) => apiUpdate(list), ); + +export const fetchLists = createDataLoadingThunk('lists/fetch', () => + apiGetLists(), +); diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts index a5586eb6d..fa7e6e455 100644 --- a/app/javascript/mastodon/api/lists.ts +++ b/app/javascript/mastodon/api/lists.ts @@ -13,6 +13,8 @@ export const apiCreate = (list: Partial) => export const apiUpdate = (list: Partial) => apiRequestPut(`v1/lists/${list.id}`, list); +export const apiGetLists = () => apiRequestGet('v1/lists'); + export const apiGetAccounts = (listId: string) => apiRequestGet(`v1/lists/${listId}/accounts`, { limit: 0, diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts index cb84ccb1c..28694d3b2 100644 --- a/app/javascript/mastodon/api/tags.ts +++ b/app/javascript/mastodon/api/tags.ts @@ -16,10 +16,11 @@ export const apiFeatureTag = (tagId: string) => export const apiUnfeatureTag = (tagId: string) => apiRequestPost(`v1/tags/${tagId}/unfeature`); -export const apiGetFollowedTags = async (url?: string) => { +export const apiGetFollowedTags = async (url?: string, limit?: number) => { const response = await api().request({ method: 'GET', url: url ?? '/api/v1/followed_tags', + params: { limit }, }); return { diff --git a/app/javascript/mastodon/components/navigation_portal.tsx b/app/javascript/mastodon/components/navigation_portal.tsx deleted file mode 100644 index d3ac8baa6..000000000 --- a/app/javascript/mastodon/components/navigation_portal.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import Trends from 'mastodon/features/getting_started/containers/trends_container'; -import { showTrends } from 'mastodon/initial_state'; - -export const NavigationPortal: React.FC = () => ( -
{showTrends && }
-); diff --git a/app/javascript/mastodon/features/getting_started/components/trends.jsx b/app/javascript/mastodon/features/getting_started/components/trends.jsx deleted file mode 100644 index 5e1d31a87..000000000 --- a/app/javascript/mastodon/features/getting_started/components/trends.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; - -export default class Trends extends ImmutablePureComponent { - - static defaultProps = { - loading: false, - }; - - static propTypes = { - trends: ImmutablePropTypes.list, - fetchTrends: PropTypes.func.isRequired, - }; - - componentDidMount () { - this.props.fetchTrends(); - this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000); - } - - componentWillUnmount () { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - } - } - - render () { - const { trends } = this.props; - - if (!trends || trends.isEmpty()) { - return null; - } - - return ( -
-

- - - -

- - {trends.take(3).map(hashtag => )} -
- ); - } - -} diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js deleted file mode 100644 index 82df526ff..000000000 --- a/app/javascript/mastodon/features/getting_started/containers/trends_container.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; - -import { fetchTrendingHashtags } from 'mastodon/actions/trends'; - -import Trends from '../components/trends'; - -const mapStateToProps = state => ({ - trends: state.getIn(['trends', 'tags', 'items']), -}); - -const mapDispatchToProps = dispatch => ({ - fetchTrends: () => dispatch(fetchTrendingHashtags()), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Trends); diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx index a05f02a16..a7422158d 100644 --- a/app/javascript/mastodon/features/getting_started/index.jsx +++ b/app/javascript/mastodon/features/getting_started/index.jsx @@ -34,7 +34,7 @@ import { NavigationBar } from '../compose/components/navigation_bar'; import { ColumnLink } from '../ui/components/column_link'; import ColumnSubheading from '../ui/components/column_subheading'; -import TrendsContainer from './containers/trends_container'; +import { Trends } from 'mastodon/features/navigation_panel/components/trends'; const messages = defineMessages({ home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, @@ -164,7 +164,7 @@ class GettingStarted extends ImmutablePureComponent { - {(multiColumn && showTrends) && } + {(multiColumn && showTrends) && } {intl.formatMessage(messages.menu)} diff --git a/app/javascript/mastodon/features/list_adder/index.tsx b/app/javascript/mastodon/features/list_adder/index.tsx index 5429c24ae..c23df73cd 100644 --- a/app/javascript/mastodon/features/list_adder/index.tsx +++ b/app/javascript/mastodon/features/list_adder/index.tsx @@ -122,7 +122,7 @@ const ListAdder: React.FC<{ const [listIds, setListIds] = useState([]); useEffect(() => { - dispatch(fetchLists()); + void dispatch(fetchLists()); apiGetAccountLists(accountId) .then((data) => { diff --git a/app/javascript/mastodon/features/lists/index.tsx b/app/javascript/mastodon/features/lists/index.tsx index a45559712..65fb11d0b 100644 --- a/app/javascript/mastodon/features/lists/index.tsx +++ b/app/javascript/mastodon/features/lists/index.tsx @@ -79,7 +79,7 @@ const Lists: React.FC<{ const lists = useAppSelector((state) => getOrderedLists(state)); useEffect(() => { - dispatch(fetchLists()); + void dispatch(fetchLists()); }, [dispatch]); const emptyMessage = ( diff --git a/app/javascript/mastodon/features/navigation_panel/components/collapsible_panel.tsx b/app/javascript/mastodon/features/navigation_panel/components/collapsible_panel.tsx new file mode 100644 index 000000000..39a789959 --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/collapsible_panel.tsx @@ -0,0 +1,85 @@ +import { useState, useCallback, useId } from 'react'; + +import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react'; +import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react'; +import type { IconProp } from 'mastodon/components/icon'; +import { IconButton } from 'mastodon/components/icon_button'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { ColumnLink } from 'mastodon/features/ui/components/column_link'; + +export const CollapsiblePanel: React.FC<{ + children: React.ReactNode[]; + to: string; + title: string; + collapseTitle: string; + expandTitle: string; + icon: string; + iconComponent: IconProp; + activeIconComponent?: IconProp; + loading?: boolean; +}> = ({ + children, + to, + icon, + iconComponent, + activeIconComponent, + title, + collapseTitle, + expandTitle, + loading, +}) => { + const [expanded, setExpanded] = useState(false); + const accessibilityId = useId(); + + const handleClick = useCallback(() => { + setExpanded((value) => !value); + }, [setExpanded]); + + return ( +
+
+ + + {(loading || children.length > 0) && ( + <> +
+ + + + )} +
+ + {children.length > 0 && expanded && ( +
+ {children} +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/navigation_panel/components/disabled_account_banner.tsx b/app/javascript/mastodon/features/navigation_panel/components/disabled_account_banner.tsx new file mode 100644 index 000000000..9103170fa --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/disabled_account_banner.tsx @@ -0,0 +1,88 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { openModal } from 'mastodon/actions/modal'; +import { + disabledAccountId, + movedToAccountId, + domain, +} from 'mastodon/initial_state'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +export const DisabledAccountBanner: React.FC = () => { + const disabledAccount = useAppSelector((state) => + disabledAccountId ? state.accounts.get(disabledAccountId) : undefined, + ); + const movedToAccount = useAppSelector((state) => + movedToAccountId ? state.accounts.get(movedToAccountId) : undefined, + ); + const dispatch = useAppDispatch(); + + const handleLogOutClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} })); + + return false; + }, + [dispatch], + ); + + const disabledAccountLink = ( + + {disabledAccount?.acct}@{domain} + + ); + + return ( +
+

+ {movedToAccount ? ( + + {movedToAccount.acct.includes('@') + ? movedToAccount.acct + : `${movedToAccount.acct}@${domain}`} + + ), + }} + /> + ) : ( + + )} +

+ + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/navigation_panel/components/followed_tags_panel.tsx b/app/javascript/mastodon/features/navigation_panel/components/followed_tags_panel.tsx new file mode 100644 index 000000000..e6d680b20 --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/followed_tags_panel.tsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import TagIcon from '@/material-icons/400-24px/tag.svg?react'; +import { apiGetFollowedTags } from 'mastodon/api/tags'; +import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; +import { ColumnLink } from 'mastodon/features/ui/components/column_link'; + +import { CollapsiblePanel } from './collapsible_panel'; + +const messages = defineMessages({ + followedTags: { + id: 'navigation_bar.followed_tags', + defaultMessage: 'Followed hashtags', + }, + expand: { + id: 'navigation_panel.expand_followed_tags', + defaultMessage: 'Expand followed hashtags menu', + }, + collapse: { + id: 'navigation_panel.collapse_followed_tags', + defaultMessage: 'Collapse followed hashtags menu', + }, +}); + +export const FollowedTagsPanel: React.FC = () => { + const intl = useIntl(); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + + void apiGetFollowedTags(undefined, 4) + .then(({ tags }) => { + setTags(tags); + setLoading(false); + + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, [setLoading, setTags]); + + return ( + + {tags.map((tag) => ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/navigation_panel/components/list_panel.tsx b/app/javascript/mastodon/features/navigation_panel/components/list_panel.tsx new file mode 100644 index 000000000..020abf36c --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/list_panel.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +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 'mastodon/features/ui/components/column_link'; +import { getOrderedLists } from 'mastodon/selectors/lists'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import { CollapsiblePanel } from './collapsible_panel'; + +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 [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + + void dispatch(fetchLists()).then(() => { + setLoading(false); + + return ''; + }); + }, [dispatch, setLoading]); + + return ( + + {lists.map((list) => ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/more_link.tsx b/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx similarity index 94% rename from app/javascript/mastodon/features/ui/components/more_link.tsx rename to app/javascript/mastodon/features/navigation_panel/components/more_link.tsx index 765b059df..a26935eac 100644 --- a/app/javascript/mastodon/features/ui/components/more_link.tsx +++ b/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx @@ -12,10 +12,6 @@ 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', @@ -54,11 +50,6 @@ export const MoreLink: React.FC = () => { 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' }, diff --git a/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx b/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx new file mode 100644 index 000000000..bb7548f33 --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx @@ -0,0 +1,105 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { openModal } from 'mastodon/actions/modal'; +import { registrationsOpen, sso_redirect } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +export const SignInBanner: React.FC = () => { + const dispatch = useAppDispatch(); + + const openClosedRegistrationsModal = useCallback( + () => + dispatch( + openModal({ modalType: 'CLOSED_REGISTRATIONS', modalProps: {} }), + ), + [dispatch], + ); + + let signupButton: React.ReactNode; + + const signupUrl = useAppSelector( + (state) => + (state.server.getIn(['server', 'registrations', 'url'], null) as + | string + | null) ?? '/auth/sign_up', + ); + + if (sso_redirect) { + return ( +
+

+ + + +

+

+ +

+ + + +
+ ); + } + + if (registrationsOpen) { + signupButton = ( + + + + ); + } else { + signupButton = ( + + ); + } + + return ( +
+

+ + + +

+

+ +

+ {signupButton} + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/navigation_panel/components/trends.tsx b/app/javascript/mastodon/features/navigation_panel/components/trends.tsx new file mode 100644 index 000000000..f51bb2ed9 --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/components/trends.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { fetchTrendingHashtags } from 'mastodon/actions/trends'; +import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; +import { showTrends } from 'mastodon/initial_state'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +export const Trends: React.FC = () => { + const dispatch = useAppDispatch(); + const trends = useAppSelector( + (state) => + state.trends.getIn(['tags', 'items']) as ImmutableList< + ImmutableMap + >, + ); + + useEffect(() => { + dispatch(fetchTrendingHashtags()); + + const refreshInterval = setInterval(() => { + dispatch(fetchTrendingHashtags()); + }, 900 * 1000); + + return () => { + clearInterval(refreshInterval); + }; + }, [dispatch]); + + if (!showTrends || trends.isEmpty()) { + return null; + } + + return ( +
+
+

+ + + +

+ + {trends.take(4).map((hashtag) => ( + + ))} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.tsx b/app/javascript/mastodon/features/navigation_panel/index.tsx similarity index 90% rename from app/javascript/mastodon/features/ui/components/navigation_panel.tsx rename to app/javascript/mastodon/features/navigation_panel/index.tsx index da0ce09e9..d96267c2d 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.tsx +++ b/app/javascript/mastodon/features/navigation_panel/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -22,7 +22,6 @@ 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'; @@ -32,7 +31,8 @@ import { openNavigation, closeNavigation } from 'mastodon/actions/navigation'; import { Account } from 'mastodon/components/account'; import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { WordmarkLogo } from 'mastodon/components/logo'; -import { NavigationPortal } from 'mastodon/components/navigation_portal'; +import { Search } from 'mastodon/features/compose/components/search'; +import { ColumnLink } from 'mastodon/features/ui/components/column_link'; import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint'; import { useIdentity } from 'mastodon/identity_context'; import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state'; @@ -40,11 +40,12 @@ import { transientSingleColumn } from 'mastodon/is_mobile'; 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'; +import { DisabledAccountBanner } from './components/disabled_account_banner'; +import { FollowedTagsPanel } from './components/followed_tags_panel'; +import { ListPanel } from './components/list_panel'; +import { MoreLink } from './components/more_link'; +import { SignInBanner } from './components/sign_in_banner'; +import { Trends } from './components/trends'; const messages = defineMessages({ home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, @@ -67,6 +68,10 @@ const messages = defineMessages({ }, about: { id: 'navigation_bar.about', defaultMessage: 'About' }, search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, + searchTrends: { + id: 'navigation_bar.search_trends', + defaultMessage: 'Search / Trending', + }, advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface', @@ -159,33 +164,6 @@ const FollowRequestsLink: React.FC = () => { ); }; -const SearchLink: React.FC = () => { - const intl = useIntl(); - const showAsSearch = useBreakpoint('full'); - - if (!trendsEnabled || showAsSearch) { - return ( - - ); - } - - return ( - - ); -}; - const ProfileCard: React.FC = () => { if (!me) { return null; @@ -198,6 +176,13 @@ const ProfileCard: React.FC = () => { ); }; +const isFirehoseActive = ( + match: unknown, + { pathname }: { pathname: string }, +) => { + return !!match || pathname.startsWith('/public'); +}; + const MENU_WIDTH = 284; export const NavigationPanel: React.FC = () => { @@ -206,6 +191,7 @@ export const NavigationPanel: React.FC = () => { const open = useAppSelector((state) => state.navigation.open); const dispatch = useAppDispatch(); const openable = useBreakpoint('openable'); + const showSearch = useBreakpoint('full'); const location = useLocation(); const overlayRef = useRef(null); @@ -275,13 +261,6 @@ export const NavigationPanel: React.FC = () => { }, ); - const isFirehoseActive = useCallback( - (match: unknown, location: { pathname: string }): boolean => { - return !!match || location.pathname.startsWith('/public'); - }, - [], - ); - const previouslyFocusedElementRef = useRef(); useEffect(() => { @@ -297,7 +276,7 @@ export const NavigationPanel: React.FC = () => { } }, [open]); - let banner = undefined; + let banner: React.ReactNode; if (transientSingleColumn) { banner = ( @@ -335,6 +314,8 @@ export const NavigationPanel: React.FC = () => {
+ {showSearch && } + {banner &&
{banner}
} @@ -358,43 +339,49 @@ export const NavigationPanel: React.FC = () => { activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} /> - - )} - + {trendsEnabled && ( + + )} {(signedIn || timelinePreview) && ( )} - {!signedIn && ( -
-
- {disabledAccountId ? ( - - ) : ( - - )} -
- )} - {signedIn && ( <> + + + + +
+ + + + + { /> - -
{ )}
-
- { text={intl.formatMessage(messages.about)} />
+ + {!signedIn && ( +
+
+ + {disabledAccountId ? ( + + ) : ( + + )} +
+ )}
- +
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 5d596b7bc..9737d31e3 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 'mastodon/features/navigation_panel'; const componentMap = { 'COMPOSE': Compose, diff --git a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx deleted file mode 100644 index 3d1380f66..000000000 --- a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, injectIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { connect } from 'react-redux'; - -import { openModal } from 'mastodon/actions/modal'; -import { disabledAccountId, movedToAccountId, domain } from 'mastodon/initial_state'; - -const mapStateToProps = (state) => ({ - disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']), - movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined, -}); - -const mapDispatchToProps = (dispatch) => ({ - onLogout () { - dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' })); - }, -}); - -class DisabledAccountBanner extends PureComponent { - - static propTypes = { - disabledAcct: PropTypes.string.isRequired, - movedToAcct: PropTypes.string, - onLogout: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleLogOutClick = e => { - e.preventDefault(); - e.stopPropagation(); - - this.props.onLogout(); - - return false; - }; - - render () { - const { disabledAcct, movedToAcct } = this.props; - - const disabledAccountLink = ( - - {disabledAcct}@{domain} - - ); - - return ( -
-

- {movedToAcct ? ( - {movedToAcct.includes('@') ? movedToAcct : `${movedToAcct}@${domain}`}, - }} - /> - ) : ( - - )} -

- - - - -
- ); - } - -} - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DisabledAccountBanner)); diff --git a/app/javascript/mastodon/features/ui/components/list_panel.tsx b/app/javascript/mastodon/features/ui/components/list_panel.tsx deleted file mode 100644 index ef4cdad2c..000000000 --- a/app/javascript/mastodon/features/ui/components/list_panel.tsx +++ /dev/null @@ -1,92 +0,0 @@ -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/sign_in_banner.jsx b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx deleted file mode 100644 index 74a8fdb84..000000000 --- a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback } from 'react'; - -import { FormattedMessage } from 'react-intl'; - - -import { openModal } from 'mastodon/actions/modal'; -import { registrationsOpen, sso_redirect } from 'mastodon/initial_state'; -import { useAppDispatch, useAppSelector } from 'mastodon/store'; - -const SignInBanner = () => { - const dispatch = useAppDispatch(); - - const openClosedRegistrationsModal = useCallback( - () => dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })), - [dispatch], - ); - - let signupButton; - - const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up'); - - if (sso_redirect) { - return ( -
-

-

- -
- ); - } - - if (registrationsOpen) { - signupButton = ( - - - - ); - } else { - signupButton = ( - - ); - } - - return ( -
-

-

- {signupButton} - -
- ); -}; - -export default SignInBanner; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index d4acdd257..ad5d66c16 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -579,8 +579,11 @@ "navigation_bar.privacy_and_reach": "Privacy and reach", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.search": "Search", + "navigation_bar.search_trends": "Search / Trending", "navigation_bar.security": "Security", + "navigation_panel.collapse_followed_tags": "Collapse followed hashtags menu", "navigation_panel.collapse_lists": "Collapse list menu", + "navigation_panel.expand_followed_tags": "Expand followed hashtags 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}", diff --git a/app/javascript/mastodon/reducers/lists.ts b/app/javascript/mastodon/reducers/lists.ts index 593e71794..ba8417cf6 100644 --- a/app/javascript/mastodon/reducers/lists.ts +++ b/app/javascript/mastodon/reducers/lists.ts @@ -1,7 +1,11 @@ import type { Reducer } from '@reduxjs/toolkit'; import { Map as ImmutableMap } from 'immutable'; -import { createList, updateList } from 'mastodon/actions/lists_typed'; +import { + createList, + updateList, + fetchLists, +} from 'mastodon/actions/lists_typed'; import type { ApiListJSON } from 'mastodon/api_types/lists'; import { createList as createListFromJSON } from 'mastodon/models/list'; import type { List } from 'mastodon/models/list'; @@ -9,7 +13,6 @@ import type { List } from 'mastodon/models/list'; import { LIST_FETCH_SUCCESS, LIST_FETCH_FAIL, - LISTS_FETCH_SUCCESS, LIST_DELETE_SUCCESS, } from '../actions/lists'; @@ -33,12 +36,12 @@ export const listsReducer: Reducer = (state = initialState, action) => { updateList.fulfilled.match(action) ) { return normalizeList(state, action.payload); + } else if (fetchLists.fulfilled.match(action)) { + return normalizeLists(state, action.payload); } else { switch (action.type) { case LIST_FETCH_SUCCESS: return normalizeList(state, action.list as ApiListJSON); - case LISTS_FETCH_SUCCESS: - return normalizeLists(state, action.lists as ApiListJSON[]); case LIST_DELETE_SUCCESS: case LIST_FETCH_FAIL: return state.set(action.id as string, null); diff --git a/app/javascript/material-icons/400-24px/keyboard_arrow_down-fill.svg b/app/javascript/material-icons/400-24px/keyboard_arrow_down-fill.svg new file mode 100644 index 000000000..60482b650 --- /dev/null +++ b/app/javascript/material-icons/400-24px/keyboard_arrow_down-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/keyboard_arrow_down.svg b/app/javascript/material-icons/400-24px/keyboard_arrow_down.svg new file mode 100644 index 000000000..60482b650 --- /dev/null +++ b/app/javascript/material-icons/400-24px/keyboard_arrow_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/keyboard_arrow_up-fill.svg b/app/javascript/material-icons/400-24px/keyboard_arrow_up-fill.svg new file mode 100644 index 000000000..01321a128 --- /dev/null +++ b/app/javascript/material-icons/400-24px/keyboard_arrow_up-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/keyboard_arrow_up.svg b/app/javascript/material-icons/400-24px/keyboard_arrow_up.svg new file mode 100644 index 000000000..01321a128 --- /dev/null +++ b/app/javascript/material-icons/400-24px/keyboard_arrow_up.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 af6429e7b..72393e62b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3144,10 +3144,9 @@ a.account__display-name { height: 100vh; } - .navigation-panel__sign-in-banner, .navigation-panel__banner, - .getting-started__trends, - .navigation-panel__logo { + .navigation-panel__logo, + .getting-started__trends { display: none; } } @@ -3481,15 +3480,24 @@ a.account__display-name { &__header { display: flex; align-items: center; - padding-inline-end: 12px; + + &__sep { + width: 0; + height: 24px; + border-left: 1px solid var(--background-border-color); + } .column-link { flex: 1 1 auto; } + + .icon-button { + padding: 8px; + } } &__items { - padding-inline-start: 24px + 5px; + padding-inline-start: 24px + 8px; .icon { display: none; @@ -3500,14 +3508,16 @@ a.account__display-name { &__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; + padding-top: 8px; + padding-bottom: 8px; + padding-inline-start: 12px - 7px; + padding-inline-end: 12px; + gap: 8px; margin: 12px; - margin-bottom: 4px; border-radius: 6px; + font-size: 16px; + font-weight: 600; + line-height: 18px; .icon { width: 24px; @@ -3517,7 +3527,11 @@ a.account__display-name { .navigation-bar { padding: 16px; - border-bottom: 1px solid var(--background-border-color); + } + + .search { + margin: 16px; + margin-bottom: 12px; } .logo { @@ -3529,26 +3543,36 @@ a.account__display-name { margin-bottom: 12px; } - @media screen and (height <= 710px) { - &__portal { + .getting-started__trends h4 { + padding: 10px 12px; + padding-inline-start: 16px; + } + + .getting-started__trends .trends__item { + padding: 10px 12px; + padding-inline-start: 16px; + } + + @media screen and (height <= 930px) { + &__portal .trends__item:nth-child(n + 5) { display: none; } } - @media screen and (height <= 765px) { - &__portal .trends__item:nth-child(n + 3) { - display: none; - } - } - - @media screen and (height <= 820px) { + @media screen and (height <= 930px - 56px) { &__portal .trends__item:nth-child(n + 4) { display: none; } } - @media screen and (height <= 920px) { - .column-link.column-link--optional { + @media screen and (height <= 930px - (56px * 2)) { + &__portal .trends__item:nth-child(n + 3) { + display: none; + } + } + + @media screen and (height <= 930px - (56px * 3)) { + &__portal { display: none; } } @@ -3821,9 +3845,10 @@ a.account__display-name { .column-link { display: flex; align-items: center; - gap: 5px; + gap: 8px; font-size: 16px; - padding: 13px; + font-weight: 400; + padding: 12px; text-decoration: none; overflow: hidden; white-space: nowrap; @@ -4605,7 +4630,8 @@ a.status-card { } .load-more .loading-indicator, -.button .loading-indicator { +.button .loading-indicator, +.icon-button .loading-indicator { position: static; transform: none; @@ -4616,6 +4642,13 @@ a.status-card { } } +.icon-button .loading-indicator .circular-progress { + color: lighten($ui-base-color, 26%); + width: 12px; + height: 12px; + margin: 6px; +} + .load-more .loading-indicator .circular-progress { color: lighten($ui-base-color, 26%); }