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; };