diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index b187f71a5..99a67525e 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -5,24 +5,16 @@ import { queryClient } from 'pl-fe/queries/client'; import { selectAccount } from 'pl-fe/selectors'; import { isLoggedIn } from 'pl-fe/utils/auth'; -import { getClient, type PlfeResponse } from '../api'; +import { getClient } from '../api'; import { importEntities } from './importer'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; import type { AppDispatch, RootState } from 'pl-fe/store'; -import type { History } from 'pl-fe/types/history'; const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const; const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const; -const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History) => { - // The client is unauthorized - redirect to login. - if (history && error?.response?.status === 401) { - history.push('/login'); - } -}; - const createAccount = (params: CreateAccountParams) => async (dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).settings.createAccount(params).then((response) => @@ -47,7 +39,7 @@ const fetchAccount = (accountId: string) => }); }; -const fetchAccountByUsername = (username: string, history?: History) => +const fetchAccountByUsername = (username: string) => (dispatch: AppDispatch, getState: () => RootState) => { const { auth, me } = getState(); const features = auth.client.features; @@ -60,8 +52,6 @@ const fetchAccountByUsername = (username: string, history?: History) => } else if (features.accountLookup) { return dispatch(accountLookup(username)).then(account => { dispatch(fetchRelationships([account.id])); - }).catch(error => { - maybeRedirectLogin(error, history); }); } else { return getClient(getState()).accounts.searchAccounts(username, { resolve: true, limit: 1 }).then(accounts => { diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 822c70915..12698cbb7 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -26,7 +26,6 @@ import type { Status } from 'pl-fe/normalizers/status'; import type { Policy, Rule, Scope } from 'pl-fe/pages/settings/interaction-policies'; import type { ClearLinkSuggestion } from 'pl-fe/reducers/compose'; import type { AppDispatch, RootState } from 'pl-fe/store'; -import type { History } from 'pl-fe/types/history'; let cancelFetchComposeSuggestions = new AbortController(); @@ -338,14 +337,13 @@ const validateSchedule = (state: RootState, composeId: string) => { }; interface SubmitComposeOpts { - history?: History; force?: boolean; onSuccess?: () => void; } const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview = false) => async (dispatch: AppDispatch, getState: () => RootState) => { - const { history, force = false, onSuccess } = opts; + const { force = false, onSuccess } = opts; if (!isLoggedIn(getState)) return; const state = getState(); @@ -373,7 +371,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview useModalsStore.getState().actions.openModal('MISSING_DESCRIPTION', { onContinue: () => { useModalsStore.getState().actions.closeModal('MISSING_DESCRIPTION'); - dispatch(submitCompose(composeId, { history, force: true, onSuccess })); + dispatch(submitCompose(composeId, { force: true, onSuccess })); }, }); return; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group.ts b/packages/pl-fe/src/api/hooks/groups/use-group.ts index 9e80c208d..e3fb3a938 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-group.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-group.ts @@ -1,5 +1,5 @@ +import { useLocation, useNavigate } from '@tanstack/react-router'; import { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; import { Entities } from 'pl-fe/entity-store/entities'; import { useEntity } from 'pl-fe/entity-store/hooks/use-entity'; @@ -11,7 +11,8 @@ import type { Group } from 'pl-api'; const useGroup = (groupId: string, refetch = true) => { const client = useClient(); - const history = useHistory(); + const location = useLocation(); + const navigate = useNavigate(); const { entity: group, isUnauthorized, ...result } = useEntity( [Entities.GROUPS, groupId], @@ -25,7 +26,8 @@ const useGroup = (groupId: string, refetch = true) => { useEffect(() => { if (isUnauthorized) { - history.push('/login'); + localStorage.setItem('plfe:redirect_uri', location.href); + navigate({ to: '/login', replace: true }); } }, [isUnauthorized]); diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index a784bf8e4..df43dbd56 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -1,9 +1,9 @@ import { autoUpdate, flip, shift, useFloating, useTransitionStyles } from '@floating-ui/react'; import { useQuery } from '@tanstack/react-query'; +import { useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect } from 'react'; import { useIntl, FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchRelationships } from 'pl-fe/actions/accounts'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; @@ -54,7 +54,7 @@ interface IAccountHoverCard { /** Popup profile preview that appears when hovering avatars and display names. */ const AccountHoverCard: React.FC = ({ visible = true }) => { const dispatch = useAppDispatch(); - const history = useHistory(); + const router = useRouter(); const intl = useIntl(); const { accountId, ref } = useAccountHoverCardStore(); @@ -70,9 +70,11 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { }, [dispatch, accountId]); useEffect(() => { - const unlisten = history.listen(() => { - showAccountHoverCard.cancel(); - closeAccountHoverCard(true); + const unlisten = router.subscribe('onLoad', ({ pathChanged }) => { + if (pathChanged) { + showAccountHoverCard.cancel(); + closeAccountHoverCard(true); + } }); return () => { diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 767f6e749..0738eb768 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -1,7 +1,7 @@ +import { Link, useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { Link, useHistory } from 'react-router-dom'; import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; import Avatar from 'pl-fe/components/ui/avatar'; @@ -38,7 +38,7 @@ const messages = defineMessages({ }); const InstanceFavicon: React.FC = ({ account, disabled }) => { - const history = useHistory(); + const navigate = useNavigate(); const intl = useIntl(); const handleClick: React.MouseEventHandler = (e) => { @@ -48,7 +48,7 @@ const InstanceFavicon: React.FC = ({ account, disabled }) => { const timelineUrl = `/timeline/${account.domain}`; if (!(e.ctrlKey || e.metaKey)) { - history.push(timelineUrl); + navigate({ to: timelineUrl }); } else { window.open(timelineUrl, '_blank'); } diff --git a/packages/pl-fe/src/components/announcements/announcement-content.tsx b/packages/pl-fe/src/components/announcements/announcement-content.tsx index c953a2bf2..012a754f5 100644 --- a/packages/pl-fe/src/components/announcements/announcement-content.tsx +++ b/packages/pl-fe/src/components/announcements/announcement-content.tsx @@ -1,5 +1,5 @@ +import { useNavigate } from '@tanstack/react-router'; import React, { useEffect, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; import { getTextDirection } from 'pl-fe/utils/rtl'; @@ -12,7 +12,7 @@ interface IAnnouncementContent { } const AnnouncementContent: React.FC = ({ announcement }) => { - const history = useHistory(); + const navigate = useNavigate(); const node = useRef(null); const direction = getTextDirection(announcement.content); @@ -25,7 +25,7 @@ const AnnouncementContent: React.FC = ({ announcement }) = if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); e.stopPropagation(); - history.push(`/@${mention.acct}`); + navigate({ to: '/@{$username}', params: { username: mention.acct } }); } }; @@ -35,14 +35,14 @@ const AnnouncementContent: React.FC = ({ announcement }) = if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); e.stopPropagation(); - history.push(`/tags/${hashtag}`); + navigate({ to: '/tags/$id', params: { id: hashtag } }); } }; - const onStatusClick = (status: string, e: MouseEvent) => { + const onStatusClick = (statusId: string, e: MouseEvent) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - history.push(status); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: 'undefined', statusId } }); } }; diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx index 692c37e55..a93b0fb28 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -1,13 +1,13 @@ +import { useNavigate, type LinkProps } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; import Counter from 'pl-fe/components/ui/counter'; import Icon from 'pl-fe/components/ui/icon'; import Toggle from 'pl-fe/components/ui/toggle'; import { userTouching } from 'pl-fe/is-mobile'; -interface MenuItem { +type MenuItem = { action?: React.EventHandler; active?: boolean; checked?: boolean; @@ -20,12 +20,15 @@ interface MenuItem { onChange?: (value: boolean) => void; target?: React.HTMLAttributeAnchorTarget; text: string; - to?: string; type?: 'toggle' | 'radio'; items?: Array>; onSelectFile?: (files: FileList) => void; accept?: string; -} +} & ({ + to: LinkProps['to']; + params?: LinkProps['params']; + search?: LinkProps['search']; +} | { to?: undefined }); interface IDropdownMenuItem { index: number; @@ -36,7 +39,7 @@ interface IDropdownMenuItem { } const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdownMenuItem) => { - const history = useHistory(); + const navigate = useNavigate(); const itemRef = useRef(null); const fileElement = useRef(null); @@ -62,8 +65,8 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo if (item.to) { event.preventDefault(); if (userTouching.matches) { - history.replace(item.to); - } else history.push(item.to); + navigate({ to: item.to, params: item.params, search: item.search, replace: true }); + } else navigate({ to: item.to, params: item.params, search: item.search }); } else if (typeof item.action === 'function') { const action = item.action; event.preventDefault(); diff --git a/packages/pl-fe/src/components/dropdown-navigation.tsx b/packages/pl-fe/src/components/dropdown-navigation.tsx index 60009b0a3..743fa2853 100644 --- a/packages/pl-fe/src/components/dropdown-navigation.tsx +++ b/packages/pl-fe/src/components/dropdown-navigation.tsx @@ -1,9 +1,9 @@ /* eslint-disable jsx-a11y/interactive-supports-focus */ import { useInfiniteQuery } from '@tanstack/react-query'; +import { Link, type LinkProps } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link, NavLink } from 'react-router-dom'; import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; @@ -29,15 +29,14 @@ import sourceCode from 'pl-fe/utils/code'; import type { Account as AccountEntity } from 'pl-api'; -interface IDropdownNavigationLink { +interface IDropdownNavigationLink extends Partial> { href?: string; - to?: string; icon: string; text: string | JSX.Element; onClick: React.EventHandler; } -const DropdownNavigationLink: React.FC = React.memo(({ href, to, icon, text, onClick }) => { +const DropdownNavigationLink: React.FC = React.memo(({ href, to, params, icon, text, onClick }) => { const body = ( <>
@@ -50,9 +49,9 @@ const DropdownNavigationLink: React.FC = React.memo(({ if (to) { return ( - + {body} - + ); } @@ -182,7 +181,7 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { > {account ? (
- + @@ -197,7 +196,8 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { } onClick={closeSidebar} @@ -268,7 +268,7 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { {features.drive && ( } onClick={closeSidebar} @@ -352,7 +352,7 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { } onClick={closeSidebar} @@ -369,7 +369,7 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { {(account.is_admin || account.is_moderator) && ( } onClick={closeSidebar} @@ -416,10 +416,10 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => {
{otherAccounts.map(account => renderAccount(account))} - + - +
)}
diff --git a/packages/pl-fe/src/components/event-preview.tsx b/packages/pl-fe/src/components/event-preview.tsx index fc0d6335d..2c7131ae6 100644 --- a/packages/pl-fe/src/components/event-preview.tsx +++ b/packages/pl-fe/src/components/event-preview.tsx @@ -42,7 +42,8 @@ const EventPreview: React.FC = ({ status, className, hideAction, diff --git a/packages/pl-fe/src/components/groups/popover/group-popover.tsx b/packages/pl-fe/src/components/groups/popover/group-popover.tsx index 5219d2669..c9311e755 100644 --- a/packages/pl-fe/src/components/groups/popover/group-popover.tsx +++ b/packages/pl-fe/src/components/groups/popover/group-popover.tsx @@ -1,6 +1,6 @@ +import { Link, useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { Link, matchPath, useHistory } from 'react-router-dom'; import Button from 'pl-fe/components/ui/button'; import Divider from 'pl-fe/components/ui/divider'; @@ -11,11 +11,13 @@ import Text from 'pl-fe/components/ui/text'; import Emojify from 'pl-fe/features/emoji/emojify'; import GroupMemberCount from 'pl-fe/features/group/components/group-member-count'; import GroupPrivacy from 'pl-fe/features/group/components/group-privacy'; +import { groupTimelineRoute } from 'pl-fe/features/ui/router'; import GroupAvatar from '../group-avatar'; import type { Group } from 'pl-api'; + interface IGroupPopoverContainer { children: React.ReactElement>; isEnabled: boolean; @@ -32,13 +34,8 @@ const GroupPopover = (props: IGroupPopoverContainer) => { const { children, group, isEnabled } = props; const intl = useIntl(); - const history = useHistory(); - const path = history.location.pathname; - const shouldHideAction = matchPath(path, { - path: ['/groups/:groupId'], - exact: true, - }); + const shouldHideAction = !!useMatch({ from: groupTimelineRoute.fullPath, shouldThrow: false }); if (!isEnabled) { return children; @@ -96,7 +93,7 @@ const GroupPopover = (props: IGroupPopoverContainer) => { {!shouldHideAction && (
- + diff --git a/packages/pl-fe/src/components/hashtag-link.tsx b/packages/pl-fe/src/components/hashtag-link.tsx index 5cb71c656..67af7e3e1 100644 --- a/packages/pl-fe/src/components/hashtag-link.tsx +++ b/packages/pl-fe/src/components/hashtag-link.tsx @@ -7,7 +7,7 @@ interface IHashtagLink { } const HashtagLink: React.FC = ({ hashtag }) => ( - e.stopPropagation()}> + e.stopPropagation()}> #{hashtag} ); diff --git a/packages/pl-fe/src/components/hashtag.tsx b/packages/pl-fe/src/components/hashtag.tsx index d5e7a2e0e..abbf92718 100644 --- a/packages/pl-fe/src/components/hashtag.tsx +++ b/packages/pl-fe/src/components/hashtag.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import HStack from 'pl-fe/components/ui/hstack'; @@ -34,7 +34,7 @@ const Hashtag: React.FC = ({ hashtag }) => { return ( - + #{hashtag.name} diff --git a/packages/pl-fe/src/components/hashtags-bar.tsx b/packages/pl-fe/src/components/hashtags-bar.tsx index 72547f089..5dfda8de6 100644 --- a/packages/pl-fe/src/components/hashtags-bar.tsx +++ b/packages/pl-fe/src/components/hashtags-bar.tsx @@ -1,7 +1,7 @@ // Adapted from Mastodon https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/components/hashtag_bar.tsx +import { Link } from '@tanstack/react-router'; import React, { useCallback, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import HStack from './ui/hstack'; import Text from './ui/text'; @@ -34,7 +34,8 @@ const HashtagsBar: React.FC = ({ hashtags }) => { {revealedHashtags.map((hashtag) => ( e.stopPropagation()} className='flex items-center rounded-sm bg-gray-100 px-1.5 py-1 text-xs font-medium text-primary-600 black:bg-primary-900 dark:bg-primary-700 dark:text-white' > diff --git a/packages/pl-fe/src/components/link.tsx b/packages/pl-fe/src/components/link.tsx index 413061279..f378bd041 100644 --- a/packages/pl-fe/src/components/link.tsx +++ b/packages/pl-fe/src/components/link.tsx @@ -1,5 +1,5 @@ +import { Link as Comp, type LinkProps } from '@tanstack/react-router'; import React from 'react'; -import { Link as Comp, LinkProps } from 'react-router-dom'; const Link = (props: LinkProps) => ( = ({ mention: { acct, username }, disabled }) return ( e.stopPropagation()} diff --git a/packages/pl-fe/src/components/parsed-mfm.tsx b/packages/pl-fe/src/components/parsed-mfm.tsx index 39006f6c3..53066c3c5 100644 --- a/packages/pl-fe/src/components/parsed-mfm.tsx +++ b/packages/pl-fe/src/components/parsed-mfm.tsx @@ -1,9 +1,9 @@ // ~~Shamelessly stolen~~ ported to React from Sharkey // https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/packages/frontend/src/components/global/MkMfm.ts +import { Link } from '@tanstack/react-router'; import * as mfm from '@transfem-org/sfm-js'; import { clamp } from 'lodash'; import React, { CSSProperties } from 'react'; -import { Link } from 'react-router-dom'; import { useSettings } from 'pl-fe/stores/settings'; import { makeEmojiMap } from 'pl-fe/utils/normalizers'; @@ -410,7 +410,8 @@ const ParsedMfm: React.FC = React.memo(({ text, emojis, mentions, sp return ( e.stopPropagation()} @@ -427,7 +428,8 @@ const ParsedMfm: React.FC = React.memo(({ text, emojis, mentions, sp return ( e.stopPropagation()} diff --git a/packages/pl-fe/src/components/pending-items-row.tsx b/packages/pl-fe/src/components/pending-items-row.tsx index 03fe72e30..1f207390c 100644 --- a/packages/pl-fe/src/components/pending-items-row.tsx +++ b/packages/pl-fe/src/components/pending-items-row.tsx @@ -1,7 +1,7 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index a82c53e4d..9de11473a 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -1,8 +1,8 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api'; import React, { useState, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import * as v from 'valibot'; import Blurhash from 'pl-fe/components/blurhash'; @@ -267,7 +267,7 @@ const PreviewCard: React.FC = ({ values={{ name: card.authors.map(author => ( - + {author.account && ( diff --git a/packages/pl-fe/src/components/quoted-status.tsx b/packages/pl-fe/src/components/quoted-status.tsx index 0edbed4e1..2260f71b4 100644 --- a/packages/pl-fe/src/components/quoted-status.tsx +++ b/packages/pl-fe/src/components/quoted-status.tsx @@ -1,7 +1,7 @@ +import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { MouseEventHandler } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import StatusMedia from 'pl-fe/components/status-media'; import Stack from 'pl-fe/components/ui/stack'; @@ -32,7 +32,7 @@ interface IQuotedStatus { /** Status embedded in a quote post. */ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const handleExpandClick: MouseEventHandler = (e) => { if (!status) return; @@ -41,7 +41,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => if (!compose && e.button === 0) { const statusUrl = `/@${account.acct}/posts/${status.id}`; if (!(e.ctrlKey || e.metaKey)) { - history.push(statusUrl); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: account.acct, statusId: status.id } }); } else { window.open(statusUrl, '_blank'); } diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index 074a33aa3..784604489 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -1,6 +1,6 @@ +import { useRouter } from '@tanstack/react-router'; import debounce from 'lodash/debounce'; import React, { useEffect, useRef, useMemo, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle, ListRange, IndexLocationWithAlign } from 'react-virtuoso'; import LoadMore from 'pl-fe/components/load-more'; @@ -104,7 +104,7 @@ const ScrollableList = React.forwardRef(({ useWindowScroll = true, ...params }, ref) => { - const history = useHistory(); + const { history } = useRouter(); const { autoloadMore } = useSettings(); // Preserve scroll position diff --git a/packages/pl-fe/src/components/search-input.tsx b/packages/pl-fe/src/components/search-input.tsx index 860ef8c02..2fa3df71d 100644 --- a/packages/pl-fe/src/components/search-input.tsx +++ b/packages/pl-fe/src/components/search-input.tsx @@ -1,36 +1,24 @@ +import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import AutosuggestAccountInput from 'pl-fe/components/autosuggest-account-input'; import SvgIcon from 'pl-fe/components/ui/svg-icon'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { selectAccount } from 'pl-fe/selectors'; -import type { AppDispatch, RootState } from 'pl-fe/store'; -import type { History } from 'pl-fe/types/history'; - const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, clear: { id: 'search.clear', defaultMessage: 'Clear input' }, action: { id: 'search.action', defaultMessage: 'Search for “{query}”' }, }); -const redirectToAccount = (accountId: string, routerHistory: History) => - (_dispatch: AppDispatch, getState: () => RootState) => { - const acct = selectAccount(getState(), accountId)!.acct; - - if (acct && routerHistory) { - routerHistory.push(`/@${acct}`); - } - }; - const SearchInput = React.memo(() => { const [value, setValue] = useState(''); const dispatch = useAppDispatch(); - const history = useHistory(); + const navigate = useNavigate(); const intl = useIntl(); const handleChange = (event: React.ChangeEvent) => { @@ -45,7 +33,7 @@ const SearchInput = React.memo(() => { const handleSubmit = () => { setValue(''); - history.push('/search?' + new URLSearchParams({ q: value })); + navigate({ to: '/search', search: { q: value } }); }; const handleKeyDown = (event: React.KeyboardEvent) => { @@ -60,7 +48,7 @@ const SearchInput = React.memo(() => { const handleSelected = (accountId: string) => { setValue(''); - dispatch(redirectToAccount(accountId, history)); + dispatch((_, getState) => navigate({ to: '/@{$username}', params: { username: selectAccount(getState(), accountId)!.acct } })); }; const makeMenu = () => [ diff --git a/packages/pl-fe/src/components/sidebar-navigation-link.tsx b/packages/pl-fe/src/components/sidebar-navigation-link.tsx index 3de63df4a..2d031902d 100644 --- a/packages/pl-fe/src/components/sidebar-navigation-link.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation-link.tsx @@ -1,12 +1,11 @@ -import clsx from 'clsx'; +import { Link, type LinkProps } from '@tanstack/react-router'; import React from 'react'; -import { NavLink } from 'react-router-dom'; import { useSettings } from 'pl-fe/stores/settings'; import Icon from './ui/icon'; -interface ISidebarNavigationLink { +interface ISidebarNavigationLink extends Partial> { /** Notification count, if any. */ count?: number; /** Optional max to cap count (ie: N+) */ @@ -17,15 +16,13 @@ interface ISidebarNavigationLink { activeIcon?: string; /** Link label. */ text: React.ReactNode; - /** Route to an internal page. */ - to?: string; /** Callback when the link is clicked. */ onClick?: React.EventHandler; } /** Desktop sidebar navigation link. */ const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { - const { icon, activeIcon, text, to = '', count, countMax, onClick } = props; + const { icon, activeIcon, text, to, params, count, countMax, onClick } = props; const isActive = location.pathname === to; const { demetricator } = useSettings(); @@ -39,15 +36,14 @@ const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNaviga }; return ( -

{text}

-
+ ); }), (prevProps, nextProps) => prevProps.count === nextProps.count); diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index db9b5d458..38dbdc43d 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -285,18 +285,21 @@ const SidebarNavigation: React.FC = React.memo(({ shrink }) )} } /> - {features.drive && } - />} + {features.drive && ( + } + /> + )} = ({ publicStatus, }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); - const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); + const match = useMatch({ from: layouts.group.id, shouldThrow: false }); const { boostModal } = useSettings(); const client = useClient(); @@ -644,7 +645,7 @@ const MenuButton: React.FC = ({ }; const handleEditClick: React.EventHandler = () => { - if (status.event) history.push(`/@${status.account.acct}/events/${status.id}/edit`); + if (status.event) navigate({ to: '/@{$username}/events/$statusId/edit', params: { username: status.account.acct, statusId: status.id } }); else dispatch(editStatus(status.id)); }; @@ -677,7 +678,7 @@ const MenuButton: React.FC = ({ const account = status.account; getOrCreateChatByAccountId(account.id) - .then((chat) => history.push(`/chats/${chat.id}`)) + .then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } })) .catch(() => {}); }; @@ -803,7 +804,8 @@ const MenuButton: React.FC = ({ menu.push({ text: intl.formatMessage(messages.open), icon: require('@phosphor-icons/core/regular/arrows-vertical.svg'), - to: `/@${status.account.acct}/posts/${status.id}`, + to: '/@{$username}/posts/$statusId', + params: { username: status.account.acct, statusId: status.id }, }); } @@ -1056,7 +1058,8 @@ const MenuButton: React.FC = ({ menu.push({ text: intl.formatMessage(messages.adminAccount, { name: username }), - to: `/pl-fe/admin/accounts/${status.account_id}`, + to: '/pl-fe/admin/accounts/$accountId', + params: { accountId: status.account_id }, icon: require('@phosphor-icons/core/regular/gavel.svg'), }); diff --git a/packages/pl-fe/src/components/status-hover-card.tsx b/packages/pl-fe/src/components/status-hover-card.tsx index cfe7c6015..eb60e9248 100644 --- a/packages/pl-fe/src/components/status-hover-card.tsx +++ b/packages/pl-fe/src/components/status-hover-card.tsx @@ -1,8 +1,8 @@ import { autoUpdate, flip, shift, useFloating, useTransitionStyles } from '@floating-ui/react'; +import { useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect } from 'react'; import { useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchStatus } from 'pl-fe/actions/statuses'; import { showStatusHoverCard } from 'pl-fe/components/hover-status-wrapper'; @@ -18,8 +18,8 @@ interface IStatusHoverCard { /** Popup status preview that appears when hovering reply to */ const StatusHoverCard: React.FC = ({ visible = true }) => { const dispatch = useAppDispatch(); + const router = useRouter(); const intl = useIntl(); - const history = useHistory(); const { statusId, ref } = useStatusHoverCardStore(); const { closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardActions(); @@ -33,9 +33,11 @@ const StatusHoverCard: React.FC = ({ visible = true }) => { }, [statusId, status]); useEffect(() => { - const unlisten = history.listen(() => { - showStatusHoverCard.cancel(); - closeStatusHoverCard(true); + const unlisten = router.subscribe('onLoad', ({ pathChanged }) => { + if (pathChanged) { + showStatusHoverCard.cancel(); + closeStatusHoverCard(true); + } }); return () => { diff --git a/packages/pl-fe/src/components/status-mention.tsx b/packages/pl-fe/src/components/status-mention.tsx index fba8f4408..3fb6c21b9 100644 --- a/packages/pl-fe/src/components/status-mention.tsx +++ b/packages/pl-fe/src/components/status-mention.tsx @@ -1,5 +1,5 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; -import { Link } from 'react-router-dom'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; @@ -21,7 +21,8 @@ const StatusMention: React.FC = ({ accountId, fallback }) => { return ( e.stopPropagation()} diff --git a/packages/pl-fe/src/components/status-reply-mentions.tsx b/packages/pl-fe/src/components/status-reply-mentions.tsx index b3c344ec7..6cfb4257a 100644 --- a/packages/pl-fe/src/components/status-reply-mentions.tsx +++ b/packages/pl-fe/src/components/status-reply-mentions.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; import HoverStatusWrapper from 'pl-fe/components/hover-status-wrapper'; @@ -62,7 +62,8 @@ const StatusReplyMentions: React.FC = ({ status, hoverable const link = ( e.stopPropagation()} > diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 3564708b7..a872f3c0a 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -1,7 +1,7 @@ +import { Link, useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect, useMemo, useRef } from 'react'; import { defineMessages, useIntl, FormattedList, FormattedMessage } from 'react-intl'; -import { Link, useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { unfilterStatus } from 'pl-fe/actions/statuses'; @@ -74,7 +74,7 @@ const Status: React.FC = (props) => { } = props; const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const { toggleStatusesMediaHidden } = useStatusMetaActions(); @@ -92,7 +92,7 @@ const Status: React.FC = (props) => { const { mutate: unreblogStatus } = useUnreblogStatus(actualStatus.id); const isReblog = status.reblog_id; - const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`; + const statusUrl = '/@{$username}/posts/$postId'; const group = actualStatus.group; const filterResults = useMemo(() => [...status.filtered, ...actualStatus.filtered].filter(({ filter }) => filter.filter_action === 'warn'), [status.filtered, actualStatus.filtered]); @@ -115,7 +115,7 @@ const Status: React.FC = (props) => { if (onClick) { onClick(); } else { - history.push(statusUrl); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: actualStatus.account.acct, statusId: actualStatus.id } }); } } else { window.open(statusUrl, '_blank'); @@ -161,11 +161,11 @@ const Status: React.FC = (props) => { }; const handleHotkeyOpen = () => { - history.push(statusUrl); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: actualStatus.account.acct, statusId: actualStatus.id } }); }; const handleHotkeyOpenProfile = () => { - history.push(`/@${actualStatus.account.acct}`); + navigate({ to: '/@{$username}', params: { username: actualStatus.account.acct } }); }; const handleHotkeyMoveUp = (e?: KeyboardEvent) => { @@ -207,7 +207,8 @@ const Status: React.FC = (props) => { values={{ name: ( @@ -218,7 +219,7 @@ const Status: React.FC = (props) => { ), group: ( - + @@ -233,7 +234,7 @@ const Status: React.FC = (props) => { const accounts = status.accounts || [status.account]; const renderedAccounts = accounts.slice(0, 2).map(account => !!account && ( - + @@ -302,7 +303,7 @@ const Status: React.FC = (props) => { defaultMessage='Posted in {group}' values={{ group: ( - + @@ -388,7 +389,7 @@ const Status: React.FC = (props) => { id={actualStatus.account_id} action={
- event.stopPropagation()}> + event.stopPropagation()}> diff --git a/packages/pl-fe/src/components/thumb-navigation-link.tsx b/packages/pl-fe/src/components/thumb-navigation-link.tsx index ebf5b45ad..4b95e25ed 100644 --- a/packages/pl-fe/src/components/thumb-navigation-link.tsx +++ b/packages/pl-fe/src/components/thumb-navigation-link.tsx @@ -1,6 +1,5 @@ -import clsx from 'clsx'; +import { Link, useMatchRoute } from '@tanstack/react-router'; import React from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; import IconWithCounter from 'pl-fe/components/icon-with-counter'; import Icon from 'pl-fe/components/ui/icon'; @@ -14,27 +13,17 @@ interface IThumbNavigationLink { text: string; to: string; exact?: boolean; - paths?: Array; } -const ThumbNavigationLink: React.FC = ({ count, countMax, src, activeSrc, text, to, exact, paths }): JSX.Element => { - const { pathname } = useLocation(); +const ThumbNavigationLink: React.FC = ({ count, countMax, src, activeSrc, text, to, exact }): JSX.Element => { const { demetricator } = useSettings(); - const isActive = (): boolean => { - if (paths) { - return paths.some(path => pathname.startsWith(path)); - } else { - return exact ? pathname === to : pathname.startsWith(to); - } - }; + const matchRoute = useMatchRoute(); - const active = isActive(); - - const icon = (active && activeSrc) || src; + const icon = (activeSrc && matchRoute({ to }) !== null && activeSrc) || src; return ( - + {!demetricator && count !== undefined ? ( = ({ count, countMax, ) : ( )} - + ); }; diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index e33d719cc..1c4eecfc3 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -1,12 +1,13 @@ +import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useRouteMatch } from 'react-router-dom'; import { groupComposeModal } from 'pl-fe/actions/compose'; import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link'; import Icon from 'pl-fe/components/ui/icon'; import { useStatContext } from 'pl-fe/contexts/stat-context'; import { Entities } from 'pl-fe/entity-store/entities'; +import { layouts } from 'pl-fe/features/ui/router'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; @@ -31,7 +32,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => { const { account } = useOwnAccount(); const features = useFeatures(); - const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); + const match = useMatch({ from: layouts.group.id, shouldThrow: false }); const isSidebarOpen = useIsSidebarOpen(); const { openSidebar, closeSidebar } = useUiStoreActions(); diff --git a/packages/pl-fe/src/components/trending-link.tsx b/packages/pl-fe/src/components/trending-link.tsx index 6e64591a1..4d538f692 100644 --- a/packages/pl-fe/src/components/trending-link.tsx +++ b/packages/pl-fe/src/components/trending-link.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import { TrendsLink } from 'pl-api'; import React from 'react'; -import { Link } from 'react-router-dom'; import { getTextDirection } from 'pl-fe/utils/rtl'; @@ -58,7 +58,7 @@ const TrendingLink: React.FC = ({ trendingLink }) => { {!!count && ( - + {accountsCountRenderer(count)} )} diff --git a/packages/pl-fe/src/components/ui/button/index.tsx b/packages/pl-fe/src/components/ui/button/index.tsx index 4f21c837d..6a4f71507 100644 --- a/packages/pl-fe/src/components/ui/button/index.tsx +++ b/packages/pl-fe/src/components/ui/button/index.tsx @@ -1,6 +1,6 @@ +import { Link, type LinkProps } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; -import { Link } from 'react-router-dom'; import Icon from '../icon'; @@ -8,10 +8,10 @@ import { useButtonStyles } from './useButtonStyles'; import type { ButtonSizes, ButtonThemes } from './useButtonStyles'; -interface IButton extends Pick< +type IButton = Pick< React.ComponentProps<'button'>, 'children' | 'className' | 'disabled' | 'onClick' | 'onMouseDown' | 'onKeyDown' | 'onKeyPress' | 'title' | 'type' -> { +> & (Pick | { to?: undefined }) & { /** Whether this button expands the width of its container. */ block?: boolean; /** URL to an SVG icon to render inside the button. */ @@ -24,8 +24,6 @@ interface IButton extends Pick< size?: ButtonSizes; /** Text inside the button. Takes precedence over `children`. */ text?: React.ReactNode; - /** Makes the button into a navlink, if provided. */ - to?: string; /** Makes the button into an anchor, if provided. */ href?: string; /** Styles the button visually with a predefined theme. */ @@ -44,7 +42,6 @@ const Button = React.forwardRef(({ size = 'md', text, theme = 'secondary', - to, href, type = 'button', className, @@ -84,9 +81,9 @@ const Button = React.forwardRef(({ ); - if (to) { + if (props.to) { return ( - + {renderButton()} ); diff --git a/packages/pl-fe/src/components/ui/card.tsx b/packages/pl-fe/src/components/ui/card.tsx index 73c2e30d3..7768fc201 100644 --- a/packages/pl-fe/src/components/ui/card.tsx +++ b/packages/pl-fe/src/components/ui/card.tsx @@ -1,7 +1,7 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; import SvgIcon from 'pl-fe/components/ui/svg-icon'; diff --git a/packages/pl-fe/src/components/ui/column.tsx b/packages/pl-fe/src/components/ui/column.tsx index 31875175c..42ec014b7 100644 --- a/packages/pl-fe/src/components/ui/column.tsx +++ b/packages/pl-fe/src/components/ui/column.tsx @@ -1,7 +1,7 @@ +import { LinkProps, useNavigate, useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; import throttle from 'lodash/throttle'; import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import Helmet from 'pl-fe/components/helmet'; import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config'; @@ -11,19 +11,20 @@ import { Card, CardBody, CardHeader, CardTitle, type CardSizes } from './card'; type IColumnHeader = Pick; /** Contains the column title with optional back button. */ -const ColumnHeader: React.FC = ({ label, backHref, className, action }) => { - const history = useHistory(); +const ColumnHeader: React.FC = ({ label, backHref, backParams, className, action }) => { + const navigate = useNavigate(); + const { history } = useRouter(); const handleBackClick = () => { if (backHref) { - history.push(backHref); + navigate({ to: backHref, params: backParams }); return; } - if (history.length === 1) { - history.push('/'); + if (!history.canGoBack) { + navigate({ to: '/' }); } else { - history.goBack(); + history.back(); } }; @@ -42,7 +43,8 @@ const ColumnHeader: React.FC = ({ label, backHref, className, act interface IColumn { /** Route the back button goes to. */ - backHref?: string; + backHref?: LinkProps['to']; + backParams?: LinkProps['params']; /** Column title text. */ label?: string; /** Whether this column should have a transparent background. */ diff --git a/packages/pl-fe/src/components/ui/tabs.tsx b/packages/pl-fe/src/components/ui/tabs.tsx index bc4a7721e..bed91b7a8 100644 --- a/packages/pl-fe/src/components/ui/tabs.tsx +++ b/packages/pl-fe/src/components/ui/tabs.tsx @@ -5,12 +5,14 @@ import { Tab as ReachTab, useTabsContext, } from '@reach/tabs'; +import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import Counter from './counter'; +import type { LinkProps } from '@tanstack/react-router'; + import './tabs.css'; const HORIZONTAL_PADDING = 8; @@ -109,15 +111,18 @@ type Item = { title?: string; /** URL to visit when the tab is selected. */ href?: string; - /** Route to visit when the tab is selected. */ - to?: string; /** Callback when the tab is selected. */ action?: () => void; /** Display a counter over the tab. */ count?: number; /** Unique name for this tab. */ name: string; -} +} & ({ + /** Route to visit when the tab is selected. */ + to: LinkProps['to']; + params?: LinkProps['params']; + search?: LinkProps['search']; +} | { to?: undefined }); interface ITabs { /** Array of structured tab items. */ @@ -128,9 +133,9 @@ interface ITabs { /** Animated tabs component. */ const Tabs = ({ items, activeItem }: ITabs) => { - const defaultIndex = items.findIndex(({ name }) => name === activeItem); + const navigate = useNavigate(); - const history = useHistory(); + const defaultIndex = items.findIndex(({ name }) => name === activeItem); const onChange = (selectedIndex: number) => { const item = items[selectedIndex]; @@ -138,7 +143,7 @@ const Tabs = ({ items, activeItem }: ITabs) => { if (typeof item.action === 'function') { item.action(); } else if (item.to) { - history.push(item.to); + navigate({ to: item.to, params: item.params, search: item.search }); } }; diff --git a/packages/pl-fe/src/components/ui/toast.tsx b/packages/pl-fe/src/components/ui/toast.tsx index 9bf25cb2c..dd5a1e77f 100644 --- a/packages/pl-fe/src/components/ui/toast.tsx +++ b/packages/pl-fe/src/components/ui/toast.tsx @@ -1,8 +1,8 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; import toast, { Toast as RHToast } from 'react-hot-toast'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { ToastText, ToastType } from 'pl-fe/toast'; diff --git a/packages/pl-fe/src/contexts/chat-context.tsx b/packages/pl-fe/src/contexts/chat-context.tsx index dbd879c0c..c6f2d1257 100644 --- a/packages/pl-fe/src/contexts/chat-context.tsx +++ b/packages/pl-fe/src/contexts/chat-context.tsx @@ -1,7 +1,8 @@ +import { useMatch } from '@tanstack/react-router'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; import { toggleChatPane } from 'pl-fe/actions/chats'; +import { chatRoute, layouts } from 'pl-fe/features/ui/router'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useChat } from 'pl-fe/queries/chats'; import { useSettings } from 'pl-fe/stores/settings'; @@ -25,13 +26,12 @@ interface IChatProvider { } const ChatProvider: React.FC = ({ children }) => { - const history = useHistory(); const dispatch = useAppDispatch(); const { chats } = useSettings(); - const path = history.location.pathname; - const isUsingMainChatPage = Boolean(path.match(/^\/chats/)); - const { chatId } = useParams<{ chatId: string }>(); + const isUsingMainChatPage = !!useMatch({ from: layouts.chats.id, shouldThrow: false }); + const chatPageMatch = useMatch({ from: chatRoute.id, shouldThrow: false }); + const { chatId = null } = chatPageMatch?.params ?? {}; const [screen, setScreen] = useState(ChatWidgetScreens.INBOX); const [currentChatId, setCurrentChatId] = useState(chatId); diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index a5fb7cb6a..ccd3a800f 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -1,9 +1,9 @@ import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import { GOTOSOCIAL, MASTODON, mediaAttachmentSchema } from 'pl-api'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import * as v from 'valibot'; import { mentionCompose, directCompose } from 'pl-fe/actions/compose'; @@ -139,7 +139,7 @@ interface IHeader { const Header: React.FC = ({ account }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const client = useClient(); @@ -170,7 +170,7 @@ const Header: React.FC = ({ account }) => { toast.error(data?.error); }, onSuccess: (response) => { - history.push(`/chats/${response.id}`); + navigate({ to: '/chats/$chatId', params: { chatId: response.id } }); queryClient.invalidateQueries({ queryKey: ['chats', 'search'], }); @@ -406,7 +406,8 @@ const Header: React.FC = ({ account }) => { if (features.searchFromAccount) { menu.push({ text: intl.formatMessage(account.id === ownAccount.id ? messages.searchSelf : messages.search, { name: account.username }), - to: '/search?' + new URLSearchParams({ type: 'statuses', accountId: account.id }).toString(), + to: '/search', + search: { type: 'statuses', accountId: account.id }, icon: require('@phosphor-icons/core/regular/magnifying-glass.svg'), }); } @@ -584,7 +585,8 @@ const Header: React.FC = ({ account }) => { menu.push({ text: intl.formatMessage(messages.adminAccount, { name: account.username }), - to: `/pl-fe/admin/accounts/${account.id}`, + to: '/pl-fe/admin/accounts/$accountId', + params: { accountId: account.id }, icon: require('@phosphor-icons/core/regular/gavel.svg'), }); } diff --git a/packages/pl-fe/src/features/admin/components/admin-tabs.tsx b/packages/pl-fe/src/features/admin/components/admin-tabs.tsx index 38802af32..d9cc16424 100644 --- a/packages/pl-fe/src/features/admin/components/admin-tabs.tsx +++ b/packages/pl-fe/src/features/admin/components/admin-tabs.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { useRouteMatch } from 'react-router-dom'; -import Tabs from 'pl-fe/components/ui/tabs'; +import Tabs, { type Item } from 'pl-fe/components/ui/tabs'; import { usePendingUsersCount } from 'pl-fe/queries/admin/use-accounts'; import { usePendingReportsCount } from 'pl-fe/queries/admin/use-reports'; @@ -19,7 +19,7 @@ const AdminTabs: React.FC = () => { const { data: awaitingApprovalCount } = usePendingUsersCount(); const { data: pendingReportsCount = 0 } = usePendingReportsCount(); - const tabs = [{ + const tabs: Array = [{ name: '/pl-fe/admin', text: intl.formatMessage(messages.dashboard), to: '/pl-fe/admin', diff --git a/packages/pl-fe/src/features/admin/components/counter.tsx b/packages/pl-fe/src/features/admin/components/counter.tsx index 0a53f20a1..f2546679e 100644 --- a/packages/pl-fe/src/features/admin/components/counter.tsx +++ b/packages/pl-fe/src/features/admin/components/counter.tsx @@ -1,7 +1,7 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; import { FormattedNumber } from 'react-intl'; -import { Link } from 'react-router-dom'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import Text from 'pl-fe/components/ui/text'; diff --git a/packages/pl-fe/src/features/admin/components/dashcounter.tsx b/packages/pl-fe/src/features/admin/components/dashcounter.tsx index d2d1d4eec..4f784012c 100644 --- a/packages/pl-fe/src/features/admin/components/dashcounter.tsx +++ b/packages/pl-fe/src/features/admin/components/dashcounter.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedNumber } from 'react-intl'; -import { Link } from 'react-router-dom'; import Text from 'pl-fe/components/ui/text'; import { isNumber } from 'pl-fe/utils/numbers'; diff --git a/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx b/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx index 673b5a9e4..81b560921 100644 --- a/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx +++ b/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Widget from 'pl-fe/components/ui/widget'; import AccountContainer from 'pl-fe/containers/account-container'; @@ -17,7 +17,7 @@ interface ILatestAccountsPanel { const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const { data: accountIds } = useAdminAccounts({ origin: 'local', @@ -28,7 +28,7 @@ const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { const total = accountIds?.total; const handleAction = () => { - history.push('/pl-fe/admin/users'); + navigate({ to: '/pl-fe/admin/users' }); }; return ( diff --git a/packages/pl-fe/src/features/admin/components/report.tsx b/packages/pl-fe/src/features/admin/components/report.tsx index f94fad341..48cd3f6c1 100644 --- a/packages/pl-fe/src/features/admin/components/report.tsx +++ b/packages/pl-fe/src/features/admin/components/report.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; @@ -35,7 +35,7 @@ const Report: React.FC = ({ id }) => { const reporterAcct = account?.acct; return ( - + {targetAccount && ( @@ -68,7 +68,7 @@ const Report: React.FC = ({ id }) => { /> - + @{reporterAcct} diff --git a/packages/pl-fe/src/features/admin/tabs/reports.tsx b/packages/pl-fe/src/features/admin/tabs/reports.tsx index f21acac87..5a1209cf6 100644 --- a/packages/pl-fe/src/features/admin/tabs/reports.tsx +++ b/packages/pl-fe/src/features/admin/tabs/reports.tsx @@ -1,22 +1,20 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import ScrollableList from 'pl-fe/components/scrollable-list'; import HStack from 'pl-fe/components/ui/hstack'; import IconButton from 'pl-fe/components/ui/icon-button'; import Text from 'pl-fe/components/ui/text'; +import { adminReportsRoute } from 'pl-fe/features/ui/router'; import { useReports } from 'pl-fe/queries/admin/use-reports'; import Report from '../components/report'; const Reports: React.FC = () => { - const [params, setParams] = useSearchParams(); - - const resolved = params.get('resolved') as any as boolean || undefined; - const accountId = params.get('account_id') || undefined; - const targetAccountId = params.get('target_account_id') || undefined; + const { resolved, account_id: accountId, target_account_id: targetAccountId } = adminReportsRoute.useSearch(); + const navigate = useNavigate({ from: adminReportsRoute.fullPath }); const { account } = useAccount(accountId); const { account: targetAccount } = useAccount(targetAccountId); @@ -27,11 +25,7 @@ const Reports: React.FC = () => { target_account_id: targetAccountId, }); - const handleUnsetAccounts = () => { - params.delete('account_id'); - params.delete('target_account_id'); - setParams(params => Object.fromEntries(params.entries())); - }; + const handleUnsetAccounts = () => navigate({ search: (prev) => ({ resolved: prev.resolved }) }); return ( <> diff --git a/packages/pl-fe/src/features/auth-login/components/login-form.tsx b/packages/pl-fe/src/features/auth-login/components/login-form.tsx index 2752369cf..e108be0e5 100644 --- a/packages/pl-fe/src/features/auth-login/components/login-form.tsx +++ b/packages/pl-fe/src/features/auth-login/components/login-form.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; import Button from 'pl-fe/components/ui/button'; import Form from 'pl-fe/components/ui/form'; diff --git a/packages/pl-fe/src/features/auth-login/components/otp-auth-form.tsx b/packages/pl-fe/src/features/auth-login/components/otp-auth-form.tsx index 5d83f141a..d33d64f09 100644 --- a/packages/pl-fe/src/features/auth-login/components/otp-auth-form.tsx +++ b/packages/pl-fe/src/features/auth-login/components/otp-auth-form.tsx @@ -1,6 +1,6 @@ +import { Navigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Redirect } from 'react-router-dom'; import { otpVerify, verifyCredentials, switchAccount } from 'pl-fe/actions/auth'; import { BigCard } from 'pl-fe/components/big-card'; @@ -51,7 +51,7 @@ const OtpAuthForm: React.FC = ({ mfa_token, small }) => { event.preventDefault(); }; - if (shouldRedirect) return ; + if (shouldRedirect) return ; const form = (
diff --git a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx index f09f86c89..3e24d1b21 100644 --- a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx +++ b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx @@ -1,7 +1,7 @@ +import { Link, useNavigate } from '@tanstack/react-router'; import debounce from 'lodash/debounce'; import React, { useState, useRef, useCallback } from 'react'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { Link, useHistory } from 'react-router-dom'; import { accountLookup } from 'pl-fe/actions/accounts'; import { register, verifyCredentials } from 'pl-fe/actions/auth'; @@ -47,7 +47,7 @@ interface IRegistrationForm { /** Allows the user to sign up for the website. */ const RegistrationForm: React.FC = ({ inviteToken }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const { locale } = useSettings(); @@ -167,7 +167,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { return launchModal(); } else { return dispatch(verifyCredentials(access_token)).then(() => { - history.push('/'); + navigate({ to: '/' }); }); } }; @@ -352,7 +352,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { /> {intl.formatMessage(messages.tos)} })} + labelText={intl.formatMessage(messages.agreement, { tos: {intl.formatMessage(messages.tos)} })} > = ({ chat, onClick }) => { const { openModal } = useModalsActions(); const intl = useIntl(); const features = useFeatures(); - const history = useHistory(); + const navigate = useNavigate(); const { isUsingMainChatPage } = useChatContext(); const { deleteChat } = useChatActions(chat?.id as string); @@ -59,7 +59,7 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { deleteChat.mutate(undefined, { onSuccess() { if (isUsingMainChatPage) { - history.push('/chats'); + navigate({ to: '/chats' }); } }, }); diff --git a/packages/pl-fe/src/features/chats/components/chat-page/chat-page.tsx b/packages/pl-fe/src/features/chats/components/chat-page/chat-page.tsx index cf7d8c761..124982fcd 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/chat-page.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/chat-page.tsx @@ -1,6 +1,7 @@ +import { useMatches } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { matchPath, Route, Switch, useHistory } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import Stack from 'pl-fe/components/ui/stack'; @@ -10,13 +11,11 @@ import ChatPageSettings from './components/chat-page-settings'; import ChatPageShoutbox from './components/chat-page-shoutbox'; import ChatPageSidebar from './components/chat-page-sidebar'; -const ChatPage: React.FC = () => { - const history = useHistory(); +const SIDEBAR_HIDDEN_PATHS = ['/chats/settings', '/chats/new', '/chats/:chatId', '/chats/shoutbox']; - const path = history.location.pathname; - const isSidebarHidden = matchPath(path, { - path: ['/chats/settings', '/chats/new', '/chats/:chatId', '/chats/shoutbox'], - exact: true, +const ChatPage: React.FC = () => { + const isSidebarHidden = useMatches({ + select: (matches) => SIDEBAR_HIDDEN_PATHS.some((path) => matches.some(match => match.pathname === path)), }); const containerRef = useRef(null); diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-empty.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-empty.tsx index 2ef8e7bc0..7355472d1 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-empty.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-empty.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Button from 'pl-fe/components/ui/button'; import Stack from 'pl-fe/components/ui/stack'; @@ -11,10 +11,10 @@ interface IBlankslate { /** To display on the chats main page when no message is selected. */ const BlankslateEmpty: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const handleNewChat = () => { - history.push('/chats/new'); + navigate({ to: '/chats/new' }); }; return ( diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-with-chats.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-with-chats.tsx index 9e4847ba5..aa6a478a5 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-with-chats.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/blankslate-with-chats.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Button from 'pl-fe/components/ui/button'; import Stack from 'pl-fe/components/ui/stack'; @@ -8,10 +8,10 @@ import Text from 'pl-fe/components/ui/text'; /** To display on the chats main page when no message is selected, but chats are present. */ const BlankslateWithChats = () => { - const history = useHistory(); + const navigate = useNavigate(); const handleNewChat = () => { - history.push('/chats/new'); + navigate({ to: '/chats/new' }); }; return ( diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx index ba6b2fe1b..8f929b0e2 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx @@ -1,6 +1,7 @@ +import { useNavigate } from '@tanstack/react-router'; import React, { useRef } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { Link, useHistory, useParams } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import DropdownMenu, { type Menu } from 'pl-fe/components/dropdown-menu'; import Avatar from 'pl-fe/components/ui/avatar'; @@ -38,7 +39,7 @@ const messages = defineMessages({ const ChatPageMain = () => { const intl = useIntl(); const features = useFeatures(); - const history = useHistory(); + const navigate = useNavigate(); const { chatId } = useParams<{ chatId: string }>(); @@ -82,7 +83,7 @@ const ChatPageMain = () => { onConfirm: () => { deleteChat.mutate(undefined, { onSuccess() { - history.push('/chats'); + navigate({ to: '/chats' }); }, }); }, @@ -127,7 +128,7 @@ const ChatPageMain = () => { history.push('/chats')} + onClick={() => navigate({ to: '/chats' })} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-new.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-new.tsx index c1e63ab66..68e4a7988 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-new.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { CardTitle } from 'pl-fe/components/ui/card'; import HStack from 'pl-fe/components/ui/hstack'; @@ -19,7 +19,7 @@ interface IChatPageNew { /** New message form to create a chat. */ const ChatPageNew: React.FC = () => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); return ( @@ -28,7 +28,7 @@ const ChatPageNew: React.FC = () => { history.push('/chats')} + onClick={() => navigate({ to: '/chats' })} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-settings.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-settings.tsx index e0e8e0525..c74e83b0e 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-settings.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { changeSetting } from 'pl-fe/actions/settings'; import List, { ListItem } from 'pl-fe/components/list'; @@ -33,7 +33,7 @@ const messages = defineMessages({ const ChatPageSettings = () => { const { account } = useOwnAccount(); const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const settings = useSettings(); const updateCredentials = useUpdateCredentials(); @@ -58,7 +58,7 @@ const ChatPageSettings = () => { history.push('/chats')} + onClick={() => navigate({ to: '/chats' })} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-shoutbox.tsx index 0097543da..417701f63 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-shoutbox.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; @@ -13,7 +13,7 @@ import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config'; import Shoutbox from '../../shoutbox'; const ChatPageShoutbox = () => { - const history = useHistory(); + const navigate = useNavigate(); const instance = useInstance(); const { logo } = usePlFeConfig(); @@ -25,7 +25,7 @@ const ChatPageShoutbox = () => { history.push('/chats')} + onClick={() => navigate({ to: '/chats' })} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx index 3105f4516..918d6244f 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { CardTitle } from 'pl-fe/components/ui/card'; import HStack from 'pl-fe/components/ui/hstack'; @@ -17,18 +17,22 @@ const messages = defineMessages({ const ChatPageSidebar = () => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const handleClickChat = (chat: Chat | 'shoutbox') => { - history.push(`/chats/${chat === 'shoutbox' ? 'shoutbox' : chat.id}`); + if (chat === 'shoutbox') { + navigate({ to: '/chats/shoutbox' }); + } else { + navigate({ to: '/chats/$chatId', params: { chatId: chat.id } }); + } }; const handleChatCreate = () => { - history.push('/chats/new'); + navigate({ to: '/chats/new' }); }; const handleSettingsClick = () => { - history.push('/chats/settings'); + navigate({ to: '/chats/settings' }); }; return ( diff --git a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx index abaed7f32..a8a004db8 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import React, { useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Icon from 'pl-fe/components/ui/icon'; import Input from 'pl-fe/components/ui/input'; @@ -31,14 +31,13 @@ const ChatSearch: React.FC = ({ isMainPage = false }) => { const parentRef = useRef(null); const intl = useIntl(); - const debounce = useDebounce; - const history = useHistory(); + const navigate = useNavigate(); const { changeScreen } = useChatContext(); const { getOrCreateChatByAccountId } = useChats(); const [value, setValue] = useState(''); - const debouncedValue = debounce(value as string, 300); + const debouncedValue = useDebounce(value as string, 300); const accountSearchResult = useAccountSearch(debouncedValue); const { data: accounts, isFetching } = accountSearchResult; @@ -54,7 +53,7 @@ const ChatSearch: React.FC = ({ isMainPage = false }) => { }, onSuccess: (response) => { if (isMainPage) { - history.push(`/chats/${response.id}`); + navigate({ to: '/chats/$chatId', params: { chatId: response.id } }); } else { changeScreen(ChatWidgetScreens.CHAT, response.id); } diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-widget.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-widget.tsx index 4ff43969d..a44c0a3d3 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-widget.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-widget.tsx @@ -1,17 +1,15 @@ +import { useMatch } from '@tanstack/react-router'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import { ChatProvider } from 'pl-fe/contexts/chat-context'; +import { layouts } from 'pl-fe/features/ui/router'; import ChatPane from '../chat-pane/chat-pane'; const ChatWidget = () => { - const history = useHistory(); + const match = useMatch({ from: layouts.chats.id, shouldThrow: false }); - const path = history.location.pathname; - const isChatsPath = Boolean(path.match(/^\/chats/)); - - if (isChatsPath) { + if (match) { return null; } diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx index f415213d5..4af127018 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx @@ -1,5 +1,5 @@ +import { Link } from '@tanstack/react-router'; import React, { useRef } from 'react'; -import { Link } from 'react-router-dom'; import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; @@ -69,7 +69,7 @@ const ChatWindow = () => { {isOpen && ( - + )} diff --git a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx index b0f6dc4d8..4f10da0f8 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { Link } from 'react-router-dom'; import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; @@ -40,7 +40,7 @@ const ShoutboxMessage: React.FC = ({ message, isMyMessage }) = })} > {!isMyMessage && ( - + = ({ statusId }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - const history = useHistory(); + const navigate = useNavigate(); const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector((state) => statusId ? getStatus(state, { id: statusId }) : undefined); @@ -129,7 +129,7 @@ const EditEvent: React.FC = ({ statusId }) => { joinMode: approvalRequired ? 'restricted' : 'free', location, })).then((status) => { - if (status) history.push(`/@${status.account.acct}/events/${status.id}`); + if (status) navigate({ to: '/@{$username}/events/$statusId', params: { username: status.account.acct, statusId: status.id } }); dispatch(resetCompose(composeId)); }).catch(() => { }); diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 02486f9c1..a56db4b4d 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -2,7 +2,6 @@ import clsx from 'clsx'; import { $getNodeByKey, CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical'; import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { length } from 'stringz'; import { @@ -124,7 +123,6 @@ interface IComposeForm { } const ComposeForm = ({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, withAvatar, transparent, compact }: IComposeForm) => { - const history = useHistory(); const intl = useIntl(); const dispatch = useAppDispatch(); const { configuration } = useInstance(); @@ -194,15 +192,17 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab if (!canSubmit) return; e?.preventDefault(); - dispatch(submitCompose(id, { history, onSuccess: () => { - editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); - } })); + dispatch(submitCompose(id, { + onSuccess: () => { + editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); + }, + })); }; const handlePreview = (e?: React.FormEvent) => { e?.preventDefault(); - dispatch(submitCompose(id, { history }, true)); + dispatch(submitCompose(id, {}, true)); }; const handleSaveDraft = (e?: React.FormEvent) => { diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index ff74f357e..d0b8f7c43 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -30,7 +30,7 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { defaultMessage='Posting to {groupLink}' values={{ groupLink: ( - + ), diff --git a/packages/pl-fe/src/features/compose/containers/warning-container.tsx b/packages/pl-fe/src/features/compose/containers/warning-container.tsx index 1c35c3c28..62260ba40 100644 --- a/packages/pl-fe/src/features/compose/containers/warning-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/warning-container.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useCompose } from 'pl-fe/hooks/use-compose'; diff --git a/packages/pl-fe/src/features/conversations/components/conversation.tsx b/packages/pl-fe/src/features/conversations/components/conversation.tsx index 3b0e05064..2baa0b19f 100644 --- a/packages/pl-fe/src/features/conversations/components/conversation.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversation.tsx @@ -1,5 +1,5 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import { markConversationRead } from 'pl-fe/actions/conversations'; import StatusContainer from 'pl-fe/containers/status-container'; @@ -15,7 +15,7 @@ interface IConversation { const Conversation: React.FC = ({ conversationId, onMoveUp, onMoveDown }) => { const dispatch = useAppDispatch(); - const history = useHistory(); + const navigate = useNavigate(); const { accounts, unread, lastStatusId } = useAppSelector((state) => { const conversation = state.conversations.items.find(x => x.id === conversationId)!; @@ -32,7 +32,7 @@ const Conversation: React.FC = ({ conversationId, onMoveUp, onMov dispatch(markConversationRead(conversationId)); } - history.push(`/statuses/${lastStatusId}`); + if (lastStatusId) navigate({ to: '/@{$username}/posts/$statusId', params: { username: accounts[0].acct, statusId: lastStatusId } }); }; const handleHotkeyMoveUp = () => { diff --git a/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx b/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx index 96032e34a..7044c7091 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Text from 'pl-fe/components/ui/text'; import Widget from 'pl-fe/components/ui/widget'; @@ -19,7 +19,7 @@ interface ICryptoDonatePanel { const CryptoDonatePanel: React.FC = ({ limit = 3 }): JSX.Element | null => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const instance = useInstance(); const addresses = usePlFeConfig().cryptoAddresses; @@ -29,7 +29,7 @@ const CryptoDonatePanel: React.FC = ({ limit = 3 }): JSX.Ele } const handleAction = () => { - history.push('/donate/crypto'); + navigate({ to: '/donate/crypto' }); }; return ( diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index 03627ac49..7158c817d 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -1,6 +1,6 @@ +import { Link, useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { Link, useHistory } from 'react-router-dom'; import { directCompose, mentionCompose, quoteCompose } from 'pl-fe/actions/compose'; import { fetchEventIcs } from 'pl-fe/actions/events'; @@ -78,7 +78,7 @@ interface IEventHeader { const EventHeader: React.FC = ({ status }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - const history = useHistory(); + const navigate = useNavigate(); const { openModal } = useModalsActions(); const { getOrCreateChatByAccountId } = useChats(); @@ -175,7 +175,7 @@ const EventHeader: React.FC = ({ status }) => { const handleChatClick = () => { getOrCreateChatByAccountId(account.id) - .then((chat) => history.push(`/chats/${chat.id}`)) + .then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } })) .catch(() => {}); }; @@ -355,7 +355,8 @@ const EventHeader: React.FC = ({ status }) => { menu.push({ text: intl.formatMessage(messages.adminAccount, { name: username }), - to: `/pl-fe/admin/accounts/${account.id}`, + to: '/pl-fe/admin/accounts/$accountId', + params: { accountId: account.id }, icon: require('@phosphor-icons/core/regular/gavel.svg'), }); @@ -431,7 +432,8 @@ const EventHeader: React.FC = ({ status }) => { @@ -447,7 +449,7 @@ const EventHeader: React.FC = ({ status }) => { defaultMessage='Organized by {name}' values={{ name: ( - + {account.verified && } diff --git a/packages/pl-fe/src/features/group/components/group-action-button.tsx b/packages/pl-fe/src/features/group/components/group-action-button.tsx index 1c6ff5557..bb53394ad 100644 --- a/packages/pl-fe/src/features/group/components/group-action-button.tsx +++ b/packages/pl-fe/src/features/group/components/group-action-button.tsx @@ -84,7 +84,8 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { return ( diff --git a/packages/pl-fe/src/features/group/components/group-member-count.tsx b/packages/pl-fe/src/features/group/components/group-member-count.tsx index 101a692f4..67e8ba58f 100644 --- a/packages/pl-fe/src/features/group/components/group-member-count.tsx +++ b/packages/pl-fe/src/features/group/components/group-member-count.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import Text from 'pl-fe/components/ui/text'; import { shortNumberFormat } from 'pl-fe/utils/numbers'; @@ -12,7 +12,7 @@ interface IGroupMemberCount { } const GroupMemberCount = ({ group }: IGroupMemberCount) => ( - + {shortNumberFormat(group.members_count)} {' '} diff --git a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx index 48004e3f2..43aba4bac 100644 --- a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx +++ b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import GroupAvatar from 'pl-fe/components/groups/group-avatar'; import HStack from 'pl-fe/components/ui/hstack'; @@ -27,7 +27,7 @@ const GroupListItem = (props: IGroupListItem) => { justifyContent='between' data-testid='group-list-item' > - + @@ -258,7 +259,7 @@ const Notification: React.FC = (props) => { const { mutate: reblogStatus } = useReblogStatus(status?.id!); const { mutate: unreblogStatus } = useUnreblogStatus(status?.id!); - const history = useHistory(); + const navigate = useNavigate(); const intl = useIntl(); const instance = useInstance(); @@ -268,7 +269,7 @@ const Notification: React.FC = (props) => { const handleOpen = () => { if (status && typeof status === 'object' && account && typeof account === 'object') { - history.push(`/@${account.acct}/posts/${status.id}`); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: account.acct, statusId: status.id } }); } else { handleOpenProfile(); } @@ -276,7 +277,7 @@ const Notification: React.FC = (props) => { const handleOpenProfile = () => { if (account && typeof account === 'object') { - history.push(`/@${account.acct}`); + navigate({ to: '/@{$username}', params: { username: account.acct } }); } }; diff --git a/packages/pl-fe/src/features/remote-timeline/components/pinned-hosts-picker.tsx b/packages/pl-fe/src/features/remote-timeline/components/pinned-hosts-picker.tsx index 067af74c8..45900b8bb 100644 --- a/packages/pl-fe/src/features/remote-timeline/components/pinned-hosts-picker.tsx +++ b/packages/pl-fe/src/features/remote-timeline/components/pinned-hosts-picker.tsx @@ -20,7 +20,8 @@ const PinnedHostsPicker: React.FC = ({ host: activeHost }) = {pinnedHosts.map((host) => (
- + {/* - + */} layouts.default, - path: '/list/$id', + path: '/list/$listId', component: ListTimeline, beforeLoad: ({ context: { features } }) => { if (!features.lists) throw notFound(); @@ -407,7 +407,7 @@ export const circlesRoute = createRoute({ export const circleTimelineRoute = createRoute({ getParentRoute: () => layouts.default, - path: '/circles/$id', + path: '/circles/$circleId', component: CircleTimeline, beforeLoad: ({ context: { features } }) => { if (!features.circles) throw notFound(); @@ -445,12 +445,21 @@ export const notificationsRoute = createRoute({ export const searchRoute = createRoute({ getParentRoute: () => layouts.search, path: '/search', + validateSearch: v.object({ + type: v.optional(v.picklist(['accounts', 'statuses', 'hashtags', 'links']), 'accounts'), + q: v.optional(v.string()), + accountId: v.optional(v.string()), + }), component: Search, }); export const directoryRoute = createRoute({ getParentRoute: () => layouts.default, path: '/directory', + validateSearch: v.object({ + order: v.optional(v.picklist(['active', 'new']), 'active'), + local: v.optional(v.boolean(), false), + }), component: Directory, beforeLoad: ({ context: { features } }) => { if (!features.profileDirectory) throw notFound(); @@ -513,7 +522,7 @@ export const shoutboxRoute = createRoute({ }, }); -export const chatIdRoute = createRoute({ +export const chatRoute = createRoute({ getParentRoute: () => layouts.chats, path: '/chats/$chatId', component: ChatIndex, @@ -604,7 +613,7 @@ export const profileRoute = createRoute({ path: '/', component: AccountTimeline, validateSearch: v.object({ - with_replies: v.optional(v.boolean(), false), + with_replies: v.optional(v.boolean()), }), }); @@ -960,6 +969,11 @@ export const adminReportsRoute = createRoute({ getParentRoute: () => layouts.admin, path: '/pl-fe/admin/reports', component: Dashboard, + validateSearch: v.object({ + resolved: v.optional(v.boolean(), false), + account_id: v.optional(v.string()), + target_account_id: v.optional(v.string()), + }), beforeLoad: (options) => { requireAuth(options); if (!options.context.isAdmin) throw notFound(); @@ -988,6 +1002,9 @@ export const adminUsersRoute = createRoute({ getParentRoute: () => layouts.admin, path: '/pl-fe/admin/users', component: UserIndex, + validateSearch: v.object({ + q: v.optional(v.string()), + }), beforeLoad: ({ context: { isAdmin } }) => { if (!isAdmin) throw notFound(); }, @@ -1047,13 +1064,7 @@ export const serverInfoRoute = createRoute({ export const aboutRoute = createRoute({ getParentRoute: () => layouts.default, - path: '/about/$slug', - component: AboutPage, -}); - -export const aboutIndexRoute = createRoute({ - getParentRoute: () => layouts.default, - path: '/about', + path: '/about/{-$slug}', component: AboutPage, }); @@ -1145,7 +1156,7 @@ const routeTree = rootRoute.addChildren([ chatsNewRoute, chatsSettingsRoute, shoutboxRoute, - chatIdRoute, + chatRoute, ]), layouts.default.addChildren([ conversationsRoute, @@ -1189,7 +1200,6 @@ const routeTree = rootRoute.addChildren([ settingsPrivacyRoute, plFeConfigRoute, aboutRoute, - aboutIndexRoute, shareRoute, developersRoute, developersAppsRoute, diff --git a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx index b2566ba1b..1eb12a3c9 100644 --- a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx +++ b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx @@ -1,5 +1,5 @@ +import { useNavigate, useRouter } from '@tanstack/react-router'; import React, { useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; import { resetCompose } from 'pl-fe/actions/compose'; import { FOCUS_EDITOR_COMMAND } from 'pl-fe/features/compose/editor/plugins/focus-plugin'; @@ -43,7 +43,8 @@ interface IGlobalHotkeys { } const GlobalHotkeys: React.FC = ({ children, node }) => { - const history = useHistory(); + const navigate = useNavigate(); + const { history } = useRouter(); const dispatch = useAppDispatch(); const { account } = useOwnAccount(); const { openModal } = useModalsActions(); @@ -72,7 +73,7 @@ const GlobalHotkeys: React.FC = ({ children, node }) => { if (element?.checkVisibility()) { element.focus(); } else { - history.push('/search'); + navigate({ to: '/search' }); } }; @@ -82,10 +83,10 @@ const GlobalHotkeys: React.FC = ({ children, node }) => { }; const handleHotkeyBack = () => { - if (window.history && window.history.length === 1) { - history.push('/'); + if (!history.canGoBack) { + navigate({ to: '/' }); } else { - history.goBack(); + history.back(); } }; @@ -94,33 +95,33 @@ const GlobalHotkeys: React.FC = ({ children, node }) => { }; const handleHotkeyGoToHome = () => { - history.push('/'); + navigate({ to: '/' }); }; const handleHotkeyGoToNotifications = () => { - history.push('/notifications'); + navigate({ to: '/notifications' }); }; const handleHotkeyGoToFavourites = () => { if (!account) return; - history.push(`/@${account.username}/favorites`); + navigate({ to: '/@{$username}/favorites', params: { username: account.acct } }); }; const handleHotkeyGoToProfile = () => { if (!account) return; - history.push(`/@${account.username}`); + navigate({ to: '/@{$username}', params: { username: account.acct } }); }; const handleHotkeyGoToBlocked = () => { - history.push('/blocks'); + navigate({ to: '/blocks' }); }; const handleHotkeyGoToMuted = () => { - history.push('/mutes'); + navigate({ to: '/mutes' }); }; const handleHotkeyGoToRequests = () => { - history.push('/follow_requests'); + navigate({ to: '/follow_requests' }); }; type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void }; diff --git a/packages/pl-fe/src/layouts/event-layout.tsx b/packages/pl-fe/src/layouts/event-layout.tsx index 5bff2415e..5e3e5315e 100644 --- a/packages/pl-fe/src/layouts/event-layout.tsx +++ b/packages/pl-fe/src/layouts/event-layout.tsx @@ -1,12 +1,11 @@ -import { Outlet } from '@tanstack/react-router'; +import { Outlet, useLocation, useNavigate } from '@tanstack/react-router'; import React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Column from 'pl-fe/components/ui/column'; import Layout from 'pl-fe/components/ui/layout'; -import Tabs from 'pl-fe/components/ui/tabs'; +import Tabs, { type Item } from 'pl-fe/components/ui/tabs'; import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; import LinkFooter from 'pl-fe/features/ui/components/link-footer'; import { layouts } from 'pl-fe/features/ui/router'; @@ -28,31 +27,34 @@ const EventLayout = () => { const me = useAppSelector(state => state.me); const features = useFeatures(); - const history = useHistory(); + const navigate = useNavigate(); + const location = useLocation(); const status = useAppSelector(state => getStatus(state, { id: statusId }) || undefined); const event = status?.event; if (status && !event) { - history.push(`/@${status.account.acct}/posts/${status.id}`); + navigate({ to: '/@{$username}/posts/$statusId', params: { username: status.account.acct, statusId: status.id } }); return ( ); } - const pathname = history.location.pathname; + const pathname = location.pathname; const activeItem = pathname.endsWith('/discussion') ? 'discussion' : 'info'; - const tabs = status ? [ + const tabs: Array = status ? [ { text: , - to: `/@${status.account.acct}/events/${status.id}`, + to: '/@{$username}/events/$statusId', + params: { username: status.account.acct, statusId: status.id }, name: 'info', }, { text: , - to: `/@${status.account.acct}/events/${status.id}/discussion`, + to: '/@{$username}/events/$statusId/discussion', + params: { username: status.account.acct, statusId: status.id }, name: 'discussion', }, ] : []; diff --git a/packages/pl-fe/src/layouts/group-layout.tsx b/packages/pl-fe/src/layouts/group-layout.tsx index d453d65ec..282fc55a7 100644 --- a/packages/pl-fe/src/layouts/group-layout.tsx +++ b/packages/pl-fe/src/layouts/group-layout.tsx @@ -9,7 +9,7 @@ import Column from 'pl-fe/components/ui/column'; import Icon from 'pl-fe/components/ui/icon'; import Layout from 'pl-fe/components/ui/layout'; import Stack from 'pl-fe/components/ui/stack'; -import Tabs from 'pl-fe/components/ui/tabs'; +import Tabs, { type Item } from 'pl-fe/components/ui/tabs'; import Text from 'pl-fe/components/ui/text'; import GroupHeader from 'pl-fe/features/group/components/group-header'; import LinkFooter from 'pl-fe/features/ui/components/link-footer'; @@ -59,22 +59,26 @@ const GroupLayout = () => { const isPrivate = group?.locked; const tabItems = useMemo(() => { - const items = []; - items.push({ - text: intl.formatMessage(messages.all), - to: `/groups/${groupId}`, - name: '/groups/:groupId', - }); + const items: Array = [ + { + text: intl.formatMessage(messages.all), + to: '/groups/$groupId', + params: { groupId }, + name: '/groups/:groupId', + }, + ]; items.push( { text: intl.formatMessage(messages.media), - to: `/groups/${groupId}/media`, + to: '/groups/$groupId/media', + params: { groupId }, name: '/groups/:groupId/media', }, { text: intl.formatMessage(messages.members), - to: `/groups/${groupId}/members`, + to: '/groups/$groupId/members', + params: { groupId }, name: '/groups/:groupId/members', count: pending.length, }, diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index 03cef972a..32f80df5e 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -1,8 +1,7 @@ -import { Outlet } from '@tanstack/react-router'; +import { Outlet , Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useRef } from 'react'; import { useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; import { uploadCompose } from 'pl-fe/actions/compose'; import Avatar from 'pl-fe/components/ui/avatar'; @@ -62,7 +61,7 @@ const HomeLayout = () => { >
{!disableUserProvidedMedia && ( - + )} diff --git a/packages/pl-fe/src/layouts/profile-layout.tsx b/packages/pl-fe/src/layouts/profile-layout.tsx index a20e9fad8..b39cfaa94 100644 --- a/packages/pl-fe/src/layouts/profile-layout.tsx +++ b/packages/pl-fe/src/layouts/profile-layout.tsx @@ -1,13 +1,12 @@ -import { Outlet } from '@tanstack/react-router'; +import { Navigate, Outlet, useLocation } from '@tanstack/react-router'; import React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { Redirect, useHistory } from 'react-router-dom'; import { useAccountLookup } from 'pl-fe/api/hooks/accounts/use-account-lookup'; import Column from 'pl-fe/components/ui/column'; import Layout from 'pl-fe/components/ui/layout'; -import Tabs from 'pl-fe/components/ui/tabs'; +import Tabs, { type Item } from 'pl-fe/components/ui/tabs'; import Header from 'pl-fe/features/account/components/header'; import LinkFooter from 'pl-fe/features/ui/components/link-footer'; import { layouts } from 'pl-fe/features/ui/router'; @@ -28,7 +27,7 @@ import { getAcct } from 'pl-fe/utils/accounts'; /** Layout to display a user's profile. */ const ProfileLayout: React.FC = () => { const { username } = layouts.profile.useParams(); - const history = useHistory(); + const location = useLocation(); const { account, isUnauthorized } = useAccountLookup(username, { withRelationship: true }); @@ -37,28 +36,33 @@ const ProfileLayout: React.FC = () => { const { displayFqn } = usePlFeConfig(); if (isUnauthorized) { - return ; + localStorage.setItem('plfe:redirect_uri', location.href); + return ; } // Fix case of username if (account && account.acct !== username) { - return ; + return ; } - const tabItems = [ + const tabItems: Array = [ { text: , - to: `/@${username}`, + to: '/@{$username}', + params: { username }, name: 'profile', }, { text: , - to: `/@${username}/with_replies`, + to: '/@{$username}', + params: { username }, + search: { with_replies: true }, name: 'replies', }, { text: , - to: `/@${username}/media`, + to: '/@{$username}/media', + params: { username }, name: 'media', }, ]; @@ -68,14 +72,15 @@ const ProfileLayout: React.FC = () => { if (ownAccount || account.hide_favorites === false) { tabItems.push({ text: , - to: `/@${account.acct}/favorites`, + to: '/@{$username}/favorites', + params: { username: account.acct }, name: 'likes', }); } } let activeItem; - const pathname = history.location.pathname.replace(`@${username}/`, ''); + const pathname = location.pathname.replace(`@${username}/`, ''); if (pathname.endsWith('/with_replies')) { activeItem = 'replies'; } else if (pathname.endsWith('/media')) { diff --git a/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx b/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx index e294fb86b..3d07d5648 100644 --- a/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx +++ b/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { changeComposeInteractionPolicyOption } from 'pl-fe/actions/compose'; import Modal from 'pl-fe/components/ui/modal'; diff --git a/packages/pl-fe/src/modals/media-modal.tsx b/packages/pl-fe/src/modals/media-modal.tsx index 994b7ddd6..7b78ca32e 100644 --- a/packages/pl-fe/src/modals/media-modal.tsx +++ b/packages/pl-fe/src/modals/media-modal.tsx @@ -1,7 +1,7 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import ReactSwipeableViews from 'react-swipeable-views'; import { fetchStatusWithContext } from 'pl-fe/actions/statuses'; @@ -114,7 +114,7 @@ const MediaModal: React.FC = (props) => { } const link = (status && ( - + )); diff --git a/packages/pl-fe/src/modals/unauthorized-modal.tsx b/packages/pl-fe/src/modals/unauthorized-modal.tsx index c7551b709..7c9ea526d 100644 --- a/packages/pl-fe/src/modals/unauthorized-modal.tsx +++ b/packages/pl-fe/src/modals/unauthorized-modal.tsx @@ -1,6 +1,6 @@ +import { useNavigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import Button from 'pl-fe/components/ui/button'; import Form from 'pl-fe/components/ui/form'; @@ -37,7 +37,7 @@ interface UnauthorizedModalProps { /** Modal to display when a logged-out user tries to do something that requires login. */ const UnauthorizedModal: React.FC = ({ action, onClose, account: accountId, ap_id: apId }) => { const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const instance = useInstance(); const { isOpen } = useRegistrationStatus(); const client = useClient(); @@ -71,12 +71,12 @@ const UnauthorizedModal: React.FC = ({ }; const onLogin = () => { - history.push('/login'); + navigate({ to: '/login' }); onClickClose(); }; const onRegister = () => { - history.push('/signup'); + navigate({ to: '/signup' }); onClickClose(); }; diff --git a/packages/pl-fe/src/pages/account-lists/directory.tsx b/packages/pl-fe/src/pages/account-lists/directory.tsx index d1ce8e3bf..f562fea71 100644 --- a/packages/pl-fe/src/pages/account-lists/directory.tsx +++ b/packages/pl-fe/src/pages/account-lists/directory.tsx @@ -1,8 +1,7 @@ +import { Link, useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import Account from 'pl-fe/components/account'; @@ -19,6 +18,7 @@ import Column from 'pl-fe/components/ui/column'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import ActionButton from 'pl-fe/features/ui/components/action-button'; +import { directoryRoute } from 'pl-fe/features/ui/router'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; @@ -72,7 +72,7 @@ const AccountCard: React.FC = ({ id }) => {
)} - + = ({ id }) => { const DirectoryPage = () => { const intl = useIntl(); - const [params, setParams] = useSearchParams(); + const { order, local } = directoryRoute.useSearch(); + const navigate = useNavigate({ from: directoryRoute.fullPath }); const instance = useInstance(); const features = useFeatures(); - const order = (params.get('order') || 'active') as 'active' | 'new'; - const local = !!params.get('local'); - const { data: accountIds = [], isLoading, hasNextPage, fetchNextPage } = useDirectory(order, local); const handleChangeOrder: React.ChangeEventHandler = e => { - setParams({ local: local ? 'true' : '', order: e.target.value }); + navigate({ search: ({ local }) => ({ local, order: e.target.value as 'active' | 'new' }) }); }; const handleChangeLocal: React.ChangeEventHandler = e => { - setParams({ local: e.target.value === '1' ? 'true' : '', order }); + navigate({ search: ({ order }) => ({ local: e.target.checked, order }) }); }; const handleLoadMore = () => { diff --git a/packages/pl-fe/src/pages/account-lists/follow-requests.tsx b/packages/pl-fe/src/pages/account-lists/follow-requests.tsx index d905fe212..6ace7adb8 100644 --- a/packages/pl-fe/src/pages/account-lists/follow-requests.tsx +++ b/packages/pl-fe/src/pages/account-lists/follow-requests.tsx @@ -1,6 +1,6 @@ +import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useRouteMatch } from 'react-router-dom'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import Account from 'pl-fe/components/account'; @@ -8,7 +8,8 @@ import { AuthorizeRejectButtons } from 'pl-fe/components/authorize-reject-button import ScrollableList from 'pl-fe/components/scrollable-list'; import Column from 'pl-fe/components/ui/column'; import Spinner from 'pl-fe/components/ui/spinner'; -import Tabs from 'pl-fe/components/ui/tabs'; +import Tabs, { type Item } from 'pl-fe/components/ui/tabs'; +import { followRequestsRoute } from 'pl-fe/features/ui/router'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useAcceptFollowRequestMutation, useFollowRequests, useRejectFollowRequestMutation } from 'pl-fe/queries/accounts/use-follow-requests'; @@ -51,14 +52,14 @@ const AccountAuthorize: React.FC = ({ id }) => { const FollowRequestsTabs = () => { const intl = useIntl(); - const match = useRouteMatch(); + const match = useMatch({ from: followRequestsRoute.id, shouldThrow: false }); const features = useFeatures(); if (!features.outgoingFollowRequests) { return null; } - const tabs = [{ + const tabs: Array = [{ name: '/follow_requests', text: intl.formatMessage(messages.followRequests), to: '/follow_requests', @@ -68,7 +69,7 @@ const FollowRequestsTabs = () => { to: '/outgoing_follow_requests', }]; - return ; + return ; }; const FollowRequestsPage: React.FC = () => { diff --git a/packages/pl-fe/src/pages/accounts/account-gallery.tsx b/packages/pl-fe/src/pages/accounts/account-gallery.tsx index f102cf9ba..a42101904 100644 --- a/packages/pl-fe/src/pages/accounts/account-gallery.tsx +++ b/packages/pl-fe/src/pages/accounts/account-gallery.tsx @@ -1,7 +1,7 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link, useParams } from 'react-router-dom'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import { useAccountLookup } from 'pl-fe/api/hooks/accounts/use-account-lookup'; @@ -12,6 +12,7 @@ import MissingIndicator from 'pl-fe/components/missing-indicator'; import StillImage from 'pl-fe/components/still-image'; import Column from 'pl-fe/components/ui/column'; import Spinner from 'pl-fe/components/ui/spinner'; +import { profileMediaRoute } from 'pl-fe/features/ui/router'; import { type AccountGalleryAttachment, useAccountGallery } from 'pl-fe/hooks/use-account-gallery'; import { isIOS } from 'pl-fe/is-mobile'; import { useModalsActions } from 'pl-fe/stores/modals'; @@ -126,7 +127,7 @@ const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => return (
- + = ({ attachment, onOpenMedia, isLast }) => }; const AccountGalleryPage = () => { - const { username } = useParams<{ username: string }>(); + const { username } = profileMediaRoute.useParams(); const { openModal } = useModalsActions(); const { diff --git a/packages/pl-fe/src/pages/accounts/account-timeline.tsx b/packages/pl-fe/src/pages/accounts/account-timeline.tsx index fc2fc5e69..03c10d0e4 100644 --- a/packages/pl-fe/src/pages/accounts/account-timeline.tsx +++ b/packages/pl-fe/src/pages/accounts/account-timeline.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchAccountByUsername } from 'pl-fe/actions/accounts'; import { fetchAccountTimeline } from 'pl-fe/actions/timelines'; @@ -23,7 +22,6 @@ const AccountTimelinePage: React.FC = () => { const { username } = profileRoute.useParams(); const { with_replies: withReplies = false } = profileRoute.useSearch(); - const history = useHistory(); const dispatch = useAppDispatch(); const features = useFeatures(); const settings = useSettings(); @@ -44,7 +42,7 @@ const AccountTimelinePage: React.FC = () => { const accountUsername = account?.username || username; useEffect(() => { - dispatch(fetchAccountByUsername(username, history)) + dispatch(fetchAccountByUsername(username)) .then(() => setAccountLoading(false)) .catch(() => setAccountLoading(false)); }, [username]); diff --git a/packages/pl-fe/src/pages/auth/login.tsx b/packages/pl-fe/src/pages/auth/login.tsx index f9722995e..93ba8396d 100644 --- a/packages/pl-fe/src/pages/auth/login.tsx +++ b/packages/pl-fe/src/pages/auth/login.tsx @@ -1,6 +1,6 @@ +import { Navigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Redirect } from 'react-router-dom'; import { logIn, verifyCredentials, switchAccount } from 'pl-fe/actions/auth'; import { fetchInstance } from 'pl-fe/actions/instance'; @@ -66,11 +66,11 @@ const LoginPage = () => { event.preventDefault(); }; - if (standalone) return ; + if (standalone) return ; if (shouldRedirect) { const redirectUri = getRedirectUrl(); - return ; + return ; } if (mfaAuthNeeded) return ; diff --git a/packages/pl-fe/src/pages/auth/logout.tsx b/packages/pl-fe/src/pages/auth/logout.tsx index d79e763c2..2f2ac5d3b 100644 --- a/packages/pl-fe/src/pages/auth/logout.tsx +++ b/packages/pl-fe/src/pages/auth/logout.tsx @@ -1,6 +1,6 @@ +import { Navigate } from '@tanstack/react-router'; import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Redirect } from 'react-router-dom'; import { logOut } from 'pl-fe/actions/auth'; import Spinner from 'pl-fe/components/ui/spinner'; @@ -17,7 +17,7 @@ const LogoutPage: React.FC = () => { }, []); if (done) { - return ; + return ; } else { return ; } diff --git a/packages/pl-fe/src/pages/auth/password-reset.tsx b/packages/pl-fe/src/pages/auth/password-reset.tsx index 870cf0fc0..ba2ffe0e0 100644 --- a/packages/pl-fe/src/pages/auth/password-reset.tsx +++ b/packages/pl-fe/src/pages/auth/password-reset.tsx @@ -1,6 +1,6 @@ +import { Navigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { Redirect } from 'react-router-dom'; import { resetPassword } from 'pl-fe/actions/security'; import { BigCard } from 'pl-fe/components/big-card'; @@ -39,7 +39,7 @@ const PasswordResetPage = () => { }); }; - if (success) return ; + if (success) return ; return ( }> diff --git a/packages/pl-fe/src/pages/auth/register-with-invite.tsx b/packages/pl-fe/src/pages/auth/register-with-invite.tsx index 5891810b2..64978c277 100644 --- a/packages/pl-fe/src/pages/auth/register-with-invite.tsx +++ b/packages/pl-fe/src/pages/auth/register-with-invite.tsx @@ -1,19 +1,15 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router-dom'; import { BigCard } from 'pl-fe/components/big-card'; import RegistrationForm from 'pl-fe/features/auth-login/components/registration-form'; +import { inviteRoute } from 'pl-fe/features/ui/router'; import { useInstance } from 'pl-fe/hooks/use-instance'; -interface RegisterInviteParams { - token: string; -} - /** Page to register with an invitation. */ const RegisterWithInvitePage: React.FC = () => { + const { token } = inviteRoute.useParams(); const instance = useInstance(); - const { token } = useParams(); const title = ( {
{targetAccount && ( - + @@ -184,7 +184,7 @@ const ReportPage: React.FC = () => { - + @{authorAccount.acct} @@ -216,7 +216,7 @@ const ReportPage: React.FC = () => { {report.assigned_account ? ( - + @{report.assigned_account.acct} diff --git a/packages/pl-fe/src/pages/dashboard/user-index.tsx b/packages/pl-fe/src/pages/dashboard/user-index.tsx index 92dbac381..a25a6fee8 100644 --- a/packages/pl-fe/src/pages/dashboard/user-index.tsx +++ b/packages/pl-fe/src/pages/dashboard/user-index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import ScrollableList from 'pl-fe/components/scrollable-list'; import Column from 'pl-fe/components/ui/column'; import AccountContainer from 'pl-fe/containers/account-container'; +import { adminUsersRoute } from 'pl-fe/features/ui/router'; import { useAdminAccounts } from 'pl-fe/queries/admin/use-accounts'; import { SearchInput } from '../search/search'; @@ -14,8 +14,7 @@ const messages = defineMessages({ }); const UserIndexPage: React.FC = () => { - const [params] = useSearchParams(); - const query = params.get('q') || ''; + const { q: query } = adminUsersRoute.useSearch(); const intl = useIntl(); diff --git a/packages/pl-fe/src/pages/developers/developers.tsx b/packages/pl-fe/src/pages/developers/developers.tsx index d82f90bec..7d9158dba 100644 --- a/packages/pl-fe/src/pages/developers/developers.tsx +++ b/packages/pl-fe/src/pages/developers/developers.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; import Column from 'pl-fe/components/ui/column'; import SvgIcon from 'pl-fe/components/ui/svg-icon'; diff --git a/packages/pl-fe/src/pages/drive/drive.tsx b/packages/pl-fe/src/pages/drive/drive.tsx index c9ddf52e0..cb7f7abc3 100644 --- a/packages/pl-fe/src/pages/drive/drive.tsx +++ b/packages/pl-fe/src/pages/drive/drive.tsx @@ -1,9 +1,9 @@ import defaultIcon from '@phosphor-icons/core/regular/paperclip.svg'; +import { Link, useNavigate } from '@tanstack/react-router'; import { clsx } from 'clsx'; import { mediaAttachmentSchema, type DriveFile, type DriveFolder } from 'pl-api'; import React, { useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { Link, useHistory } from 'react-router-dom'; import * as v from 'valibot'; import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu'; @@ -91,7 +91,7 @@ const Breadcrumbs: React.FC = ({ folderId, depth = 0, onClick }) = ); } else { return ( - + {label} @@ -116,7 +116,8 @@ const Breadcrumbs: React.FC = ({ folderId, depth = 0, onClick }) = ) : ( {data.name} @@ -358,7 +359,7 @@ interface IFolder { } const Folder: React.FC = ({ folder }) => { - const history = useHistory(); + const navigate = useNavigate(); const intl = useIntl(); const { openModal } = useModalsActions(); @@ -367,7 +368,7 @@ const Folder: React.FC = ({ folder }) => { const { mutate: moveFolder } = useMoveDriveFolderMutation(folder.id!); const handleEnterFolder = () => { - history.push(`/drive/${folder.id}`); + navigate({ to: '/drive/{-$folderId}', params: { folderId: folder.id || undefined } }); }; const items: Menu = useMemo(() => { @@ -419,7 +420,8 @@ const Folder: React.FC = ({ folder }) => { { text: intl.formatMessage(messages.folderView), icon: require('@phosphor-icons/core/regular/folder-open.svg'), - to: `/drive/${folder.id}`, + to: '/drive/{-$folderId}', + params: { folderId: folder.id || undefined }, }, { text: intl.formatMessage(messages.folderRename), @@ -516,7 +518,8 @@ const DrivePage: React.FC = () => { } >
diff --git a/packages/pl-fe/src/pages/fun/circle.tsx b/packages/pl-fe/src/pages/fun/circle.tsx index 9627f281c..c88cd2d8e 100644 --- a/packages/pl-fe/src/pages/fun/circle.tsx +++ b/packages/pl-fe/src/pages/fun/circle.tsx @@ -1,6 +1,6 @@ +import { Link } from '@tanstack/react-router'; import React, { useEffect, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; import { processCircle } from 'pl-fe/actions/circle'; import { resetCompose, uploadComposeSuccess, uploadFile } from 'pl-fe/actions/compose'; @@ -208,7 +208,7 @@ const CirclePage: React.FC = () => { > {users?.map(user => ( - + diff --git a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx index 8cf9f22a0..9e91218f2 100644 --- a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx +++ b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx @@ -76,7 +76,7 @@ const GroupBlockedMembers: React.FC = () => { const emptyMessage = ; return ( - + { hasMore={hasNextPage} > {groups.map((group) => ( - + ))} diff --git a/packages/pl-fe/src/pages/groups/manage-group.tsx b/packages/pl-fe/src/pages/groups/manage-group.tsx index 73d30fd0e..560b2d99e 100644 --- a/packages/pl-fe/src/pages/groups/manage-group.tsx +++ b/packages/pl-fe/src/pages/groups/manage-group.tsx @@ -1,7 +1,7 @@ +import { useNavigate } from '@tanstack/react-router'; import { GroupRoles } from 'pl-api'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { useDeleteGroup } from 'pl-fe/api/hooks/groups/use-delete-group'; import { useGroup } from 'pl-fe/api/hooks/groups/use-group'; @@ -34,7 +34,7 @@ const ManageGroup: React.FC = () => { const { groupId } = manageGroupRoute.useParams(); const { openModal } = useModalsActions(); - const history = useHistory(); + const navigate = useNavigate(); const intl = useIntl(); const { group } = useGroup(groupId); @@ -64,14 +64,14 @@ const ManageGroup: React.FC = () => { deleteGroup.mutate(group.id, { onSuccess() { toast.success(intl.formatMessage(messages.deleteSuccess)); - history.push('/groups'); + navigate({ to: '/groups' }); }, }); }, }); return ( - + {isOwner && ( <> diff --git a/packages/pl-fe/src/pages/search/search.tsx b/packages/pl-fe/src/pages/search/search.tsx index 4cbdf6864..064ff4da9 100644 --- a/packages/pl-fe/src/pages/search/search.tsx +++ b/packages/pl-fe/src/pages/search/search.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; import SearchColumn from 'pl-fe/columns/search'; @@ -12,6 +12,7 @@ import Input from 'pl-fe/components/ui/input'; import SvgIcon from 'pl-fe/components/ui/svg-icon'; import Tabs from 'pl-fe/components/ui/tabs'; import Text from 'pl-fe/components/ui/text'; +import { searchRoute } from 'pl-fe/features/ui/router'; import { useFeatures } from 'pl-fe/hooks/use-features'; type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links'; @@ -31,13 +32,14 @@ interface ISearchInput { } const SearchInput: React.FC = ({ placeholder }) => { - const [params, setParams] = useSearchParams(); - const [value, setValue] = useState(params.get('q') || ''); + const { q: query } = searchRoute.useSearch(); + const navigate = useNavigate({ from: searchRoute.fullPath }); + const [value, setValue] = useState(query || ''); const intl = useIntl(); const setQuery = (value: string) => { - setParams(params => ({ ...Object.fromEntries(params.entries()), q: value })); + navigate({ search: (prev) => ({ ...prev, q: value }) }); }; const handleChange = (event: React.ChangeEvent) => { @@ -49,7 +51,7 @@ const SearchInput: React.FC = ({ placeholder }) => { const handleClick = (event: React.MouseEvent) => { event.preventDefault(); - if (params.get('q') === value) { + if (query === value) { if (value.length > 0) { setValue(''); setQuery(''); @@ -92,9 +94,9 @@ const SearchInput: React.FC = ({ placeholder }) => { tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto' onClick={handleClick} - title={params.get('q') === value ? intl.formatMessage(messages.clear) : intl.formatMessage(messages.placeholder)} + title={query === value ? intl.formatMessage(messages.clear) : intl.formatMessage(messages.placeholder)} > - {params.get('q') === value ? ( + {query === value ? ( { const features = useFeatures(); const queryClient = useQueryClient(); - const [params, setParams] = useSearchParams(); + const { q: value = '', type: selectedFilter = 'accounts', accountId } = searchRoute.useSearch(); + const navigate = useNavigate({ from: searchRoute.fullPath }); - const value = params.get('q') || ''; const submitted = !!value.trim(); - const selectedFilter = (params.get('type') || 'accounts') as SearchFilter; - const accountId = params.get('accountId') || undefined; const selectFilter = (newActiveFilter: SearchFilter) => { if (newActiveFilter === selectedFilter) { @@ -129,14 +129,13 @@ const SearchResults = () => { queryKey: ['search', newActiveFilter, value, newActiveFilter === 'statuses' ? { account_id: accountId } : undefined], exact: true, }); - } else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter })); + } else navigate({ search: (prev) => ({ ...prev, type: newActiveFilter }) }); }; const { account } = useAccount(accountId); const handleUnsetAccount = () => { - params.delete('accountId'); - setParams(params => Object.fromEntries(params.entries())); + navigate({ search: ({ accountId, ...prev }) => prev }); }; const renderFilterBar = () => { diff --git a/packages/pl-fe/src/pages/settings/edit-filter.tsx b/packages/pl-fe/src/pages/settings/edit-filter.tsx index 5a3d76380..b180dd226 100644 --- a/packages/pl-fe/src/pages/settings/edit-filter.tsx +++ b/packages/pl-fe/src/pages/settings/edit-filter.tsx @@ -1,7 +1,7 @@ +import { useNavigate } from '@tanstack/react-router'; import { Filter, type FilterContext } from 'pl-api'; import React, { useEffect, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { createFilter, fetchFilter, updateFilter } from 'pl-fe/actions/filters'; import List, { ListItem } from 'pl-fe/components/list'; @@ -98,7 +98,7 @@ const EditFilterPage: React.FC = () => { const { filterId } = editFilterRoute.useParams(); const intl = useIntl(); - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const features = useFeatures(); @@ -152,7 +152,7 @@ const EditFilterPage: React.FC = () => { dispatch(filterId !== 'new' ? updateFilter(filterId, title, expiresIn, context, filterAction, keywords) : createFilter(title, expiresIn, context, filterAction, keywords)).then(() => { - history.push('/filters'); + navigate({ to: '/filters' }); }).catch(() => { toast.error(intl.formatMessage(messages.create_error)); }); diff --git a/packages/pl-fe/src/pages/settings/filters.tsx b/packages/pl-fe/src/pages/settings/filters.tsx index 7b1177a13..fa278fb84 100644 --- a/packages/pl-fe/src/pages/settings/filters.tsx +++ b/packages/pl-fe/src/pages/settings/filters.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchFilters, deleteFilter } from 'pl-fe/actions/filters'; import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; @@ -38,13 +37,10 @@ const contexts = { const FiltersPage = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const history = useHistory(); const { filtersV2 } = useFeatures(); const filters = useAppSelector((state) => state.filters); - const handleFilterEdit = (id: string) => () => history.push(`/filters/${id}`); - const handleFilterDelete = (id: string) => () => { dispatch(deleteFilter(id)).then(() => dispatch(fetchFilters())).catch(() => { toast.error(intl.formatMessage(messages.delete_error)); @@ -61,7 +57,8 @@ const FiltersPage = () => {