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