diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 85f07cb..7960209 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -29,7 +29,11 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { ErrorView } from "./FullScreenView"; -import { initClient, initMatroskaClient, defaultHomeserver } from "./matrix-utils"; +import { + initClient, + initMatroskaClient, + defaultHomeserver, +} from "./matrix-utils"; declare global { interface Window { diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 90cd00e..0d2e58d 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -5,7 +5,11 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store"; import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store"; -import { createClient, createRoomWidgetClient, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { + createClient, + createRoomWidgetClient, + MatrixClient, +} from "matrix-js-sdk/src/matrix"; import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { EventType } from "matrix-js-sdk/src/@types/event"; @@ -15,6 +19,7 @@ import { WidgetApi } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; import IndexedDBWorker from "./IndexedDBWorker?worker"; +import { getRoomParams } from "./room/useRoomParams"; export const defaultHomeserver = (import.meta.env.VITE_DEFAULT_HOMESERVER as string) ?? @@ -82,20 +87,20 @@ const SEND_RECV_TO_DEVICE = [ * @returns The MatrixClient instance */ export async function initMatroskaClient( - widgetId: string, parentUrl: string, + widgetId: string, + parentUrl: string ): Promise { // In this mode, we use a special client which routes all requests through // the host application via the widget API - // The rest of the data we need is encoded in the fragment so as to avoid - // leaking it to the server - const fragmentQueryStart = window.location.hash.indexOf("?"); - const roomId = window.location.hash.substring(0, fragmentQueryStart); - const fragmentQuery = new URLSearchParams(window.location.hash.substring(fragmentQueryStart)); + const { roomId, userId, deviceId } = getRoomParams(); + if (!roomId) throw new Error("Room ID must be supplied"); + if (!userId) throw new Error("User ID must be supplied"); + if (!deviceId) throw new Error("Device ID must be supplied"); // Since all data should be coming from the host application, there's no // need to persist anything, and therefore we can use the default stores - // We don't even need to set up crypto! + // We don't even need to set up crypto const client = createRoomWidgetClient( new WidgetApi(widgetId, new URL(parentUrl).origin), { @@ -103,14 +108,15 @@ export async function initMatroskaClient( receiveState: SEND_RECV_STATE, sendToDevice: SEND_RECV_TO_DEVICE, receiveToDevice: SEND_RECV_TO_DEVICE, + turnServers: true, }, roomId, { baseUrl: "", - userId: fragmentQuery.get("userId"), - deviceId: fragmentQuery.get("deviceId"), + userId, + deviceId, timelineSupport: true, - }, + } ); await client.startClient(); @@ -192,16 +198,13 @@ export async function initClient( storeOpts.cryptoStore = new MemoryCryptoStore(); } - // XXX: we read from the URL search params in RoomPage too: + // XXX: we read from the room params in RoomPage too: // it would be much better to read them in one place and pass // the values around, but we initialise the matrix client in // many different places so we'd have to pass it into all of // them. - const params = new URLSearchParams(window.location.search); - // disable e2e only if enableE2e=false is given - const enableE2e = params.get("enableE2e") !== "false"; - - if (!enableE2e) { + const { e2eEnabled } = getRoomParams(); + if (!e2eEnabled) { logger.info("Disabling E2E: group call signalling will NOT be encrypted."); } @@ -212,7 +215,7 @@ export async function initClient( // Use a relatively low timeout for API calls: this is a realtime app // so we don't want API calls taking ages, we'd rather they just fail. localTimeoutMs: 5000, - useE2eForGroupCall: enableE2e, + useE2eForGroupCall: e2eEnabled, }); try { @@ -319,17 +322,17 @@ export async function createRoom( return [fullAliasFromRoomName(name, client), result.room_id]; } -export function getRoomUrl(roomId: string): string { - if (roomId.startsWith("#")) { - const [localPart, host] = roomId.replace("#", "").split(":"); +export function getRoomUrl(roomIdOrAlias: string): string { + if (roomIdOrAlias.startsWith("#")) { + const [localPart, host] = roomIdOrAlias.replace("#", "").split(":"); if (host !== defaultHomeserverHost) { - return `${window.location.protocol}//${window.location.host}/room/${roomId}`; + return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`; } else { return `${window.location.protocol}//${window.location.host}/${localPart}`; } } else { - return `${window.location.protocol}//${window.location.host}/room/${roomId}`; + return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`; } } diff --git a/src/room/GroupCallLoader.jsx b/src/room/GroupCallLoader.jsx index 70791a2..5b7f481 100644 --- a/src/room/GroupCallLoader.jsx +++ b/src/room/GroupCallLoader.jsx @@ -21,14 +21,14 @@ import { usePageTitle } from "../usePageTitle"; export function GroupCallLoader({ client, - roomId, + roomIdOrAlias, viaServers, createPtt, children, }) { const { loading, error, groupCall } = useLoadGroupCall( client, - roomId, + roomIdOrAlias, viaServers, createPtt ); diff --git a/src/room/GroupCallView.jsx b/src/room/GroupCallView.jsx index 9794a00..445a507 100644 --- a/src/room/GroupCallView.jsx +++ b/src/room/GroupCallView.jsx @@ -31,7 +31,7 @@ export function GroupCallView({ client, isPasswordlessUser, isEmbedded, - roomId, + roomIdOrAlias, groupCall, }) { const { @@ -89,7 +89,7 @@ export function GroupCallView({ return ( ); @@ -153,7 +153,7 @@ export function GroupCallView({ localVideoMuted={localVideoMuted} toggleLocalVideoMuted={toggleLocalVideoMuted} toggleMicrophoneMuted={toggleMicrophoneMuted} - roomId={roomId} + roomIdOrAlias={roomIdOrAlias} isEmbedded={isEmbedded} /> ); diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx index 43a71a8..5bdef29 100644 --- a/src/room/InCallView.jsx +++ b/src/room/InCallView.jsx @@ -65,7 +65,7 @@ export function InCallView({ toggleScreensharing, isScreensharing, screenshareFeeds, - roomId, + roomIdOrAlias, unencryptedEventsFromUsers, }) { usePreventScroll(); @@ -184,7 +184,7 @@ export function InCallView({ )} )} diff --git a/src/room/InviteModal.jsx b/src/room/InviteModal.jsx index 0a7e9ce..f804162 100644 --- a/src/room/InviteModal.jsx +++ b/src/room/InviteModal.jsx @@ -20,7 +20,7 @@ import { CopyButton } from "../button"; import { getRoomUrl } from "../matrix-utils"; import styles from "./InviteModal.module.css"; -export function InviteModal({ roomId, ...rest }) { +export function InviteModal({ roomIdOrAlias, ...rest }) { return (

Copy and share this meeting link

- +
); diff --git a/src/room/LobbyView.jsx b/src/room/LobbyView.jsx index de22db0..fd43560 100644 --- a/src/room/LobbyView.jsx +++ b/src/room/LobbyView.jsx @@ -41,7 +41,7 @@ export function LobbyView({ localVideoMuted, toggleLocalVideoMuted, toggleMicrophoneMuted, - roomId, + roomIdOrAlias, isEmbedded, }) { const { stream } = useCallFeed(localCallFeed); @@ -95,7 +95,7 @@ export function LobbyView({ Or diff --git a/src/room/OverflowMenu.jsx b/src/room/OverflowMenu.jsx index c5810f0..39c9ffe 100644 --- a/src/room/OverflowMenu.jsx +++ b/src/room/OverflowMenu.jsx @@ -30,7 +30,7 @@ import { TooltipTrigger } from "../Tooltip"; import { FeedbackModal } from "./FeedbackModal"; export function OverflowMenu({ - roomId, + roomIdOrAlias, inCall, groupCall, showInvite, @@ -88,7 +88,7 @@ export function OverflowMenu({ {settingsModalState.isOpen && } {inviteModalState.isOpen && ( - + )} {feedbackModalState.isOpen && ( = ({ client, - roomId, + roomIdOrAlias, roomName, avatarUrl, groupCall, @@ -204,7 +204,7 @@ export const PTTCallView: React.FC = ({
= ({
{inviteModalState.isOpen && showControls && ( - + )} ); diff --git a/src/room/RageshakeRequestModal.jsx b/src/room/RageshakeRequestModal.jsx index 201b308..29fbe9e 100644 --- a/src/room/RageshakeRequestModal.jsx +++ b/src/room/RageshakeRequestModal.jsx @@ -21,7 +21,11 @@ import { FieldRow, ErrorMessage } from "../input/Input"; import { useSubmitRageshake } from "../settings/submit-rageshake"; import { Body } from "../typography/Typography"; -export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) { +export function RageshakeRequestModal({ + rageshakeRequestId, + roomIdOrAlias, + ...rest +}) { const { submitRageshake, sending, sent, error } = useSubmitRageshake(); useEffect(() => { @@ -43,7 +47,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) { submitRageshake({ sendLogs: true, rageshakeRequestId, - roomId, + roomIdOrAlias, // Possibly not a room ID, but oh well }) } disabled={sending} diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.jsx index 72f6bf4..931eae1 100644 --- a/src/room/RoomPage.jsx +++ b/src/room/RoomPage.jsx @@ -1,5 +1,5 @@ /* -Copyright 2021 New Vector Ltd +Copyright 2021-2022 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useMemo, useState } from "react"; -import { useLocation, useParams } from "react-router-dom"; +import React, { FC, useEffect, useState } from "react"; import { useClient } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; import { RoomAuthView } from "./RoomAuthView"; import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallView } from "./GroupCallView"; +import { useRoomParams } from "./useRoomParams"; import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; @@ -28,20 +28,12 @@ export function RoomPage() { const { loading, isAuthenticated, error, client, isPasswordlessUser } = useClient(); - const { roomId: maybeRoomId } = useParams(); - const { hash, search } = useLocation(); - const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { - const params = new URLSearchParams(search); - return [ - params.getAll("via"), - params.has("embed"), - params.get("ptt") === "true", - params.get("displayName"), - ]; - }, [search]); - const roomId = (maybeRoomId || hash || "").toLowerCase(); - const { registerPasswordlessUser, recaptchaId } = - useRegisterPasswordlessUser(); + const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } = + useRoomParams(); + const roomIdOrAlias = roomId ?? roomAlias; + if (!roomIdOrAlias) throw new Error("No room specified"); + + const { registerPasswordlessUser } = useRegisterPasswordlessUser(); const [isRegistering, setIsRegistering] = useState(false); useEffect(() => { @@ -76,14 +68,14 @@ export function RoomPage() { {(groupCall) => ( { + const fragmentQueryStart = fragment.indexOf("?"); + const fragmentParams = new URLSearchParams( + fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart) + ); + const queryParams = new URLSearchParams(query); + + // Normally, room params should be encoded in the fragment so as to avoid + // leaking them to the server. However, we also check the normal query + // string for backwards compatibility with versions that only used that. + const hasParam = (name: string): boolean => + fragmentParams.has(name) || queryParams.has(name); + const getParam = (name: string): string | null => + fragmentParams.get(name) ?? queryParams.get(name); + const getAllParams = (name: string): string[] => [ + ...fragmentParams.getAll(name), + ...queryParams.getAll(name), + ]; + + // The part of the fragment before the ? + const fragmentRoute = + fragmentQueryStart === -1 + ? fragment + : fragment.substring(0, fragmentQueryStart); + + return { + roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null, + roomId: getParam("roomId"), + viaServers: getAllParams("via"), + isEmbedded: hasParam("embed"), + isPtt: hasParam("ptt"), + e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true + userId: getParam("userId"), + displayName: getParam("displayName"), + deviceId: getParam("deviceId"), + }; +}; + +/** + * Hook to simplify use of getRoomParams. + * @returns {RoomParams} The room parameters for the current URL + */ +export const useRoomParams = (): RoomParams => { + const { hash, search } = useLocation(); + return useMemo(() => getRoomParams(search, hash), [search, hash]); +};