Skip to content

Commit 4ca0e14

Browse files
committed
fix(forum): stop the forum route being read as a timeline route
1 parent 64becf0 commit 4ca0e14

7 files changed

Lines changed: 122 additions & 78 deletions

File tree

src/app/components/page/MobileNavDrawer.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,9 @@ import { useAtomValue, useSetAtom } from 'jotai';
1212
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
1313
import { lastVisitedRoomAtom } from '$state/room/lastRoom';
1414
import { usePrefersReducedMotion } from '$hooks/usePrefersReducedMotion';
15-
import {
16-
DIRECT_PATH,
17-
DIRECT_ROOM_PATH,
18-
DIRECT_ROOM_FORUM_PATH,
19-
EXPLORE_PATH,
20-
HOME_PATH,
21-
HOME_ROOM_PATH,
22-
HOME_ROOM_FORUM_PATH,
23-
INBOX_PATH,
24-
SPACE_PATH,
25-
SPACE_ROOM_PATH,
26-
SPACE_ROOM_FORUM_PATH,
27-
} from '$pages/paths';
15+
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '$pages/paths';
2816
import { resolveSection } from '$pages/pathUtils';
29-
import { isRoomAlias, isRoomId } from '$utils/matrix';
17+
import { matchRoomRoute } from '$pages/roomRouteMatch';
3018
import { PersistentRoomHost } from './PersistentRoomHost';
3119
import { MobileNavDrawerContext, type MobileSwipeTarget } from './MobileNavDrawerContext';
3220
import {
@@ -69,19 +57,9 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra
6957
openableSection && openableSection.getRoomPath && lastRoom?.[openableSection.key]
7058
);
7159

72-
const roomMatch =
73-
matchPath({ path: HOME_ROOM_FORUM_PATH, end: false }, location.pathname) ??
74-
matchPath({ path: DIRECT_ROOM_FORUM_PATH, end: false }, location.pathname) ??
75-
matchPath({ path: SPACE_ROOM_FORUM_PATH, end: false }, location.pathname) ??
76-
matchPath({ path: HOME_ROOM_PATH, end: false }, location.pathname) ??
77-
matchPath({ path: DIRECT_ROOM_PATH, end: false }, location.pathname) ??
78-
matchPath({ path: SPACE_ROOM_PATH, end: false }, location.pathname);
79-
const matchedRoomId = roomMatch?.params.roomIdOrAlias
80-
? decodeURIComponent(roomMatch.params.roomIdOrAlias)
81-
: undefined;
82-
// `:roomIdOrAlias` also matches non-room segments like `create`, `search`, and `lobby`.
83-
// Only treat it as a room when it is a real Matrix ID or alias.
84-
const isRoomRoute = !!matchedRoomId && (isRoomId(matchedRoomId) || isRoomAlias(matchedRoomId));
60+
const roomRoute = matchRoomRoute(location.pathname);
61+
const matchedRoomId = roomRoute?.roomIdOrAlias;
62+
const isRoomRoute = roomRoute !== undefined;
8563

8664
const listView =
8765
matchPath({ path: HOME_PATH, end: true }, location.pathname) !== null ||

src/app/components/page/PersistentRoomHost.tsx

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactNode } from 'react';
2-
import { matchPath, useLocation } from 'react-router-dom';
2+
import { useLocation } from 'react-router-dom';
33
import { useAtomValue } from 'jotai';
44
import { Room } from '$features/room';
55
import { IsInactivePanelProvider } from '$hooks/useRoom';
@@ -8,50 +8,16 @@ import { DirectRouteRoomProvider } from '$pages/client/direct';
88
import { SpaceRouteRoomProvider } from '$pages/client/space';
99
import { lastVisitedRoomAtom } from '$state/room/lastRoom';
1010
import { resolveSection, type SectionNav } from '$pages/pathUtils';
11-
import {
12-
DIRECT_ROOM_PATH,
13-
DIRECT_ROOM_FORUM_PATH,
14-
HOME_ROOM_PATH,
15-
HOME_ROOM_FORUM_PATH,
16-
SPACE_ROOM_PATH,
17-
SPACE_ROOM_FORUM_PATH,
18-
} from '$pages/paths';
19-
import { isRoomAlias, isRoomId } from '$utils/matrix';
11+
import { matchRoomRoute, type RoomRouteMatch } from '$pages/roomRouteMatch';
2012

21-
type DisplayedRoom = {
22-
roomIdOrAlias: string;
23-
eventId?: string;
24-
};
25-
26-
function useDisplayedRoom(section: SectionNav | null): DisplayedRoom | undefined {
13+
function useDisplayedRoom(section: SectionNav | null): RoomRouteMatch | undefined {
2714
const location = useLocation();
2815
const lastRoom = useAtomValue(lastVisitedRoomAtom);
2916

3017
if (!section || !section.getRoomPath) return undefined;
3118

32-
const roomMatch =
33-
matchPath({ path: HOME_ROOM_FORUM_PATH, end: false }, location.pathname) ??
34-
matchPath({ path: DIRECT_ROOM_FORUM_PATH, end: false }, location.pathname) ??
35-
matchPath({ path: SPACE_ROOM_FORUM_PATH, end: false }, location.pathname) ??
36-
matchPath({ path: HOME_ROOM_PATH, end: false }, location.pathname) ??
37-
matchPath({ path: DIRECT_ROOM_PATH, end: false }, location.pathname) ??
38-
matchPath({ path: SPACE_ROOM_PATH, end: false }, location.pathname);
39-
40-
if (roomMatch) {
41-
const encodedId = roomMatch.params.roomIdOrAlias;
42-
const encodedEvent = roomMatch.params.eventId;
43-
if (encodedId) {
44-
const decodedId = decodeURIComponent(encodedId);
45-
// `:roomIdOrAlias` also matches non-room segments like `create`, `search`, `lobby`.
46-
// Only treat it as a room when it's a real Matrix id/alias.
47-
if (isRoomId(decodedId) || isRoomAlias(decodedId)) {
48-
return {
49-
roomIdOrAlias: decodedId,
50-
eventId: encodedEvent ? decodeURIComponent(encodedEvent) : undefined,
51-
};
52-
}
53-
}
54-
}
19+
const roomRoute = matchRoomRoute(location.pathname);
20+
if (roomRoute) return roomRoute;
5521

5622
const lastRoomId = lastRoom?.[section.key];
5723
if (lastRoomId) {

src/app/hooks/useRoomNavigate.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
getDirectRoomPath,
99
getHomeForumPath,
1010
getHomeRoomPath,
11-
getSpacePath,
1211
getSpaceForumPath,
12+
getSpacePath,
1313
getSpaceRoomPath,
1414
resolveSection,
1515
} from '$pages/pathUtils';
16+
import { CustomRoomType } from '$types/matrix/room';
1617
import { getOrphanParents, guessPerfectParent } from '$utils/room/hierarchy';
1718
import { roomToParentsAtom } from '$state/room/roomToParents';
1819
import { mDirectAtom } from '$state/mDirectList';
@@ -21,7 +22,6 @@ import { settingsAtom } from '$state/settings';
2122
import { useSetting } from '$state/hooks/settings';
2223
import { useSelectedSpace } from './router/useSelectedSpace';
2324
import { useMatrixClient } from './useMatrixClient';
24-
import { CustomRoomType } from '$types/matrix/room';
2525

2626
export const useRoomNavigate = () => {
2727
const navigate = useNavigate();
@@ -44,9 +44,10 @@ export const useRoomNavigate = () => {
4444
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
4545
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
4646
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
47-
const isForum = mx.getRoom(roomId)?.getType() === CustomRoomType.Forum;
4847

4948
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
49+
// The forum view has no jump target, so an event jump still uses the room path.
50+
const forum = !eventId && mx.getRoom(roomId)?.getType() === CustomRoomType.Forum;
5051
let destPath: string;
5152
let roomPart: string;
5253
if (orphanParents.length > 0) {
@@ -59,19 +60,15 @@ export const useRoomNavigate = () => {
5960

6061
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
6162
roomPart = openSpaceTimeline ? roomId : roomIdOrAlias;
62-
destPath = isForum
63-
? getSpaceForumPath(pSpaceIdOrAlias, roomPart, eventId)
63+
destPath = forum
64+
? getSpaceForumPath(pSpaceIdOrAlias, roomPart)
6465
: getSpaceRoomPath(pSpaceIdOrAlias, roomPart, eventId);
6566
} else if (mDirects.has(roomId)) {
6667
roomPart = roomIdOrAlias;
67-
destPath = isForum
68-
? getDirectForumPath(roomPart, eventId)
69-
: getDirectRoomPath(roomPart, eventId);
68+
destPath = forum ? getDirectForumPath(roomPart) : getDirectRoomPath(roomPart, eventId);
7069
} else {
7170
roomPart = roomIdOrAlias;
72-
destPath = isForum
73-
? getHomeForumPath(roomPart, eventId)
74-
: getHomeRoomPath(roomPart, eventId);
71+
destPath = forum ? getHomeForumPath(roomPart) : getHomeRoomPath(roomPart, eventId);
7572
}
7673

7774
const section = resolveSection(destPath);

src/app/pages/client/home/Home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getHomeSearchPath,
2626
withSearchParam,
2727
} from '$pages/pathUtils';
28+
import { CustomRoomType } from '$types/matrix/room';
2829
import { useOpenShallowRoute } from '$pages/client/useShallowRoute';
2930
import { getCanonicalAliasOrRoomId } from '$utils/matrix';
3031
import { useSelectedOrLastRoom } from '$hooks/router/useSelectedRoom';
@@ -65,7 +66,6 @@ import { useClientConfig } from '$hooks/useClientConfig';
6566
import { getMxIdServer } from '$utils/mxIdHelper';
6667
import { NavMenu } from '$components/nav/NavMenu';
6768
import { useMenuAnchor } from '$hooks/useMenuAnchor';
68-
import { CustomRoomType } from '$types/matrix/room';
6969

7070
type HomeMenuProps = {
7171
requestClose: () => void;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { matchRoomRoute } from './roomRouteMatch';
3+
4+
vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => false, invoke: () => undefined }));
5+
6+
const roomId = '!room:example.com';
7+
const encodedRoomId = encodeURIComponent(roomId);
8+
const eventId = '$event-id';
9+
10+
describe('matchRoomRoute', () => {
11+
it('matches a bare room route in every section', () => {
12+
expect(matchRoomRoute(`/home/${encodedRoomId}/`)).toEqual({
13+
roomIdOrAlias: roomId,
14+
eventId: undefined,
15+
});
16+
expect(matchRoomRoute(`/direct/${encodedRoomId}/`)).toEqual({
17+
roomIdOrAlias: roomId,
18+
eventId: undefined,
19+
});
20+
expect(matchRoomRoute(`/!space:example.com/${encodedRoomId}/`)).toEqual({
21+
roomIdOrAlias: roomId,
22+
eventId: undefined,
23+
});
24+
});
25+
26+
it('matches a room route with an event id', () => {
27+
expect(matchRoomRoute(`/home/${encodedRoomId}/${encodeURIComponent(eventId)}/`)).toEqual({
28+
roomIdOrAlias: roomId,
29+
eventId,
30+
});
31+
});
32+
33+
it('rejects forum routes', () => {
34+
expect(matchRoomRoute(`/home/${encodedRoomId}/forum/`)).toBeUndefined();
35+
expect(matchRoomRoute(`/direct/${encodedRoomId}/forum/`)).toBeUndefined();
36+
expect(matchRoomRoute(`/!space:example.com/${encodedRoomId}/forum/`)).toBeUndefined();
37+
});
38+
39+
it('drops a second segment that is not an event id', () => {
40+
expect(matchRoomRoute(`/home/${encodedRoomId}/anything/`)).toEqual({
41+
roomIdOrAlias: roomId,
42+
eventId: undefined,
43+
});
44+
});
45+
46+
it('rejects non-room first segments', () => {
47+
expect(matchRoomRoute('/home/create/')).toBeUndefined();
48+
expect(matchRoomRoute('/!space:example.com/lobby/')).toBeUndefined();
49+
});
50+
});

src/app/pages/roomRouteMatch.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { matchPath } from 'react-router-dom';
2+
import { isEventId, isRoomAlias, isRoomId } from '$utils/matrix';
3+
import {
4+
DIRECT_ROOM_FORUM_PATH,
5+
DIRECT_ROOM_PATH,
6+
HOME_ROOM_FORUM_PATH,
7+
HOME_ROOM_PATH,
8+
SPACE_ROOM_FORUM_PATH,
9+
SPACE_ROOM_PATH,
10+
} from './paths';
11+
12+
export type RoomRouteMatch = {
13+
roomIdOrAlias: string;
14+
eventId?: string;
15+
};
16+
17+
const isForumRoute = (pathname: string): boolean =>
18+
[HOME_ROOM_FORUM_PATH, DIRECT_ROOM_FORUM_PATH, SPACE_ROOM_FORUM_PATH].some(
19+
(path) => matchPath({ path, end: false }, pathname) !== null
20+
);
21+
22+
/**
23+
* Matches the timeline room routes and returns their decoded params.
24+
*
25+
* `:roomIdOrAlias/:eventId?` also matches sibling routes like `:roomIdOrAlias/forum`
26+
* and non-room segments like `create` or `search`. The router ranks the literal
27+
* routes higher, but a bare `matchPath` does not — so forum routes are rejected and
28+
* both params are validated as real Matrix ids.
29+
*/
30+
export const matchRoomRoute = (pathname: string): RoomRouteMatch | undefined => {
31+
if (isForumRoute(pathname)) return undefined;
32+
33+
const match =
34+
matchPath({ path: HOME_ROOM_PATH, end: false }, pathname) ??
35+
matchPath({ path: DIRECT_ROOM_PATH, end: false }, pathname) ??
36+
matchPath({ path: SPACE_ROOM_PATH, end: false }, pathname);
37+
if (!match) return undefined;
38+
39+
const encodedId = match.params.roomIdOrAlias;
40+
if (!encodedId) return undefined;
41+
const roomIdOrAlias = decodeURIComponent(encodedId);
42+
if (!isRoomId(roomIdOrAlias) && !isRoomAlias(roomIdOrAlias)) return undefined;
43+
44+
const encodedEvent = match.params.eventId;
45+
const eventId = encodedEvent ? decodeURIComponent(encodedEvent) : undefined;
46+
47+
return {
48+
roomIdOrAlias,
49+
eventId: eventId && isEventId(eventId) ? eventId : undefined,
50+
};
51+
};

src/app/utils/matrix.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export const isRoomId = (id: string): boolean => id.startsWith('!');
4646

4747
export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#');
4848

49+
export const isEventId = (id: string): boolean => id.startsWith('$');
50+
4951
export const getCanonicalAliasRoomId = (mx: MatrixClient, alias: string): string | undefined =>
5052
mx
5153
.getRooms()

0 commit comments

Comments
 (0)