fix: rework compact nav and centralized back routing
Use master-detail layout on skinny windows so server and channel lists share one screen, with back returning to the space list instead of redirect loops. Add useBackRoute for title-bar and page-header back buttons with view transitions.
This commit is contained in:
@@ -1,90 +1,11 @@
|
|||||||
import { ReactNode, useCallback } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
import { useBackRoute } from '../hooks/useBackRoute';
|
||||||
import {
|
|
||||||
decodeRouteParam,
|
|
||||||
getDirectPath,
|
|
||||||
getExplorePath,
|
|
||||||
getHomePath,
|
|
||||||
getInboxPath,
|
|
||||||
getSpacePath,
|
|
||||||
} from '../pages/pathUtils';
|
|
||||||
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
|
|
||||||
|
|
||||||
type BackRouteHandlerProps = {
|
type BackRouteHandlerProps = {
|
||||||
children: (onBack: () => void) => ReactNode;
|
children: (onBack: () => void) => ReactNode;
|
||||||
};
|
};
|
||||||
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||||
const navigate = useNavigate();
|
const { goBack } = useBackRoute();
|
||||||
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]);
|
|
||||||
|
|
||||||
return children(goBack);
|
return children(goBack);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ContainerColor } from '../../styles/ContainerColor.css';
|
|||||||
import * as css from './style.css';
|
import * as css from './style.css';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
|
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
|
||||||
|
import { useShowCompactMasterView } from '../../hooks/useCompactNav';
|
||||||
|
|
||||||
type PageRootProps = {
|
type PageRootProps = {
|
||||||
nav: ReactNode;
|
nav: ReactNode;
|
||||||
@@ -13,6 +14,7 @@ type PageRootProps = {
|
|||||||
|
|
||||||
export function PageRoot({ nav, children }: PageRootProps) {
|
export function PageRoot({ nav, children }: PageRootProps) {
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
const showCompactMaster = useShowCompactMasterView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||||
@@ -20,7 +22,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
|||||||
{screenSize !== ScreenSize.Mobile && (
|
{screenSize !== ScreenSize.Mobile && (
|
||||||
<Line variant="Background" size="300" direction="Vertical" />
|
<Line variant="Background" size="300" direction="Vertical" />
|
||||||
)}
|
)}
|
||||||
{children}
|
{!showCompactMaster && children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export const TitleBarDragRegion = style({
|
|||||||
color: color.Surface.OnContainer,
|
color: color.Surface.OnContainer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TitleBarBackButton = style({
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
export const SyncStatusBadge = style({
|
export const SyncStatusBadge = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { ReactNode, useMemo, useEffect, useState, useCallback, useRef } from 'react';
|
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 { invoke } from '@tauri-apps/api/core';
|
||||||
import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules, SyncState } from 'matrix-js-sdk';
|
import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules, SyncState } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
@@ -18,6 +18,8 @@ import { AccountDataEvent } from '../../../types/matrix/accountData';
|
|||||||
import { getNotificationMode, NotificationMode } from '../../hooks/useNotificationMode';
|
import { getNotificationMode, NotificationMode } from '../../hooks/useNotificationMode';
|
||||||
import { isRoomId } from '../../utils/matrix';
|
import { isRoomId } from '../../utils/matrix';
|
||||||
import { useSyncState } from '../../hooks/useSyncState';
|
import { useSyncState } from '../../hooks/useSyncState';
|
||||||
|
import { useBackRoute } from '../../hooks/useBackRoute';
|
||||||
|
import { Icon, Icons } from '../icons';
|
||||||
|
|
||||||
type NewMessageNotification = {
|
type NewMessageNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -42,6 +44,7 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
const { showInTitleBar, goBack } = useBackRoute(mx);
|
||||||
|
|
||||||
// Sync status state
|
// Sync status state
|
||||||
const [syncStateData, setSyncStateData] = useState<{
|
const [syncStateData, setSyncStateData] = useState<{
|
||||||
@@ -652,6 +655,20 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
|||||||
grow="Yes"
|
grow="Yes"
|
||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
>
|
>
|
||||||
|
{showInTitleBar && (
|
||||||
|
<IconButton
|
||||||
|
className={css.TitleBarBackButton}
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
fill="None"
|
||||||
|
radii="300"
|
||||||
|
onClick={goBack}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
aria-label="Back"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.ArrowLeft} size="200" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<Text size="T200" truncate style={{ color: 'inherit' }}>
|
<Text size="T200" truncate style={{ color: 'inherit' }}>
|
||||||
<span style={{ fontWeight: 700 }}>Paarrot</span>{titleParts.suffix}
|
<span style={{ fontWeight: 700 }}>Paarrot</span>{titleParts.suffix}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
|
|||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
import { useBackRoute } from '../../hooks/useBackRoute';
|
||||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||||
@@ -165,6 +165,7 @@ export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: Lobb
|
|||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||||
const [addExisting, setAddExisting] = useState<OpenAddExistingOptions | null>(null);
|
const [addExisting, setAddExisting] = useState<OpenAddExistingOptions | null>(null);
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
const { showInPageHeader, goBack } = useBackRoute();
|
||||||
|
|
||||||
const name = useRoomName(space);
|
const name = useRoomName(space);
|
||||||
const avatarMxc = useRoomAvatar(space);
|
const avatarMxc = useRoomAvatar(space);
|
||||||
@@ -270,6 +271,13 @@ export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: Lobb
|
|||||||
<>
|
<>
|
||||||
<PageHeader outlined>
|
<PageHeader outlined>
|
||||||
<Box grow="Yes" alignItems="Center" gap="300">
|
<Box grow="Yes" alignItems="Center" gap="300">
|
||||||
|
{showInPageHeader && (
|
||||||
|
<Box shrink="No">
|
||||||
|
<IconButton onClick={goBack} aria-label="Back">
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Avatar size="300" shrink="No">
|
<Avatar size="300" shrink="No">
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
roomId={space.roomId}
|
roomId={space.roomId}
|
||||||
@@ -361,16 +369,12 @@ export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: Lobb
|
|||||||
return (
|
return (
|
||||||
<PageHeader className={showProfile ? undefined : css.Header} balance>
|
<PageHeader className={showProfile ? undefined : css.Header} balance>
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
{screenSize === ScreenSize.Mobile ? (
|
{showInPageHeader ? (
|
||||||
<>
|
<>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<BackRouteHandler>
|
<IconButton onClick={goBack} aria-label="Back">
|
||||||
{(onBack) => (
|
<Icon src={Icons.ArrowLeft} />
|
||||||
<IconButton onClick={onBack}>
|
</IconButton>
|
||||||
<Icon src={Icons.ArrowLeft} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</BackRouteHandler>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" justifyContent="Center">
|
<Box grow="Yes" justifyContent="Center">
|
||||||
{showProfile && (
|
{showProfile && (
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { stopPropagation } from '../../utils/keyboard';
|
|||||||
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||||
import { getViaServers } from '../../plugins/via-servers';
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
import { PluginButtonSlot } from '../settings/plugins/PluginButtonSlot';
|
import { PluginButtonSlot } from '../settings/plugins/PluginButtonSlot';
|
||||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
import { useBackRoute } from '../../hooks/useBackRoute';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents';
|
import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents';
|
||||||
import { RoomPinMenu } from './room-pin-menu';
|
import { RoomPinMenu } from './room-pin-menu';
|
||||||
@@ -463,6 +463,7 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
|
const { showInPageHeader, goBack } = useBackRoute();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const space = useSpaceOptionally();
|
const space = useSpaceOptionally();
|
||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||||
@@ -501,21 +502,17 @@ export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader balance={screenSize === ScreenSize.Mobile}>
|
<PageHeader balance={showInPageHeader}>
|
||||||
<Box grow="Yes" gap="300">
|
<Box grow="Yes" gap="300">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{showInPageHeader && (
|
||||||
<BackRouteHandler>
|
<Box shrink="No" alignItems="Center">
|
||||||
{(onBack) => (
|
<IconButton onClick={goBack} aria-label="Back">
|
||||||
<Box shrink="No" alignItems="Center">
|
<Icon src={Icons.ArrowLeft} />
|
||||||
<IconButton onClick={onBack}>
|
</IconButton>
|
||||||
<Icon src={Icons.ArrowLeft} />
|
</Box>
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</BackRouteHandler>
|
|
||||||
)}
|
)}
|
||||||
<Box grow="Yes" alignItems="Center" gap="300">
|
<Box grow="Yes" alignItems="Center" gap="300">
|
||||||
{screenSize !== ScreenSize.Mobile && (
|
{!showInPageHeader && (
|
||||||
<Avatar size="300">
|
<Avatar size="300">
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
|
|||||||
151
src/app/hooks/useBackRoute.ts
Normal file
151
src/app/hooks/useBackRoute.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { Location, matchPath, useLocation, useParams } from 'react-router-dom';
|
||||||
|
import type { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import {
|
||||||
|
decodeRouteParam,
|
||||||
|
getDirectPath,
|
||||||
|
getExplorePath,
|
||||||
|
getHomePath,
|
||||||
|
getInboxPath,
|
||||||
|
getSpacePath,
|
||||||
|
} from '../pages/pathUtils';
|
||||||
|
import {
|
||||||
|
DIRECT_PATH,
|
||||||
|
EXPLORE_PATH,
|
||||||
|
HOME_PATH,
|
||||||
|
INBOX_PATH,
|
||||||
|
SPACE_FEED_PATH,
|
||||||
|
SPACE_LOBBY_PATH,
|
||||||
|
SPACE_PATH,
|
||||||
|
SPACE_ROOM_PATH,
|
||||||
|
SPACE_SEARCH_PATH,
|
||||||
|
} from '../pages/paths';
|
||||||
|
import { ScreenSize } from './useScreenSize';
|
||||||
|
import { useNavigateWithTransition } from './useViewTransitions';
|
||||||
|
import { useMatrixClientOptionally } from './useMatrixClient';
|
||||||
|
import { useScreenSizeContext } from './useScreenSize';
|
||||||
|
import { getCanonicalAliasOrRoomId, getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
|
||||||
|
|
||||||
|
type BackRouteContext = {
|
||||||
|
location: Location;
|
||||||
|
mx: MatrixClient;
|
||||||
|
selectedSpaceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const spaceListPath = (mx: MatrixClient, spaceId: string, spaceIdOrAlias: string): string =>
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
};
|
||||||
26
src/app/hooks/useCompactNav.ts
Normal file
26
src/app/hooks/useCompactNav.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -10,3 +10,7 @@ export function useMatrixClient(): MatrixClient {
|
|||||||
if (!mx) throw new Error('MatrixClient not initialized!');
|
if (!mx) throw new Error('MatrixClient not initialized!');
|
||||||
return mx;
|
return mx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMatrixClientOptionally(): MatrixClient | null {
|
||||||
|
return useContext(MatrixClientContext);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useMatch } from 'react-router-dom';
|
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 { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from './paths';
|
||||||
|
import { useCompactNav } from '../hooks/useCompactNav';
|
||||||
|
|
||||||
type MobileFriendlyClientNavProps = {
|
type MobileFriendlyClientNavProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) {
|
export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) {
|
||||||
const screenSize = useScreenSizeContext();
|
const compact = useCompactNav();
|
||||||
const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true });
|
const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true });
|
||||||
const directMatch = useMatch({ path: DIRECT_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 spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true });
|
||||||
const exploreMatch = useMatch({ path: EXPLORE_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 });
|
const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true });
|
||||||
|
|
||||||
if (
|
if (compact && !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch)) {
|
||||||
screenSize === ScreenSize.Mobile &&
|
|
||||||
!(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch)
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,14 +26,14 @@ type MobileFriendlyPageNavProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
export function MobileFriendlyPageNav({ path, children }: MobileFriendlyPageNavProps) {
|
export function MobileFriendlyPageNav({ path, children }: MobileFriendlyPageNavProps) {
|
||||||
const screenSize = useScreenSizeContext();
|
const compact = useCompactNav();
|
||||||
const exactPath = useMatch({
|
const exactPath = useMatch({
|
||||||
path,
|
path,
|
||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: true,
|
end: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (screenSize === ScreenSize.Mobile && !exactPath) {
|
if (compact && !exactPath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ import { useSpace } from '../../../hooks/useSpace';
|
|||||||
import { shouldShowForumLobby } from '../../../utils/room';
|
import { shouldShowForumLobby } from '../../../utils/room';
|
||||||
import { getSpaceDefaultPath } from '../../pathUtils';
|
import { getSpaceDefaultPath } from '../../pathUtils';
|
||||||
import { ForumFeedPage } from '../../../features/forum/ForumFeedPage';
|
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() {
|
export function SpaceIndexRedirect() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const space = useSpace();
|
const space = useSpace();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const compact = useCompactNav();
|
||||||
const isForum = shouldShowForumLobby(space);
|
const isForum = shouldShowForumLobby(space);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (compact) return;
|
||||||
navigate(getSpaceDefaultPath(mx, space.roomId), { replace: true });
|
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/.
|
// Avoid a blank main pane while redirecting forum spaces to /feed/.
|
||||||
if (isForum) {
|
if (isForum) {
|
||||||
|
|||||||
Reference in New Issue
Block a user