From 6ae4f0539a5b41c2a97a5dfaf7d2500005126fd7 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 8 Sep 2026 13:53:50 +0200 Subject: [PATCH] Leave the page title to the page The call and the lobby each set `document.title`, so a component embedded in a host renamed the host's tab to "Element Call | ". The title belongs to whoever owns the page: the standalone app's RoomPage now sets it, for whichever room it has got as far as knowing about, and the call itself no longer touches it. Co-Authored-By: Claude Fable 5.1 --- src/room/GroupCallView.tsx | 2 -- src/room/LobbyView.tsx | 2 -- src/room/RoomPage.tsx | 16 ++++++++++++++++ src/room/useRoomName.ts | 29 +++++++++++++++++++++-------- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 623b8839c..054ff450d 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -57,7 +57,6 @@ import { type CallEventSounds, } from "./CallEventAudioRenderer"; import { useLatest } from "../useLatest"; -import { usePageTitle } from "../usePageTitle"; import { ConnectionLostError, E2EENotSupportedError, @@ -208,7 +207,6 @@ export const GroupCallView: FC = ({ if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl); }, [passwordFromUrl, room.roomId]); - usePageTitle(roomName); useAppBarTitle(roomName); const matrixInfo = useMemo((): MatrixInfo => { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index e122b3f26..60fb715e7 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -44,7 +44,6 @@ import { useTrackProcessor, useTrackProcessorSync, } from "../livekit/TrackProcessorContext"; -import { usePageTitle } from "../usePageTitle"; import { getValue } from "../utils/observable"; import { useBehavior } from "../useBehavior"; import { CallFooter, type FooterSnapshot } from "../components/CallFooter"; @@ -87,7 +86,6 @@ export const LobbyView: FC = ({ const { t } = useTranslation(); - usePageTitle(matrixInfo.roomName); useAppBarPrimaryButtonIconKind("back"); const audioEnabled = useBehavior(muteStates.audio.enabled$); const videoEnabled = useBehavior(muteStates.video.enabled$); diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index f49509e30..8d974ee46 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -25,6 +25,8 @@ import { useProfile } from "../profile/useProfile"; import { useOptInAnalytics } from "../settings/settings"; import { Link } from "../button/Link"; import { ErrorView } from "../ErrorView"; +import { usePageTitle } from "../usePageTitle"; +import { useRoomName } from "./useRoomName"; export const RoomPage: FC = (): ReactNode => { const urlParams = useUrlParams(); @@ -46,6 +48,20 @@ export const RoomPage: FC = (): ReactNode => { const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers); + // The page title is the page's to set, not the call's: a host embedding the + // call has a title of its own. So it is set here, for whichever room we have + // got as far as knowing about. + const roomName = useRoomName( + groupCallState.kind === "loaded" ? groupCallState.rtcSession.room : null, + ); + usePageTitle( + roomName ?? + (groupCallState.kind === "canKnock" || + groupCallState.kind === "waitForInvite" + ? groupCallState.roomSummary.name + : undefined), + ); + useEffect(() => { // If we've finished loading, are not already authed and we've been given a display name as // a URL param, automatically register a passwordless user diff --git a/src/room/useRoomName.ts b/src/room/useRoomName.ts index 838578570..f20b3b2a8 100644 --- a/src/room/useRoomName.ts +++ b/src/room/useRoomName.ts @@ -6,14 +6,27 @@ Please see LICENSE in the repository root for full details. */ import { type Room, RoomEvent } from "matrix-js-sdk"; -import { useCallback } from "react"; +import { useCallback, useSyncExternalStore } from "react"; -import { useTypedEventEmitterState } from "../useEvents"; - -export function useRoomName(room: Room): string { - return useTypedEventEmitterState( - room, - RoomEvent.Name, - useCallback(() => room.name, [room]), +/** + * The room's name, kept up to date. Null when there is no room yet, for a + * caller that only sometimes has one. + */ +export function useRoomName(room: Room): string; +export function useRoomName(room: Room | null): string | null; +export function useRoomName(room: Room | null): string | null { + const subscribe = useCallback( + (onChange: () => void) => { + if (room === null) return (): void => {}; + room.on(RoomEvent.Name, onChange); + return (): void => { + room.off(RoomEvent.Name, onChange); + }; + }, + [room], + ); + return useSyncExternalStore( + subscribe, + useCallback(() => room?.name ?? null, [room]), ); }