diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx
index a1497b4..91cb6e3 100644
--- a/src/app/components/BackRouteHandler.tsx
+++ b/src/app/components/BackRouteHandler.tsx
@@ -1,90 +1,11 @@
-import { ReactNode, useCallback } from 'react';
-import { matchPath, useLocation, useNavigate } from 'react-router-dom';
-import {
- decodeRouteParam,
- getDirectPath,
- getExplorePath,
- getHomePath,
- getInboxPath,
- getSpacePath,
-} from '../pages/pathUtils';
-import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
+import { ReactNode } from 'react';
+import { useBackRoute } from '../hooks/useBackRoute';
type BackRouteHandlerProps = {
children: (onBack: () => void) => ReactNode;
};
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
- const navigate = useNavigate();
- const location = useLocation();
-
- const goBack = useCallback(() => {
- if (
- matchPath(
- {
- path: HOME_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getHomePath());
- return;
- }
- if (
- matchPath(
- {
- path: DIRECT_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getDirectPath());
- return;
- }
- const spaceMatch = matchPath(
- {
- path: SPACE_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- );
- if (spaceMatch?.params.spaceIdOrAlias) {
- const spaceIdOrAlias = decodeRouteParam(spaceMatch.params.spaceIdOrAlias);
- if (spaceIdOrAlias) {
- navigate(getSpacePath(spaceIdOrAlias));
- }
- return;
- }
- if (
- matchPath(
- {
- path: EXPLORE_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getExplorePath());
- return;
- }
- if (
- matchPath(
- {
- path: INBOX_PATH,
- caseSensitive: true,
- end: false,
- },
- location.pathname
- )
- ) {
- navigate(getInboxPath());
- }
- }, [navigate, location]);
+ const { goBack } = useBackRoute();
return children(goBack);
}
diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx
index c8e7f85..29b0e26 100644
--- a/src/app/components/page/Page.tsx
+++ b/src/app/components/page/Page.tsx
@@ -5,6 +5,7 @@ import { ContainerColor } from '../../styles/ContainerColor.css';
import * as css from './style.css';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
+import { useShowCompactMasterView } from '../../hooks/useCompactNav';
type PageRootProps = {
nav: ReactNode;
@@ -13,6 +14,7 @@ type PageRootProps = {
export function PageRoot({ nav, children }: PageRootProps) {
const screenSize = useScreenSizeContext();
+ const showCompactMaster = useShowCompactMasterView();
return (
@@ -20,7 +22,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
{screenSize !== ScreenSize.Mobile && (
)}
- {children}
+ {!showCompactMaster && children}
);
}
diff --git a/src/app/components/title-bar/TitleBar.css.ts b/src/app/components/title-bar/TitleBar.css.ts
index 6a8c770..60af7d4 100644
--- a/src/app/components/title-bar/TitleBar.css.ts
+++ b/src/app/components/title-bar/TitleBar.css.ts
@@ -76,6 +76,11 @@ export const TitleBarDragRegion = style({
color: color.Surface.OnContainer,
});
+export const TitleBarBackButton = style({
+ WebkitAppRegion: 'no-drag',
+ flexShrink: 0,
+});
+
export const SyncStatusBadge = style({
position: 'absolute',
left: '50%',
diff --git a/src/app/components/title-bar/TitleBar.tsx b/src/app/components/title-bar/TitleBar.tsx
index bd7a275..75bab3a 100644
--- a/src/app/components/title-bar/TitleBar.tsx
+++ b/src/app/components/title-bar/TitleBar.tsx
@@ -1,5 +1,5 @@
import React, { ReactNode, useMemo, useEffect, useState, useCallback, useRef } from 'react';
-import { Box, Text } from 'folds';
+import { Box, Text, IconButton } from 'folds';
import { invoke } from '@tauri-apps/api/core';
import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules, SyncState } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
@@ -18,6 +18,8 @@ import { AccountDataEvent } from '../../../types/matrix/accountData';
import { getNotificationMode, NotificationMode } from '../../hooks/useNotificationMode';
import { isRoomId } from '../../utils/matrix';
import { useSyncState } from '../../hooks/useSyncState';
+import { useBackRoute } from '../../hooks/useBackRoute';
+import { Icon, Icons } from '../icons';
type NewMessageNotification = {
id: string;
@@ -42,6 +44,7 @@ export function TitleBar({ mx, children }: TitleBarProps) {
const navigate = useNavigate();
const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom);
+ const { showInTitleBar, goBack } = useBackRoute(mx);
// Sync status state
const [syncStateData, setSyncStateData] = useState<{
@@ -652,6 +655,20 @@ export function TitleBar({ mx, children }: TitleBarProps) {
grow="Yes"
onMouseDown={handleDragStart}
>
+ {showInTitleBar && (
+ e.stopPropagation()}
+ aria-label="Back"
+ >
+
+
+ )}
Paarrot{titleParts.suffix}
diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx
index 6bec403..28d67cf 100644
--- a/src/app/features/lobby/LobbyHeader.tsx
+++ b/src/app/features/lobby/LobbyHeader.tsx
@@ -16,7 +16,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
-import { BackRouteHandler } from '../../components/BackRouteHandler';
+import { useBackRoute } from '../../hooks/useBackRoute';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
@@ -165,6 +165,7 @@ export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: Lobb
const [menuAnchor, setMenuAnchor] = useState();
const [addExisting, setAddExisting] = useState(null);
const screenSize = useScreenSizeContext();
+ const { showInPageHeader, goBack } = useBackRoute();
const name = useRoomName(space);
const avatarMxc = useRoomAvatar(space);
@@ -270,6 +271,13 @@ export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: Lobb
<>
+ {showInPageHeader && (
+
+
+
+
+
+ )}
- {screenSize === ScreenSize.Mobile ? (
+ {showInPageHeader ? (
<>
-
- {(onBack) => (
-
-
-
- )}
-
+
+
+
{showProfile && (
diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx
index f6fa151..2971d09 100644
--- a/src/app/features/room/RoomViewHeader.tsx
+++ b/src/app/features/room/RoomViewHeader.tsx
@@ -36,7 +36,7 @@ import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers';
import { PluginButtonSlot } from '../settings/plugins/PluginButtonSlot';
-import { BackRouteHandler } from '../../components/BackRouteHandler';
+import { useBackRoute } from '../../hooks/useBackRoute';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents';
import { RoomPinMenu } from './room-pin-menu';
@@ -463,6 +463,7 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const screenSize = useScreenSizeContext();
+ const { showInPageHeader, goBack } = useBackRoute();
const room = useRoom();
const space = useSpaceOptionally();
const [menuAnchor, setMenuAnchor] = useState();
@@ -501,21 +502,17 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
};
return (
-
+
- {screenSize === ScreenSize.Mobile && (
-
- {(onBack) => (
-
-
-
-
-
- )}
-
+ {showInPageHeader && (
+
+
+
+
+
)}
- {screenSize !== ScreenSize.Mobile && (
+ {!showInPageHeader && (
+ getSpacePath(getCanonicalAliasOrRoomId(mx, spaceId) ?? spaceIdOrAlias);
+
+export const getBackRouteTarget = ({
+ location,
+ mx,
+ selectedSpaceId,
+}: BackRouteContext): string | null => {
+ const { pathname } = location;
+
+ const homeMatch = matchPath({ path: HOME_PATH, caseSensitive: true, end: false }, pathname);
+ if (homeMatch) {
+ const atRoot = matchPath({ path: HOME_PATH, caseSensitive: true, end: true }, pathname);
+ if (!atRoot) {
+ return getHomePath();
+ }
+ return null;
+ }
+
+ const directMatch = matchPath({ path: DIRECT_PATH, caseSensitive: true, end: false }, pathname);
+ if (directMatch) {
+ const atRoot = matchPath({ path: DIRECT_PATH, caseSensitive: true, end: true }, pathname);
+ if (!atRoot) {
+ return getDirectPath();
+ }
+ return null;
+ }
+
+ const spaceMatch = matchPath({ path: SPACE_PATH, caseSensitive: true, end: false }, pathname);
+ if (spaceMatch?.params.spaceIdOrAlias) {
+ const spaceIdOrAlias = decodeRouteParam(spaceMatch.params.spaceIdOrAlias);
+ if (!spaceIdOrAlias) return null;
+
+ const spaceId = selectedSpaceId ?? spaceIdOrAlias;
+ const listPath = spaceListPath(mx, spaceId, spaceIdOrAlias);
+
+ const inRoom = matchPath({ path: SPACE_ROOM_PATH, caseSensitive: true, end: false }, pathname);
+ if (inRoom) {
+ return listPath;
+ }
+
+ const atLobby = matchPath({ path: SPACE_LOBBY_PATH, caseSensitive: true, end: false }, pathname);
+ if (atLobby) {
+ return listPath;
+ }
+
+ const atFeed = matchPath({ path: SPACE_FEED_PATH, caseSensitive: true, end: false }, pathname);
+ if (atFeed) {
+ return listPath;
+ }
+
+ const atSearch = matchPath({ path: SPACE_SEARCH_PATH, caseSensitive: true, end: false }, pathname);
+ if (atSearch) {
+ return listPath;
+ }
+
+ return null;
+ }
+
+ const exploreMatch = matchPath({ path: EXPLORE_PATH, caseSensitive: true, end: false }, pathname);
+ if (exploreMatch) {
+ const atRoot = matchPath({ path: EXPLORE_PATH, caseSensitive: true, end: true }, pathname);
+ if (!atRoot) {
+ return getExplorePath();
+ }
+ return null;
+ }
+
+ const inboxMatch = matchPath({ path: INBOX_PATH, caseSensitive: true, end: false }, pathname);
+ if (inboxMatch) {
+ const atRoot = matchPath({ path: INBOX_PATH, caseSensitive: true, end: true }, pathname);
+ if (!atRoot) {
+ return getInboxPath();
+ }
+ return null;
+ }
+
+ return null;
+};
+
+export const showBackInPageHeader = (screenSize: ScreenSize): boolean =>
+ screenSize !== ScreenSize.Desktop;
+
+export const showBackInTitleBar = (screenSize: ScreenSize, canGoBack: boolean): boolean =>
+ screenSize === ScreenSize.Desktop && canGoBack;
+
+export const useBackRoute = (mxOverride?: MatrixClient | null) => {
+ const navigate = useNavigateWithTransition();
+ const location = useLocation();
+ const mxFromContext = useMatrixClientOptionally();
+ const mx = mxOverride ?? mxFromContext;
+ const screenSize = useScreenSizeContext();
+ const { spaceIdOrAlias: rawSpaceIdOrAlias } = useParams();
+ const spaceIdOrAlias = decodeRouteParam(rawSpaceIdOrAlias);
+ const selectedSpaceId =
+ mx && spaceIdOrAlias && isRoomAlias(spaceIdOrAlias)
+ ? getCanonicalAliasRoomId(mx, spaceIdOrAlias)
+ : spaceIdOrAlias;
+
+ const target = useMemo(() => {
+ if (!mx) return null;
+ return getBackRouteTarget({ location, mx, selectedSpaceId });
+ }, [location, mx, selectedSpaceId]);
+
+ const goBack = useCallback(() => {
+ if (target) {
+ navigate(target);
+ }
+ }, [navigate, target]);
+
+ return {
+ canGoBack: target !== null,
+ goBack,
+ showInPageHeader: showBackInPageHeader(screenSize) && target !== null,
+ showInTitleBar: showBackInTitleBar(screenSize, target !== null),
+ };
+};
diff --git a/src/app/hooks/useCompactNav.ts b/src/app/hooks/useCompactNav.ts
new file mode 100644
index 0000000..4e4de4a
--- /dev/null
+++ b/src/app/hooks/useCompactNav.ts
@@ -0,0 +1,26 @@
+import { useMatch } from 'react-router-dom';
+import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
+import { ScreenSize, useScreenSizeContext } from './useScreenSize';
+
+/** Skinny window layout (≤750px): master-detail instead of 3-column desktop. */
+export const useCompactNav = (): boolean => {
+ const screenSize = useScreenSizeContext();
+ return screenSize === ScreenSize.Mobile;
+};
+
+/** Routes where sidebar + channel list are shown together (no room/content pane). */
+export const useIsCompactListRoute = (): boolean => {
+ const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true });
+ const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: true });
+ const spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true });
+ const exploreMatch = useMatch({ path: EXPLORE_PATH, caseSensitive: true, end: true });
+ const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true });
+
+ return !!(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch);
+};
+
+export const useShowCompactMasterView = (): boolean => {
+ const compact = useCompactNav();
+ const isListRoute = useIsCompactListRoute();
+ return compact && isListRoute;
+};
diff --git a/src/app/hooks/useMatrixClient.ts b/src/app/hooks/useMatrixClient.ts
index 5ff6d90..ede4069 100644
--- a/src/app/hooks/useMatrixClient.ts
+++ b/src/app/hooks/useMatrixClient.ts
@@ -10,3 +10,7 @@ export function useMatrixClient(): MatrixClient {
if (!mx) throw new Error('MatrixClient not initialized!');
return mx;
}
+
+export function useMatrixClientOptionally(): MatrixClient | null {
+ return useContext(MatrixClientContext);
+}
diff --git a/src/app/pages/MobileFriendly.tsx b/src/app/pages/MobileFriendly.tsx
index ca947ac..5c4724f 100644
--- a/src/app/pages/MobileFriendly.tsx
+++ b/src/app/pages/MobileFriendly.tsx
@@ -1,23 +1,20 @@
import { ReactNode } from 'react';
import { useMatch } from 'react-router-dom';
-import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from './paths';
+import { useCompactNav } from '../hooks/useCompactNav';
type MobileFriendlyClientNavProps = {
children: ReactNode;
};
export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) {
- const screenSize = useScreenSizeContext();
+ const compact = useCompactNav();
const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true });
const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: true });
const spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true });
const exploreMatch = useMatch({ path: EXPLORE_PATH, caseSensitive: true, end: true });
const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true });
- if (
- screenSize === ScreenSize.Mobile &&
- !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch)
- ) {
+ if (compact && !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch)) {
return null;
}
@@ -29,14 +26,14 @@ type MobileFriendlyPageNavProps = {
children: ReactNode;
};
export function MobileFriendlyPageNav({ path, children }: MobileFriendlyPageNavProps) {
- const screenSize = useScreenSizeContext();
+ const compact = useCompactNav();
const exactPath = useMatch({
path,
caseSensitive: true,
end: true,
});
- if (screenSize === ScreenSize.Mobile && !exactPath) {
+ if (compact && !exactPath) {
return null;
}
diff --git a/src/app/pages/client/space/SpaceIndexRedirect.tsx b/src/app/pages/client/space/SpaceIndexRedirect.tsx
index bdf2069..9086bc3 100644
--- a/src/app/pages/client/space/SpaceIndexRedirect.tsx
+++ b/src/app/pages/client/space/SpaceIndexRedirect.tsx
@@ -5,17 +5,24 @@ import { useSpace } from '../../../hooks/useSpace';
import { shouldShowForumLobby } from '../../../utils/room';
import { getSpaceDefaultPath } from '../../pathUtils';
import { ForumFeedPage } from '../../../features/forum/ForumFeedPage';
+import { useCompactNav } from '../../../hooks/useCompactNav';
-/** Sends /:spaceId/ to the forum feed or space lobby. */
+/** Sends /:spaceId/ to the forum feed or space lobby (desktop). On compact nav, stay on index for the channel list. */
export function SpaceIndexRedirect() {
const mx = useMatrixClient();
const space = useSpace();
const navigate = useNavigate();
+ const compact = useCompactNav();
const isForum = shouldShowForumLobby(space);
useEffect(() => {
+ if (compact) return;
navigate(getSpaceDefaultPath(mx, space.roomId), { replace: true });
- }, [mx, navigate, space.roomId]);
+ }, [compact, mx, navigate, space.roomId]);
+
+ if (compact) {
+ return null;
+ }
// Avoid a blank main pane while redirecting forum spaces to /feed/.
if (isForum) {