feat(channels): route room-in-space navigations through /channels/ with eventId-anchored permalinks and channels-aware cold-push redirect
This commit is contained in:
parent
dd55900dd6
commit
19f2d64c0d
6 changed files with 71 additions and 16 deletions
|
|
@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react';
|
|||
import { NavigateOptions, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||
import { getDirectRoomPath, getSpacePath, getSpaceRoomPath } from '../pages/pathUtils';
|
||||
import { getChannelsRoomPath, getDirectRoomPath, getSpacePath } from '../pages/pathUtils';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getOrphanParents, guessPerfectParent, isSpace } from '../utils/room';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
|
|
@ -61,9 +61,18 @@ export const useRoomNavigate = () => {
|
|||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
|
||||
|
||||
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
|
||||
// Developer-tools edge case: clicking the rail-selected space row
|
||||
// opens the space's own timeline as a pseudo-room. Route through
|
||||
// /channels/<space>/<space>/ so the dev view shares the channels
|
||||
// SpaceRouteRoomProvider — its `developerTools && isSpaceRoom &&
|
||||
// room.roomId === space.roomId` branch already permits this.
|
||||
if (developerTools && spaceSelectedId === roomId) {
|
||||
safeNavigate(getChannelsRoomPath(roomIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
let parentSpace: string;
|
||||
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
|
||||
|
|
@ -74,10 +83,7 @@ export const useRoomNavigate = () => {
|
|||
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||
|
||||
safeNavigate(
|
||||
getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
|
||||
opts
|
||||
);
|
||||
safeNavigate(getChannelsRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { AuthLayout, Login, Register, ResetPassword } from './auth';
|
|||
import {
|
||||
BOTS_PATH,
|
||||
CHANNELS_PATH,
|
||||
CHANNELS_ROOM_EVENT_PATH,
|
||||
CHANNELS_ROOM_PATH,
|
||||
CHANNELS_SPACE_PATH,
|
||||
CHANNELS_THREAD_PATH,
|
||||
|
|
@ -323,6 +324,10 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
SpaceRouteRoomProvider lives on the parent route and
|
||||
stays mounted across the room↔thread URL flip. */}
|
||||
<Route path={CHANNELS_THREAD_PATH.slice(CHANNELS_ROOM_PATH.length)} element={null} />
|
||||
{/* Event-anchored URL — search/inbox/mention/push permalinks.
|
||||
RR6 merges child params into the parent's useParams so
|
||||
Room.tsx reads `eventId` without re-routing. */}
|
||||
<Route path={CHANNELS_ROOM_EVENT_PATH.slice(CHANNELS_ROOM_PATH.length)} element={null} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { getDirectRoomPath } from '../../pathUtils';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { getChannelsRoomPath, getDirectRoomPath } from '../../pathUtils';
|
||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||
import { isSpace } from '../../../utils/room';
|
||||
import { getOrphanParents, guessPerfectParent, isSpace } from '../../../utils/room';
|
||||
|
||||
export function HomeRouteRoomProvider({ children: _children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
|
|
@ -23,17 +24,32 @@ export function HomeRouteRoomProvider({ children: _children }: { children: React
|
|||
// explicitly so the redirect re-evaluates after /sync. See plan §6.7 / §6.8
|
||||
// / round-5 review notes.
|
||||
useAtomValue(allRoomsAtom);
|
||||
// Subscribe roomToParentsAtom so the channels-vs-direct branch re-evaluates
|
||||
// after /sync populates the m.space.parent / m.space.child mappings. Without
|
||||
// this subscription a cold push for a room-in-space would render once with
|
||||
// an empty parents map and miss the channels redirect.
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
const viaServers = useSearchParamsViaServers();
|
||||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
// After P3c the Home tab is gone — every non-space room redirects to the
|
||||
// unified Direct list. /home/{roomId}/ stays as a deep-link compatibility
|
||||
// shim for cold-start push (sw.ts cannot read mDirectAtom).
|
||||
// After P3c the Home tab is gone — /home/{roomId}/ stays as a deep-link
|
||||
// compatibility shim for cold-start push (sw.ts cannot read mDirectAtom or
|
||||
// roomToParentsAtom). Room-in-space rooms route into /channels/, everything
|
||||
// else falls through to the universal /direct/ list.
|
||||
if (room && !isSpace(room)) {
|
||||
const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
const orphanParents = getOrphanParents(roomToParents, room.roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const parentSpace =
|
||||
guessPerfectParent(mx, room.roomId, orphanParents) ?? orphanParents[0];
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||
return (
|
||||
<Navigate to={getChannelsRoomPath(pSpaceIdOrAlias, alias, eventId)} replace />
|
||||
);
|
||||
}
|
||||
return <Navigate to={getDirectRoomPath(alias, eventId)} replace />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
BOTS_BOT_PATH,
|
||||
BOTS_PATH,
|
||||
CHANNELS_PATH,
|
||||
CHANNELS_ROOM_EVENT_PATH,
|
||||
CHANNELS_ROOM_PATH,
|
||||
CHANNELS_SPACE_PATH,
|
||||
CHANNELS_THREAD_PATH,
|
||||
|
|
@ -171,13 +172,20 @@ export const getChannelsSpacePath = (spaceIdOrAlias: string): string => {
|
|||
};
|
||||
export const getChannelsRoomPath = (
|
||||
spaceIdOrAlias: string,
|
||||
roomIdOrAlias: string
|
||||
roomIdOrAlias: string,
|
||||
eventId?: string
|
||||
): string => {
|
||||
const params = {
|
||||
if (eventId) {
|
||||
return generatePath(CHANNELS_ROOM_EVENT_PATH, {
|
||||
spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias),
|
||||
roomIdOrAlias: encodeURIComponent(roomIdOrAlias),
|
||||
eventId: encodeURIComponent(eventId),
|
||||
});
|
||||
}
|
||||
return generatePath(CHANNELS_ROOM_PATH, {
|
||||
spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias),
|
||||
roomIdOrAlias: encodeURIComponent(roomIdOrAlias),
|
||||
};
|
||||
return generatePath(CHANNELS_ROOM_PATH, params);
|
||||
});
|
||||
};
|
||||
export const getChannelsThreadPath = (
|
||||
spaceIdOrAlias: string,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ export const BOTS_BOT_PATH = '/bots/:botId/';
|
|||
export const CHANNELS_PATH = '/channels/';
|
||||
export const CHANNELS_SPACE_PATH = '/channels/:spaceIdOrAlias/';
|
||||
export const CHANNELS_ROOM_PATH = '/channels/:spaceIdOrAlias/:roomIdOrAlias/';
|
||||
// Event-anchored channel room URL — discriminated `event/` segment so the
|
||||
// optional eventId does not collide with the sibling `thread/:rootId/`
|
||||
// sub-route. Search/inbox/mention/push permalinks land here so the timeline
|
||||
// can scroll to the cited event without dropping the user out of /channels/.
|
||||
export const CHANNELS_ROOM_EVENT_PATH =
|
||||
'/channels/:spaceIdOrAlias/:roomIdOrAlias/event/:eventId/';
|
||||
export const CHANNELS_THREAD_PATH = '/channels/:spaceIdOrAlias/:roomIdOrAlias/thread/:rootId/';
|
||||
|
||||
export const SPACE_SETTINGS_PATH = '/space-settings/';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { matchPath } from 'react-router-dom';
|
|||
import {
|
||||
BOTS_PATH,
|
||||
CHANNELS_PATH,
|
||||
CHANNELS_ROOM_EVENT_PATH,
|
||||
CHANNELS_ROOM_PATH,
|
||||
CHANNELS_SPACE_PATH,
|
||||
CHANNELS_THREAD_PATH,
|
||||
|
|
@ -51,6 +52,19 @@ export const getRouteSectionParent = (pathname: string): string | null => {
|
|||
decodeURIComponent(threadMatch.params.roomIdOrAlias)
|
||||
);
|
||||
}
|
||||
// Event-anchored permalink collapses to the same parent room view as
|
||||
// thread does — so back from a search/notification permalink lands on
|
||||
// the room timeline first, not on the space root.
|
||||
const eventMatch = matchPath(
|
||||
{ path: CHANNELS_ROOM_EVENT_PATH, caseSensitive: true, end: false },
|
||||
pathname
|
||||
);
|
||||
if (eventMatch?.params.spaceIdOrAlias && eventMatch?.params.roomIdOrAlias) {
|
||||
return getChannelsRoomPath(
|
||||
decodeURIComponent(eventMatch.params.spaceIdOrAlias),
|
||||
decodeURIComponent(eventMatch.params.roomIdOrAlias)
|
||||
);
|
||||
}
|
||||
const roomMatch = matchPath(
|
||||
{ path: CHANNELS_ROOM_PATH, caseSensitive: true, end: false },
|
||||
pathname
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue