diff --git a/app/javascript/flavours/glitch/actions/tags_typed.ts b/app/javascript/flavours/glitch/actions/tags_typed.ts index 5d1ab026ee..a60c96dc4a 100644 --- a/app/javascript/flavours/glitch/actions/tags_typed.ts +++ b/app/javascript/flavours/glitch/actions/tags_typed.ts @@ -1,12 +1,30 @@ +import { createAction } from '@reduxjs/toolkit'; + import { apiGetTag, apiFollowTag, apiUnfollowTag, apiFeatureTag, apiUnfeatureTag, + apiGetFollowedTags, } from 'flavours/glitch/api/tags'; import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; +export const fetchFollowedHashtags = createDataLoadingThunk( + 'tags/fetch-followed', + async ({ next }: { next?: string } = {}) => { + const response = await apiGetFollowedTags(next); + return { + ...response, + replace: !next, + }; + }, +); + +export const markFollowedHashtagsStale = createAction( + 'tags/mark-followed-stale', +); + export const fetchHashtag = createDataLoadingThunk( 'tags/fetch', ({ tagId }: { tagId: string }) => apiGetTag(tagId), @@ -15,6 +33,9 @@ export const fetchHashtag = createDataLoadingThunk( export const followHashtag = createDataLoadingThunk( 'tags/follow', ({ tagId }: { tagId: string }) => apiFollowTag(tagId), + (_, { dispatch }) => { + void dispatch(markFollowedHashtagsStale()); + }, ); export const unfollowHashtag = createDataLoadingThunk( diff --git a/app/javascript/flavours/glitch/features/compose/index.tsx b/app/javascript/flavours/glitch/features/compose/index.tsx index 445719b493..77922f5a22 100644 --- a/app/javascript/flavours/glitch/features/compose/index.tsx +++ b/app/javascript/flavours/glitch/features/compose/index.tsx @@ -24,33 +24,28 @@ import { Icon } from 'flavours/glitch/components/icon'; import glitchedElephant1 from 'flavours/glitch/images/mbstobon-ui-0.png'; import glitchedElephant2 from 'flavours/glitch/images/mbstobon-ui-1.png'; import glitchedElephant3 from 'flavours/glitch/images/mbstobon-ui-2.png'; -import { mascot } from 'flavours/glitch/initial_state'; +import { mascot, reduceMotion } from 'flavours/glitch/initial_state'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; +import { messages as navbarMessages } from '../ui/components/navigation_bar'; + 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', + live_feed_public: { + id: 'navigation_bar.live_feed_public', + defaultMessage: 'Live feed (public)', }, - public: { - id: 'navigation_bar.public_timeline', - defaultMessage: 'Federated timeline', - }, - community: { - id: 'navigation_bar.community_timeline', - defaultMessage: 'Local timeline', + live_feed_local: { + id: 'navigation_bar.live_feed_local', + defaultMessage: 'Live feed (local)', }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings', }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, }); type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>; @@ -127,19 +122,27 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { elephantUIPlane, ][elefriend]; + const scrollNavbarIntoView = useCallback(() => { + const navbar = document.querySelector('.navigation-panel'); + navbar?.scrollIntoView({ + behavior: reduceMotion ? 'auto' : 'smooth', + }); + }, []); + if (multiColumn) { return (
); } diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx b/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx index c6206658a5..20a433df0d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx @@ -22,7 +22,7 @@ import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state'; import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; -const messages = defineMessages({ +export 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' }, diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index c0378891bb..0069fb603f 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -69,7 +69,6 @@ import { DomainBlocks, Mutes, PinnedStatuses, - GettingStartedMisc, Directory, OnboardingProfile, OnboardingFollows, @@ -152,13 +151,8 @@ class SwitchingColumnsArea extends PureComponent { }; UNSAFE_componentWillMount () { - if (this.props.singleColumn) { - document.body.classList.toggle('layout-single-column', true); - document.body.classList.toggle('layout-multiple-columns', false); - } else { - document.body.classList.toggle('layout-single-column', false); - document.body.classList.toggle('layout-multiple-columns', true); - } + document.body.classList.toggle('layout-single-column', this.props.singleColumn); + document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn); } componentDidUpdate (prevProps) { @@ -210,8 +204,8 @@ class SwitchingColumnsArea extends PureComponent { {singleColumn ? : null} {singleColumn && pathName.startsWith('/deck/') ? : null} {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} - {!singleColumn && pathName === '/getting-started' ? : null} {!singleColumn && pathName === '/home' ? : null} + {pathName === '/getting-started' ? : null} @@ -271,7 +265,6 @@ class SwitchingColumnsArea extends PureComponent { - diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index c96941d15a..7f8f979e37 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -50,10 +50,6 @@ export function GettingStarted () { return import('../../getting_started'); } -export function GettingStartedMisc () { - return import('../../getting_started_misc'); -} - export function KeyboardShortcuts () { return import('../../keyboard_shortcuts'); } diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 71ac1d26db..03ea2c9785 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -6,12 +6,8 @@ "account.view_full_profile": "View full profile", "boost_modal.missing_description": "This toot contains some media without description", "column.favourited_by": "Favourited by", - "column.heading": "Misc", "column.reblogged_by": "Boosted by", - "column.subheading": "Miscellaneous options", "column_header.profile": "Profile", - "column_subheading.lists": "Lists", - "column_subheading.navigation": "Navigation", "community.column_settings.allow_local_only": "Show local-only toots", "compose.attach.doodle": "Draw something", "compose.change_federation": "Change federation settings", @@ -45,8 +41,6 @@ "keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting", "moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.", "navigation_bar.app_settings": "App settings", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.misc": "Misc", "notifications.column_settings.filter_bar.show_bar": "Show filter bar", "settings.always_show_spoilers_field": "Always enable the Content Warning field", "settings.close": "Close", diff --git a/app/javascript/flavours/glitch/reducers/index.ts b/app/javascript/flavours/glitch/reducers/index.ts index 145c3c4d26..24c33f70f5 100644 --- a/app/javascript/flavours/glitch/reducers/index.ts +++ b/app/javascript/flavours/glitch/reducers/index.ts @@ -37,6 +37,7 @@ import settings from './settings'; import status_lists from './status_lists'; import statuses from './statuses'; import { suggestionsReducer } from './suggestions'; +import { followedTagsReducer } from './tags'; import timelines from './timelines'; import trends from './trends'; import user_lists from './user_lists'; @@ -69,6 +70,7 @@ const reducers = { height_cache, custom_emojis, lists: listsReducer, + followedTags: followedTagsReducer, filters, conversations, suggestions: suggestionsReducer, diff --git a/app/javascript/flavours/glitch/reducers/tags.ts b/app/javascript/flavours/glitch/reducers/tags.ts new file mode 100644 index 0000000000..328e195e6e --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/tags.ts @@ -0,0 +1,48 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { + fetchFollowedHashtags, + markFollowedHashtagsStale, + unfollowHashtag, +} from 'flavours/glitch/actions/tags_typed'; +import type { ApiHashtagJSON } from 'flavours/glitch/api_types/tags'; + +export interface TagsQuery { + tags: ApiHashtagJSON[]; + loading: boolean; + stale: boolean; + next: string | undefined; +} + +const initialState: TagsQuery = { + tags: [], + loading: false, + stale: true, + next: undefined, +}; + +export const followedTagsReducer = createReducer(initialState, (builder) => { + builder + .addCase(fetchFollowedHashtags.pending, (state) => { + state.loading = true; + }) + .addCase(fetchFollowedHashtags.rejected, (state) => { + state.loading = false; + }) + .addCase(markFollowedHashtagsStale, (state) => { + state.stale = true; + }) + .addCase(unfollowHashtag.fulfilled, (state, action) => { + const tagId = action.payload.id; + state.tags = state.tags.filter((tag) => tag.id !== tagId); + }) + .addCase(fetchFollowedHashtags.fulfilled, (state, action) => { + const { tags, links, replace } = action.payload; + const next = links.refs.find((link) => link.rel === 'next'); + + state.tags = replace ? tags : [...state.tags, ...tags]; + state.next = next?.uri; + state.stale = false; + state.loading = false; + }); +}); diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts index a3e5cfd125..1f686f0c43 100644 --- a/app/javascript/mastodon/actions/tags_typed.ts +++ b/app/javascript/mastodon/actions/tags_typed.ts @@ -1,12 +1,30 @@ +import { createAction } from '@reduxjs/toolkit'; + import { apiGetTag, apiFollowTag, apiUnfollowTag, apiFeatureTag, apiUnfeatureTag, + apiGetFollowedTags, } from 'mastodon/api/tags'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; +export const fetchFollowedHashtags = createDataLoadingThunk( + 'tags/fetch-followed', + async ({ next }: { next?: string } = {}) => { + const response = await apiGetFollowedTags(next); + return { + ...response, + replace: !next, + }; + }, +); + +export const markFollowedHashtagsStale = createAction( + 'tags/mark-followed-stale', +); + export const fetchHashtag = createDataLoadingThunk( 'tags/fetch', ({ tagId }: { tagId: string }) => apiGetTag(tagId), @@ -15,6 +33,9 @@ export const fetchHashtag = createDataLoadingThunk( export const followHashtag = createDataLoadingThunk( 'tags/follow', ({ tagId }: { tagId: string }) => apiFollowTag(tagId), + (_, { dispatch }) => { + void dispatch(markFollowedHashtagsStale()); + }, ); export const unfollowHashtag = createDataLoadingThunk( diff --git a/app/javascript/mastodon/features/compose/index.tsx b/app/javascript/mastodon/features/compose/index.tsx index 54776c98ff..892cbb9761 100644 --- a/app/javascript/mastodon/features/compose/index.tsx +++ b/app/javascript/mastodon/features/compose/index.tsx @@ -15,39 +15,34 @@ 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 SettingsIcon from '@/material-icons/400-24px/settings.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 { mascot, reduceMotion } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; +import { messages as navbarMessages } from '../ui/components/navigation_bar'; + 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', + live_feed_public: { + id: 'navigation_bar.live_feed_public', + defaultMessage: 'Live feed (public)', }, - public: { - id: 'navigation_bar.public_timeline', - defaultMessage: 'Federated timeline', - }, - community: { - id: 'navigation_bar.community_timeline', - defaultMessage: 'Local timeline', + live_feed_local: { + id: 'navigation_bar.live_feed_local', + defaultMessage: 'Live feed (local)', }, 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>; @@ -82,19 +77,27 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { [dispatch], ); + const scrollNavbarIntoView = useCallback(() => { + const navbar = document.querySelector('.navigation-panel'); + navbar?.scrollIntoView({ + behavior: reduceMotion ? 'auto' : 'smooth', + }); + }, []); + if (multiColumn) { return (