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
896a2e2083
commit
0b31e6b930
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 { NavigateOptions, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||||
import { getDirectRoomPath, getSpacePath, getSpaceRoomPath } from '../pages/pathUtils';
|
import { getChannelsRoomPath, getDirectRoomPath, getSpacePath } from '../pages/pathUtils';
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
import { getOrphanParents, guessPerfectParent, isSpace } from '../utils/room';
|
import { getOrphanParents, guessPerfectParent, isSpace } from '../utils/room';
|
||||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||||
|
|
@ -61,9 +61,18 @@ export const useRoomNavigate = () => {
|
||||||
const navigateRoom = useCallback(
|
const navigateRoom = useCallback(
|
||||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
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) {
|
if (orphanParents.length > 0) {
|
||||||
let parentSpace: string;
|
let parentSpace: string;
|
||||||
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
|
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
|
||||||
|
|
@ -74,10 +83,7 @@ export const useRoomNavigate = () => {
|
||||||
|
|
||||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||||
|
|
||||||
safeNavigate(
|
safeNavigate(getChannelsRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||||
getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
|
|
||||||
opts
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { AuthLayout, Login, Register, ResetPassword } from './auth';
|
||||||
import {
|
import {
|
||||||
BOTS_PATH,
|
BOTS_PATH,
|
||||||
CHANNELS_PATH,
|
CHANNELS_PATH,
|
||||||
|
CHANNELS_ROOM_EVENT_PATH,
|
||||||
CHANNELS_ROOM_PATH,
|
CHANNELS_ROOM_PATH,
|
||||||
CHANNELS_SPACE_PATH,
|
CHANNELS_SPACE_PATH,
|
||||||
CHANNELS_THREAD_PATH,
|
CHANNELS_THREAD_PATH,
|
||||||
|
|
@ -323,6 +324,10 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||||
SpaceRouteRoomProvider lives on the parent route and
|
SpaceRouteRoomProvider lives on the parent route and
|
||||||
stays mounted across the room↔thread URL flip. */}
|
stays mounted across the room↔thread URL flip. */}
|
||||||
<Route path={CHANNELS_THREAD_PATH.slice(CHANNELS_ROOM_PATH.length)} element={null} />
|
<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>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
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 { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||||
import { isSpace } from '../../../utils/room';
|
import { getOrphanParents, guessPerfectParent, isSpace } from '../../../utils/room';
|
||||||
|
|
||||||
export function HomeRouteRoomProvider({ children: _children }: { children: ReactNode }) {
|
export function HomeRouteRoomProvider({ children: _children }: { children: ReactNode }) {
|
||||||
const mx = useMatrixClient();
|
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
|
// explicitly so the redirect re-evaluates after /sync. See plan §6.7 / §6.8
|
||||||
// / round-5 review notes.
|
// / round-5 review notes.
|
||||||
useAtomValue(allRoomsAtom);
|
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 { roomIdOrAlias, eventId } = useParams();
|
||||||
const viaServers = useSearchParamsViaServers();
|
const viaServers = useSearchParamsViaServers();
|
||||||
const roomId = useSelectedRoom();
|
const roomId = useSelectedRoom();
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
// After P3c the Home tab is gone — every non-space room redirects to the
|
// After P3c the Home tab is gone — /home/{roomId}/ stays as a deep-link
|
||||||
// unified Direct list. /home/{roomId}/ stays as a deep-link compatibility
|
// compatibility shim for cold-start push (sw.ts cannot read mDirectAtom or
|
||||||
// shim for cold-start push (sw.ts cannot read mDirectAtom).
|
// roomToParentsAtom). Room-in-space rooms route into /channels/, everything
|
||||||
|
// else falls through to the universal /direct/ list.
|
||||||
if (room && !isSpace(room)) {
|
if (room && !isSpace(room)) {
|
||||||
const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
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 />;
|
return <Navigate to={getDirectRoomPath(alias, eventId)} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
BOTS_BOT_PATH,
|
BOTS_BOT_PATH,
|
||||||
BOTS_PATH,
|
BOTS_PATH,
|
||||||
CHANNELS_PATH,
|
CHANNELS_PATH,
|
||||||
|
CHANNELS_ROOM_EVENT_PATH,
|
||||||
CHANNELS_ROOM_PATH,
|
CHANNELS_ROOM_PATH,
|
||||||
CHANNELS_SPACE_PATH,
|
CHANNELS_SPACE_PATH,
|
||||||
CHANNELS_THREAD_PATH,
|
CHANNELS_THREAD_PATH,
|
||||||
|
|
@ -171,13 +172,20 @@ export const getChannelsSpacePath = (spaceIdOrAlias: string): string => {
|
||||||
};
|
};
|
||||||
export const getChannelsRoomPath = (
|
export const getChannelsRoomPath = (
|
||||||
spaceIdOrAlias: string,
|
spaceIdOrAlias: string,
|
||||||
roomIdOrAlias: string
|
roomIdOrAlias: string,
|
||||||
|
eventId?: string
|
||||||
): 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),
|
spaceIdOrAlias: encodeURIComponent(spaceIdOrAlias),
|
||||||
roomIdOrAlias: encodeURIComponent(roomIdOrAlias),
|
roomIdOrAlias: encodeURIComponent(roomIdOrAlias),
|
||||||
};
|
});
|
||||||
return generatePath(CHANNELS_ROOM_PATH, params);
|
|
||||||
};
|
};
|
||||||
export const getChannelsThreadPath = (
|
export const getChannelsThreadPath = (
|
||||||
spaceIdOrAlias: string,
|
spaceIdOrAlias: string,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,12 @@ export const BOTS_BOT_PATH = '/bots/:botId/';
|
||||||
export const CHANNELS_PATH = '/channels/';
|
export const CHANNELS_PATH = '/channels/';
|
||||||
export const CHANNELS_SPACE_PATH = '/channels/:spaceIdOrAlias/';
|
export const CHANNELS_SPACE_PATH = '/channels/:spaceIdOrAlias/';
|
||||||
export const CHANNELS_ROOM_PATH = '/channels/:spaceIdOrAlias/:roomIdOrAlias/';
|
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 CHANNELS_THREAD_PATH = '/channels/:spaceIdOrAlias/:roomIdOrAlias/thread/:rootId/';
|
||||||
|
|
||||||
export const SPACE_SETTINGS_PATH = '/space-settings/';
|
export const SPACE_SETTINGS_PATH = '/space-settings/';
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { matchPath } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
BOTS_PATH,
|
BOTS_PATH,
|
||||||
CHANNELS_PATH,
|
CHANNELS_PATH,
|
||||||
|
CHANNELS_ROOM_EVENT_PATH,
|
||||||
CHANNELS_ROOM_PATH,
|
CHANNELS_ROOM_PATH,
|
||||||
CHANNELS_SPACE_PATH,
|
CHANNELS_SPACE_PATH,
|
||||||
CHANNELS_THREAD_PATH,
|
CHANNELS_THREAD_PATH,
|
||||||
|
|
@ -51,6 +52,19 @@ export const getRouteSectionParent = (pathname: string): string | null => {
|
||||||
decodeURIComponent(threadMatch.params.roomIdOrAlias)
|
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(
|
const roomMatch = matchPath(
|
||||||
{ path: CHANNELS_ROOM_PATH, caseSensitive: true, end: false },
|
{ path: CHANNELS_ROOM_PATH, caseSensitive: true, end: false },
|
||||||
pathname
|
pathname
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue