From ded306dcecd1ede0edae4b67ae861f03b1946ac3 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Tue, 18 Aug 2026 18:12:32 +0200 Subject: [PATCH] fix(forum): keep the event timeline open for forum rooms --- .changeset/fix-forum-event-timeline.md | 5 ++ .../page/PersistentRoomHost.test.tsx | 8 +++ src/app/features/forum/ForumHeader.tsx | 1 + src/app/features/forum/ForumMenu.tsx | 16 +++-- src/app/pages/client/RoomRoute.tsx | 8 ++- src/app/pages/paths.ts | 3 + tests/e2e/forum-event-timeline.spec.ts | 69 +++++++++++++++++++ 7 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-forum-event-timeline.md create mode 100644 tests/e2e/forum-event-timeline.spec.ts diff --git a/.changeset/fix-forum-event-timeline.md b/.changeset/fix-forum-event-timeline.md new file mode 100644 index 0000000000..454f953969 --- /dev/null +++ b/.changeset/fix-forum-event-timeline.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix the forum room "Event Timeline" developer entry bouncing straight back to the forum view. diff --git a/src/app/components/page/PersistentRoomHost.test.tsx b/src/app/components/page/PersistentRoomHost.test.tsx index cddc8e072c..b199eb73a1 100644 --- a/src/app/components/page/PersistentRoomHost.test.tsx +++ b/src/app/components/page/PersistentRoomHost.test.tsx @@ -113,6 +113,14 @@ describe('PersistentRoomHost', () => { ); }); + it('keeps a forum room on the timeline route when the timeline param asks for it', () => { + renderHost(`/home/${encodeURIComponent(FORUM_ROOM_ID)}/?timeline=true`); + expect(screen.getByTestId('room-timeline')).toBeInTheDocument(); + expect(screen.getByTestId('pathname')).toHaveTextContent( + `/home/${encodeURIComponent(FORUM_ROOM_ID)}/` + ); + }); + it('preloads the last visited room on a list route without redirecting', () => { renderHost('/home', { home: FORUM_ROOM_ID }); expect(screen.getByTestId('room-timeline')).toBeInTheDocument(); diff --git a/src/app/features/forum/ForumHeader.tsx b/src/app/features/forum/ForumHeader.tsx index dc0ec32290..57b88bcf84 100644 --- a/src/app/features/forum/ForumHeader.tsx +++ b/src/app/features/forum/ForumHeader.tsx @@ -225,6 +225,7 @@ export function ForumHeader({ room, showProfile, powerLevels }: ForumHeaderProps fill="None" onClick={menu.triggerProps.onClick} ref={triggerRef} + aria-label="More Options" aria-pressed={!!menu.anchor} > {composerIcon(DotsThreeOutlineVerticalIcon, { diff --git a/src/app/features/forum/ForumMenu.tsx b/src/app/features/forum/ForumMenu.tsx index 21cb7159d1..8f5735e332 100644 --- a/src/app/features/forum/ForumMenu.tsx +++ b/src/app/features/forum/ForumMenu.tsx @@ -15,7 +15,13 @@ import { settingsAtom } from '$state/settings'; import { markAsRead } from '$utils/notifications'; import { copyToClipboard } from '$utils/dom'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '$utils/matrix'; -import { getHomeRoomPath, getDirectRoomPath, getSpaceRoomPath } from '$pages/pathUtils'; +import { + getHomeRoomPath, + getDirectRoomPath, + getSpaceRoomPath, + withSearchParam, +} from '$pages/pathUtils'; +import { ROOM_TIMELINE_SEARCH_PARAM } from '$pages/paths'; import { getMatrixToRoom } from '$plugins/matrix-to'; import { getViaServers } from '$plugins/via-servers'; import { @@ -77,14 +83,16 @@ export const ForumMenu = forwardRef( const handleOpenTimeline = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); + let path: string; if (parentSpace) { const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace.roomId); - navigate(getSpaceRoomPath(spaceIdOrAlias, roomIdOrAlias)); + path = getSpaceRoomPath(spaceIdOrAlias, roomIdOrAlias); } else if (isDirectRoom) { - navigate(getDirectRoomPath(roomIdOrAlias)); + path = getDirectRoomPath(roomIdOrAlias); } else { - navigate(getHomeRoomPath(roomIdOrAlias)); + path = getHomeRoomPath(roomIdOrAlias); } + navigate(withSearchParam(path, { [ROOM_TIMELINE_SEARCH_PARAM]: 'true' })); requestClose(); }; diff --git a/src/app/pages/client/RoomRoute.tsx b/src/app/pages/client/RoomRoute.tsx index 08a1336ac3..54a015da67 100644 --- a/src/app/pages/client/RoomRoute.tsx +++ b/src/app/pages/client/RoomRoute.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ForumView } from '$features/forum'; import { Room } from '$features/room'; import { useRoom } from '$hooks/useRoom'; @@ -11,6 +11,7 @@ import { getSpaceForumPath, getSpaceRoomPath, } from '$pages/pathUtils'; +import { ROOM_TIMELINE_SEARCH_PARAM } from '$pages/paths'; import { CustomRoomType } from '$types/matrix/room'; export type RoomRouteSection = 'home' | 'direct' | 'space'; @@ -34,7 +35,10 @@ export function RoomGate({ }: RoomGateProps) { const room = useRoom(); const navigate = useNavigate(); - const isForum = room.getType() === CustomRoomType.Forum; + const [searchParams] = useSearchParams(); + // Lets the developer tools open a forum room's timeline without being redirected back. + const timelineRequested = !forum && searchParams.get(ROOM_TIMELINE_SEARCH_PARAM) === 'true'; + const isForum = room.getType() === CustomRoomType.Forum && !timelineRequested; useEffect(() => { if (isForum === forum) return; diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 1bb9e91433..7ed6ee74d9 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -52,7 +52,10 @@ export const SEARCH_PATH_SEGMENT = 'search/'; export type RoomSearchParams = { /* comma separated string of servers */ viaServers?: string; + /* "true" keeps a forum room on the timeline route instead of the forum view */ + timeline?: string; }; +export const ROOM_TIMELINE_SEARCH_PARAM = 'timeline'; export const ROOM_PATH_SEGMENT = ':roomIdOrAlias/:eventId?/'; export const ROOM_FORUM_PATH_SEGMENT = ':roomIdOrAlias/forum/:eventId?/'; diff --git a/tests/e2e/forum-event-timeline.spec.ts b/tests/e2e/forum-event-timeline.spec.ts new file mode 100644 index 0000000000..4986631439 --- /dev/null +++ b/tests/e2e/forum-event-timeline.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from './fixtures/test'; +import { createRoom, sendText } from './fixtures/continuwuity'; +import { AppShell } from './pages/AppShell'; +import { homeserverBaseUrl, loginAsFreshUser } from './fixtures/session'; + +const FORUM_ROOM_TYPE = 'pl.chrome.forum'; + +test.describe('forum event timeline', () => { + test('opens the event timeline of a forum room and stays on it', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop', 'desktop-focused'); + test.setTimeout(300_000); + const storageStatePath = testInfo.project.use.storageState as string; + const hsBaseUrl = await homeserverBaseUrl(storageStatePath); + const tag = `evtl-${process.pid}-${Date.now().toString(36)}`; + const app = new AppShell(page); + const user = await loginAsFreshUser(page, hsBaseUrl, `${tag}-u`); + + // The entry only exists with developer tools on. + await page.addInitScript(() => { + localStorage.setItem('settings', JSON.stringify({ developerTools: true })); + }); + + const forumId = await createRoom(hsBaseUrl, user.accessToken, { + name: `${tag} Forum`, + preset: 'private_chat', + creation_content: { type: FORUM_ROOM_TYPE }, + }); + await sendText(hsBaseUrl, user.accessToken, forumId, `${tag} topic one`, 1); + + await page.goto('/'); + await expect(page.getByText(`${tag} Forum`).first()).toBeVisible({ timeout: 180_000 }); + await app.openRoom(`${tag} Forum`); + await expect(page).toHaveURL(/\/forum\/?$/); + await expect(page.getByText(`${tag} topic one`).first()).toBeVisible(); + + // The forum header's options button, not the room nav item's. + const options = page.getByRole('button', { name: 'More Options' }); + await expect(options).toHaveCount(2); + await options.last().click(); + await page.getByRole('button', { name: 'Event Timeline' }).click(); + + await expect(page.getByText(`${tag} topic one`).first()).toBeVisible(); + await page.waitForTimeout(3000); + await expect(page).not.toHaveURL(/\/forum\/?$/); + await expect(page.getByText(`${tag} topic one`).first()).toBeVisible(); + }); + + test('keeps the timeline route of a forum room when opened cold', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop', 'desktop-focused'); + test.setTimeout(300_000); + const storageStatePath = testInfo.project.use.storageState as string; + const hsBaseUrl = await homeserverBaseUrl(storageStatePath); + const tag = `evtlcold-${process.pid}-${Date.now().toString(36)}`; + const user = await loginAsFreshUser(page, hsBaseUrl, `${tag}-u`); + + const forumId = await createRoom(hsBaseUrl, user.accessToken, { + name: `${tag} Forum`, + preset: 'private_chat', + creation_content: { type: FORUM_ROOM_TYPE }, + }); + await sendText(hsBaseUrl, user.accessToken, forumId, `${tag} topic one`, 1); + + await page.goto(`/home/${encodeURIComponent(forumId)}/?timeline=true`); + await expect(page.getByText(`${tag} topic one`).first()).toBeVisible({ timeout: 180_000 }); + await page.waitForTimeout(3000); + await expect(page).not.toHaveURL(/\/forum\/?$/); + await expect(page.getByText(`${tag} topic one`).first()).toBeVisible(); + }); +});