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:
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!');
|
||||
return mx;
|
||||
}
|
||||
|
||||
export function useMatrixClientOptionally(): MatrixClient | null {
|
||||
return useContext(MatrixClientContext);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user