From e17fd4b68e6df0f5708efa6a22b1bd889541b111 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Sep 2023 17:27:07 +0100 Subject: [PATCH] Human readable URLs This tries to implement the new scheme for element call URLs, including: * All rooms have an alias * Legacy unencrypted rooms keep their old alias * New encerypted rooms get an autogenerated alias so they have a string that can always be used to join them. * The room name is hyphenated and included in the URL However this is buggy as it stands. Firstly, I am gettign initialisation weirdness where it keeps thinking I'm unregistered, then remembering all my rooms again when I enter a display name... *realises* oh, this is because it's looking for config.json in the current directory, ie. /c/. Another bug is that it doesn't update the URL bar with the canonical URL for the room, which it probably ought to. The main blocker, however, is that it turns out we use the absence of a canonical alias to tell whether a room is supposed to be an e2ee call room, so this would need that mechanism replaced. --- src/UrlParams.ts | 57 +++++---- src/e2ee/sharedKeyManagement.ts | 10 +- src/home/CallList.tsx | 32 +++-- src/home/RegisteredView.tsx | 20 ++- src/home/UnauthenticatedView.tsx | 22 +++- src/home/useGroupCallRooms.ts | 18 ++- src/matrix-utils.ts | 207 ++++++++++++++++++++++++------- src/room/AppSelectionModal.tsx | 18 +-- src/room/GroupCallLoader.tsx | 28 ++++- src/room/GroupCallView.tsx | 2 +- src/room/RoomPage.tsx | 5 +- src/room/ShareModal.tsx | 11 +- src/room/useLoadGroupCall.ts | 83 +++++-------- 13 files changed, 334 insertions(+), 179 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 91a292a3..ca50aaeb 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -16,10 +16,11 @@ limitations under the License. import { useMemo } from "react"; import { useLocation } from "react-router-dom"; +import { logger } from "matrix-js-sdk/src/logger"; import { Config } from "./config/Config"; -export const PASSWORD_STRING = "password="; +export const PASSWORD_PARAM = "password"; interface RoomIdentifier { roomAlias: string | null; @@ -134,10 +135,20 @@ class ParamParser { // leaking them to the server. However, we also check the normal query // string for backwards compatibility with versions that only used that. hasParam(name: string): boolean { + if (!this.fragmentParams.has(name) && this.queryParams.has(name)) { + logger.warn( + `Parameter ${name} loaded from query param (not hash). This is unsupported and will soon be removed.` + ); + } return this.fragmentParams.has(name) || this.queryParams.has(name); } getParam(name: string): string | null { + if (!this.fragmentParams.has(name) && this.queryParams.has(name)) { + logger.warn( + `Parameter ${name} loaded from query param (not hash). This is unsupported and will soon be removed.` + ); + } return this.fragmentParams.get(name) ?? this.queryParams.get(name); } @@ -168,7 +179,7 @@ export const getUrlParams = ( // what would we do if it were invalid? If the widget API says that's what // the room ID is, then that's what it is. roomId: parser.getParam("roomId"), - password: parser.getParam("password"), + password: parser.getParam(PASSWORD_PARAM), isEmbedded: parser.hasParam("embed"), preload: parser.hasParam("preload"), hideHeader: parser.hasParam("hideHeader"), @@ -200,34 +211,24 @@ export function getRoomIdentifierFromUrl( search: string, hash: string ): RoomIdentifier { - let roomAlias: string | null = null; - - // Here we handle the beginning of the alias and make sure it starts with a "#" + let baseRoomString: string | undefined; if (hash === "" || hash.startsWith("#?")) { - roomAlias = pathname.substring(1); // Strip the "/" - - // Delete "/room/", if present - if (roomAlias.startsWith("room/")) { - roomAlias = roomAlias.substring("room/".length); - } - // Add "#", if not present - if (!roomAlias.startsWith("#")) { - roomAlias = `#${roomAlias}`; - } + // if the hash is absent or being used as a query string, the alias is the last + // path component. + baseRoomString = pathname.split("/").pop(); } else { - roomAlias = hash; + baseRoomString = hash; + logger.warn( + "Using whole hash as room name: this is deprecated and will be removed soon." + ); } - // Delete "?" and what comes afterwards - roomAlias = roomAlias.split("?")[0]; - - if (roomAlias.length <= 1) { - // Make roomAlias is null, if it only is a "#" - roomAlias = null; - } else { - // Add server part, if not present + let roomAlias: string | null = null; + if (baseRoomString !== undefined) { + // ensure exactly one hash on the start + roomAlias = `${baseRoomString.replace(/^#*/, "#")}`; if (!roomAlias.includes(":")) { - roomAlias = `${roomAlias}:${Config.defaultServerName()}`; + roomAlias += ":" + Config.defaultServerName(); } } @@ -241,6 +242,12 @@ export function getRoomIdentifierFromUrl( roomId = null; } + if (roomId) { + logger.warn( + "Room loaded by room ID: this is not supported and will be removed soon." + ); + } + return { roomAlias, roomId, diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index 83eca2ea..cea5efa6 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -19,7 +19,7 @@ import { useEffect, useMemo } from "react"; import { useEnableE2EE } from "../settings/useSetting"; import { useLocalStorage } from "../useLocalStorage"; import { useClient } from "../ClientContext"; -import { PASSWORD_STRING, useUrlParams } from "../UrlParams"; +import { PASSWORD_PARAM, useUrlParams } from "../UrlParams"; export const getRoomSharedKeyLocalStorageKey = (roomId: string): string => `room-shared-key-${roomId}`; @@ -50,18 +50,18 @@ export const useManageRoomSharedKey = (roomId: string): string | null => { setE2EESharedKey(password); }, [password, e2eeSharedKey, setE2EESharedKey]); - useEffect(() => { + /*useEffect(() => { const hash = location.hash; if (!hash.includes("?")) return; - if (!hash.includes(PASSWORD_STRING)) return; + if (!hash.includes(`${PASSWORD_PARAM}=`)) return; if (password !== e2eeSharedKey) return; - const [hashStart, passwordStart] = hash.split(PASSWORD_STRING); + const [hashStart, passwordStart] = hash.split(`${PASSWORD_PARAM}=`); const hashEnd = passwordStart.split("&")[1]; location.replace((hashStart ?? "") + (hashEnd ?? "")); - }, [password, e2eeSharedKey]); + }, [password, e2eeSharedKey]);*/ return e2eeSharedKey; }; diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index a82e4c13..d2f667d1 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -16,12 +16,15 @@ limitations under the License. import { Link } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { Room } from "matrix-js-sdk"; import { CopyButton } from "../button"; import { Avatar, Size } from "../Avatar"; import styles from "./CallList.module.css"; -import { getRoomUrl } from "../matrix-utils"; +import { + getAbsoluteRoomUrlForRoom, + getRelativeRoomUrlForRoom, +} from "../matrix-utils"; import { Body } from "../typography/Typography"; import { GroupCallRoom } from "./useGroupCallRooms"; import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; @@ -34,14 +37,14 @@ export function CallList({ rooms, client }: CallListProps) { return ( <>
- {rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => ( + {rooms.map(({ room, roomName, avatarUrl }) => ( ))} {rooms.length > 3 && ( @@ -57,17 +60,20 @@ export function CallList({ rooms, client }: CallListProps) { interface CallTileProps { name: string; avatarUrl: string; - roomId: string; - participants: RoomMember[]; + room: Room; + //participants: RoomMember[]; client: MatrixClient; } -function CallTile({ name, avatarUrl, roomId }: CallTileProps) { - const roomSharedKey = useRoomSharedKey(roomId); +function CallTile({ name, avatarUrl, room }: CallTileProps) { + const roomSharedKey = useRoomSharedKey(room.roomId) ?? undefined; return (
- - + +
{name} @@ -78,7 +84,7 @@ function CallTile({ name, avatarUrl, roomId }: CallTileProps) {
); diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 4e7b151c..85a077e2 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -23,6 +23,7 @@ import { Heading } from "@vector-im/compound-web"; import { createRoom, + getRelativeRoomUrl, roomAliasLocalpartFromRoomName, sanitiseRoomNameInput, } from "../matrix-utils"; @@ -75,18 +76,25 @@ export function RegisteredView({ client }: Props) { setError(undefined); setLoading(true); - const roomId = ( - await createRoom(client, roomName, e2eeEnabled ?? false) - )[1]; - + const createResult = await createRoom( + client, + roomName, + e2eeEnabled ?? false + ); if (e2eeEnabled) { setLocalStorageItem( - getRoomSharedKeyLocalStorageKey(roomId), + getRoomSharedKeyLocalStorageKey(createResult.roomId), randomString(32) ); } - history.push(`/room/#?roomId=${roomId}`); + history.push( + getRelativeRoomUrl( + createResult.roomId, + roomName, + createResult.roomAlias + ) + ); } submit().catch((error) => { diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index e001c5a8..7819b0be 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -26,7 +26,9 @@ import { UserMenuContainer } from "../UserMenuContainer"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Button } from "../button"; import { + CreateRoomResult, createRoom, + getRelativeRoomUrl, roomAliasLocalpartFromRoomName, sanitiseRoomNameInput, } from "../matrix-utils"; @@ -85,15 +87,17 @@ export const UnauthenticatedView: FC = () => { true ); - let roomId: string; + let createResult: CreateRoomResult; try { - roomId = ( - await createRoom(client, roomName, e2eeEnabled ?? false) - )[1]; + createResult = await createRoom( + client, + roomName, + e2eeEnabled ?? false + ); if (e2eeEnabled) { setLocalStorageItem( - getRoomSharedKeyLocalStorageKey(roomId), + getRoomSharedKeyLocalStorageKey(createResult.roomId), randomString(32) ); } @@ -125,7 +129,13 @@ export const UnauthenticatedView: FC = () => { } setClient({ client, session }); - history.push(`/room/#?roomId=${roomId}`); + history.push( + getRelativeRoomUrl( + createResult.roomId, + roomName, + createResult.roomAlias + ) + ); } submit().catch((error) => { diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index 58135b04..0578ca53 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -15,19 +15,16 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; -import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler"; import { useState, useEffect } from "react"; export interface GroupCallRoom { - roomAlias: string; + roomAlias?: string; roomName: string; avatarUrl: string; room: Room; - groupCall: GroupCall; - participants: RoomMember[]; + //participants: RoomMember[]; } const tsCache: { [index: string]: number } = {}; @@ -87,23 +84,22 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { return; } + // XXX: This should use client.matrixRTC.getActiveRoomSessions() not the old + // group call stuff! const groupCalls = client.groupCallEventHandler.groupCalls.values(); const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room); const sortedRooms = sortRooms(client, rooms); const items = sortedRooms.map((room) => { - const groupCall = client.getGroupCallForRoom(room.roomId)!; - return { - roomAlias: room.getCanonicalAlias(), + roomAlias: room.getCanonicalAlias() ?? undefined, roomName: room.name, avatarUrl: room.getMxcAvatarUrl()!, room, - groupCall, - participants: [...groupCall!.participants.keys()], + //participants: [...groupCall!.participants.keys()], }; }); - setRooms(items as GroupCallRoom[]); + setRooms(items); } updateRooms(); diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index a9f37011..25101cc6 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -19,7 +19,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, ICreateClientOpts } from "matrix-js-sdk/src/matrix"; +import { + createClient, + ICreateClientOpts, + MatrixError, +} from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; @@ -28,11 +32,12 @@ import { GroupCallIntent, GroupCallType, } from "matrix-js-sdk/src/webrtc/groupCall"; +import { randomLowercaseString } from "matrix-js-sdk/src/randomstring"; import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { Room } from "matrix-js-sdk/src/models/room"; import IndexedDBWorker from "./IndexedDBWorker?worker"; -import { getUrlParams, PASSWORD_STRING } from "./UrlParams"; +import { getUrlParams, PASSWORD_PARAM } from "./UrlParams"; import { loadOlm } from "./olm"; import { Config } from "./config/Config"; @@ -208,10 +213,6 @@ export function roomAliasLocalpartFromRoomName(roomName: string): string { .toLowerCase(); } -function fullAliasFromRoomName(roomName: string, client: MatrixClient): string { - return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`; -} - /** * Applies some basic sanitisation to a room name that the user * has given us @@ -269,46 +270,79 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean { return parts[1] === client.getDomain(); } +export interface CreateRoomResult { + roomAlias: string; + roomId: string; +} + +async function createRoomWrapper( + client: MatrixClient, + name: string, + e2ee: boolean +): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const aliasLocalpart = e2ee + ? randomLowercaseString(16) + : roomAliasLocalpartFromRoomName(name); + + const result = await client.createRoom({ + visibility: Visibility.Private, + preset: Preset.PublicChat, + name, + room_alias_name: aliasLocalpart, + power_level_content_override: { + invite: 100, + kick: 100, + ban: 100, + redact: 50, + state_default: 0, + events_default: 0, + users_default: 0, + events: { + "m.room.power_levels": 100, + "m.room.history_visibility": 100, + "m.room.tombstone": 100, + "m.room.encryption": 100, + "m.room.name": 50, + "m.room.message": 0, + "m.room.encrypted": 50, + "m.sticker": 50, + "org.matrix.msc3401.call.member": 0, + }, + users: { + [client.getUserId()!]: 100, + }, + }, + }); + + return { + roomAlias: `#${aliasLocalpart}:${client.getDomain()}`, + roomId: result.room_id, + }; + } catch (e) { + if (e2ee && (e as MatrixError).errcode === "M_ROOM_IN_USE") { + logger.info("Alias is taken, trying a different alias"); + continue; + } else { + throw e; + } + } + } +} + export async function createRoom( client: MatrixClient, name: string, e2ee: boolean -): Promise<[string, string]> { +): Promise { logger.log(`Creating room for group call`); - const createPromise = client.createRoom({ - visibility: Visibility.Private, - preset: Preset.PublicChat, - name, - room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name), - power_level_content_override: { - invite: 100, - kick: 100, - ban: 100, - redact: 50, - state_default: 0, - events_default: 0, - users_default: 0, - events: { - "m.room.power_levels": 100, - "m.room.history_visibility": 100, - "m.room.tombstone": 100, - "m.room.encryption": 100, - "m.room.name": 50, - "m.room.message": 0, - "m.room.encrypted": 50, - "m.sticker": 50, - "org.matrix.msc3401.call.member": 0, - }, - users: { - [client.getUserId()!]: 100, - }, - }, - }); - + const createPromise = createRoomWrapper(client, name, e2ee); // Wait for the room to arrive await new Promise((resolve, reject) => { const onRoom = async (room: Room) => { - if (room.roomId === (await createPromise).room_id) { + if (room.roomId === (await createPromise).roomId) { resolve(); cleanUp(); } @@ -326,29 +360,108 @@ export async function createRoom( const result = await createPromise; - logger.log(`Creating group call in ${result.room_id}`); + logger.log(`Creating group call in ${result.roomId}`); await client.createGroupCall( - result.room_id, + result.roomId, GroupCallType.Video, false, GroupCallIntent.Room, true ); - return [fullAliasFromRoomName(name, client), result.room_id]; + return result; } /** - * Returns a URL to that will load Element Call with the given room - * @param roomId of the room - * @param password - * @returns + * Returns an absolute URL to that will load Element Call with the given room + * @param room The room object + * @param password The shared key for the room, or undefined if none */ -export function getRoomUrl(roomId: string, password?: string): string { +export function getAbsoluteRoomUrlForRoom(room: Room, password?: string) { + return getAbsoluteRoomUrl( + room.roomId, + room.name, + room.getCanonicalAlias() ?? undefined, + password + ); +} + +export function getAbsoluteRoomUrl( + roomId?: string, + roomName?: string, + roomAlias?: string, + password?: string +): string { return `${window.location.protocol}//${ window.location.host - }/room/#?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`; + }${getRelativeRoomUrl(roomId, roomName, roomAlias, password)}`; +} + +/** + * Gets a relative URL to a room (convenience wrapper around getRelativeRoomUrl) + * @param room The room object + * @param password The shared key for the room, or undefined if none + */ +export function getRelativeRoomUrlForRoom( + room: Room, + password?: string +): string { + return getRelativeRoomUrl( + room.roomId, + room.name, + room.getCanonicalAlias() ?? undefined, + password + ); +} + +/** + * Gets a relative URL to a room + * @param roomId The room's ID' + * @param roomName The room's name, if known, otherwise undefined + * @param roomAlias The room alias string to use for the link + * @param password The shared key for the room, or undefined if none + */ +export function getRelativeRoomUrl( + roomId?: string, + roomName?: string, + roomAlias?: string, + password?: string +): string { + let url = ""; + let queryAdded = false; + + if (roomAlias) { + const aliasParts = roomAlias.substring(1).split(":"); + const aliasBase = aliasParts.shift(); + const aliasServerPart = aliasParts.join(":"); + + const compressedRoomName = roomName + ? roomAliasLocalpartFromRoomName(roomName) + : undefined; + + url += "/c/"; + + if (compressedRoomName !== undefined && compressedRoomName !== aliasBase) { + url += compressedRoomName + "/"; + } + + if (aliasServerPart === Config.defaultServerName()) { + url += aliasBase; + } else { + url += `#?${roomAlias.substring(1)}}`; + queryAdded = true; + } + } else { + url += `/c/#?roomId=${roomId}`; + queryAdded = true; + } + + if (password) { + url += `${queryAdded ? "&" : "#?"}${PASSWORD_PARAM}=${password}`; + } + + return url; } export function getAvatarUrl( diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx index 3480e9d8..c6bfdd87 100644 --- a/src/room/AppSelectionModal.tsx +++ b/src/room/AppSelectionModal.tsx @@ -21,15 +21,16 @@ import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/ import { Modal } from "../Modal"; import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; -import { getRoomUrl } from "../matrix-utils"; +import { getAbsoluteRoomUrl } from "../matrix-utils"; import styles from "./AppSelectionModal.module.css"; import { editFragmentQuery } from "../UrlParams"; interface Props { - roomId: string | null; + roomId?: string; + roomAlias?: string; } -export const AppSelectionModal: FC = ({ roomId }) => { +export const AppSelectionModal: FC = ({ roomId, roomAlias }) => { const { t } = useTranslation(); const [open, setOpen] = useState(true); @@ -46,9 +47,12 @@ export const AppSelectionModal: FC = ({ roomId }) => { const appUrl = useMemo(() => { // If the room ID is not known, fall back to the URL of the current page const url = new URL( - roomId === null - ? window.location.href - : getRoomUrl(roomId, roomSharedKey ?? undefined) + getAbsoluteRoomUrl( + roomId, + undefined, + roomAlias, + roomSharedKey ?? undefined + ) ); // Edit the URL so that it opens in embedded mode. We do this for two // reasons: It causes the mobile app to limit the user to only visiting the @@ -62,7 +66,7 @@ export const AppSelectionModal: FC = ({ roomId }) => { const result = new URL("element://call"); result.searchParams.set("url", url.toString()); return result.toString(); - }, [roomId, roomSharedKey]); + }, [roomId, roomAlias, roomSharedKey]); return ( diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index 5f71f203..cca495a4 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -14,10 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode } from "react"; +import { ReactNode, useCallback } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { MatrixError } from "matrix-js-sdk"; +import { useHistory } from "react-router-dom"; +import { Link } from "@vector-im/compound-web"; import { useLoadGroupCall } from "./useLoadGroupCall"; import { ErrorView, FullScreenView } from "../FullScreenView"; @@ -38,6 +41,9 @@ export function GroupCallLoader({ const { t } = useTranslation(); const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers); + const history = useHistory(); + const onHomeClick = useCallback(() => history.push("/"), [history]); + switch (groupCallState.kind) { case "loading": return ( @@ -48,6 +54,24 @@ export function GroupCallLoader({ case "loaded": return <>{children(groupCallState.rtcSession)}; case "failed": - return ; + if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") { + return ( + +

{t("Call not found")}

+

+ {t( + "Element Calls are now end-to-end encrypted and need to be explicitly created. This helps make sure everyone's using the same encryption key." + )} +

+ {/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have + dupes of this flow, let's make a common component and put it here. */} + + {t("Home")} + +
+ ); + } else { + return ; + } } } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 6a8c9333..870f5e92 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -301,7 +301,7 @@ export function GroupCallView({ const shareModal = ( diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 76302ca9..1095471e 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -110,7 +110,10 @@ export const RoomPage: FC = () => { {/* On mobile, show a prompt to launch the mobile app. If in embedded mode, that means we *are* in the mobile app and should show no further prompt. */} {(platform === "android" || platform === "ios") && !isEmbedded && ( - + )} ); diff --git a/src/room/ShareModal.tsx b/src/room/ShareModal.tsx index b9adc8e0..9e7703e8 100644 --- a/src/room/ShareModal.tsx +++ b/src/room/ShareModal.tsx @@ -16,29 +16,30 @@ limitations under the License. import { FC } from "react"; import { useTranslation } from "react-i18next"; +import { Room } from "matrix-js-sdk"; import { Modal } from "../Modal"; import { CopyButton } from "../button"; -import { getRoomUrl } from "../matrix-utils"; +import { getAbsoluteRoomUrlForRoom } from "../matrix-utils"; import styles from "./ShareModal.module.css"; import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; interface Props { - roomId: string; + room: Room; open: boolean; onDismiss: () => void; } -export const ShareModal: FC = ({ roomId, open, onDismiss }) => { +export const ShareModal: FC = ({ room, open, onDismiss }) => { const { t } = useTranslation(); - const roomSharedKey = useRoomSharedKey(roomId); + const roomSharedKey = useRoomSharedKey(room.roomId) ?? undefined; return (

{t("Copy and share this call link")}

diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index e6f79ced..a95fbe25 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -20,14 +20,11 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { SyncState } from "matrix-js-sdk/src/sync"; import { useTranslation } from "react-i18next"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { randomString } from "matrix-js-sdk/src/randomstring"; +import { useHistory } from "react-router-dom"; import type { Room } from "matrix-js-sdk/src/models/room"; import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; -import { setLocalStorageItem } from "../useLocalStorage"; -import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; import { useEnableE2EE } from "../settings/useSetting"; -import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement"; export type GroupCallLoaded = { kind: "loaded"; @@ -63,60 +60,46 @@ export const useLoadGroupCall = ( const [e2eeEnabled] = useEnableE2EE(); + const history = useHistory(); + useEffect(() => { const fetchOrCreateRoom = async (): Promise => { - try { + let room: Room | null = null; + if (roomIdOrAlias[0] === "#") { // We lowercase the localpart when we create the room, so we must lowercase // it here too (we just do the whole alias). We can't do the same to room IDs // though. - const sanitisedIdOrAlias = - roomIdOrAlias[0] === "#" - ? roomIdOrAlias.toLowerCase() - : roomIdOrAlias; - - const room = await client.joinRoom(sanitisedIdOrAlias, { - viaServers, - }); - logger.info( - `Joined ${sanitisedIdOrAlias}, waiting room to be ready for group calls` + // Also, we explicitly look up the room alias here. We previously just tried to + // join anyway but the js-sdk recreates the room if you pass the alias for a + // room you're already joined to (which it probably ought not to). + const lookupResult = await client.getRoomIdForAlias( + roomIdOrAlias.toLowerCase() ); - await client.waitUntilRoomReadyForGroupCalls(room.roomId); - logger.info(`${sanitisedIdOrAlias}, is ready for group calls`); - return room; - } catch (error) { - if ( - isLocalRoomId(roomIdOrAlias, client) && - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (error.errcode === "M_NOT_FOUND" || - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (error.message && - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - error.message.indexOf("Failed to fetch alias") !== -1)) - ) { - // The room doesn't exist, but we can create it - const [, roomId] = await createRoom( - client, - roomNameFromRoomId(roomIdOrAlias), - e2eeEnabled ?? false - ); - - if (e2eeEnabled) { - setLocalStorageItem( - getRoomSharedKeyLocalStorageKey(roomId), - randomString(32) - ); - } - - // likewise, wait for the room - await client.waitUntilRoomReadyForGroupCalls(roomId); - return client.getRoom(roomId)!; + logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`); + room = client.getRoom(lookupResult.room_id); + if (!room) { + logger.info(`Room ${lookupResult.room_id} not found, joining.`); + room = await client.joinRoom(lookupResult.room_id, { + viaServers: lookupResult.servers, + }); } else { - throw error; + logger.info( + `Already in room ${lookupResult.room_id}, not rejoining.` + ); } + } else { + // room IDs we just try to join by their ID, which will not work in the + // general case without providing some servers to join via. We could provide + // our own server, but in practice that is implicit. + room = await client.joinRoom(roomIdOrAlias); } + + logger.info( + `Joined ${roomIdOrAlias}, waiting room to be ready for group calls` + ); + await client.waitUntilRoomReadyForGroupCalls(room.roomId); + logger.info(`${roomIdOrAlias}, is ready for group calls`); + return room; }; const fetchOrCreateGroupCall = async (): Promise => { @@ -149,7 +132,7 @@ export const useLoadGroupCall = ( .then(fetchOrCreateGroupCall) .then((rtcSession) => setState({ kind: "loaded", rtcSession })) .catch((error) => setState({ kind: "failed", error })); - }, [client, roomIdOrAlias, viaServers, t, e2eeEnabled]); + }, [client, roomIdOrAlias, viaServers, t, e2eeEnabled, history]); return state; };