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.
This commit is contained in:
David Baker
2023-09-19 17:27:07 +01:00
parent caa6f5bcde
commit e17fd4b68e
13 changed files with 334 additions and 179 deletions

View File

@@ -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,

View File

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

View File

@@ -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 (
<>
<div className={styles.callList}>
{rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => (
{rooms.map(({ room, roomName, avatarUrl }) => (
<CallTile
key={roomAlias}
key={room.roomId}
client={client}
name={roomName}
avatarUrl={avatarUrl}
roomId={room.roomId}
participants={participants}
room={room}
//participants={participants}
/>
))}
{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 (
<div className={styles.callTile}>
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
<Avatar id={roomId} name={name} size={Size.LG} src={avatarUrl} />
<Link
to={getRelativeRoomUrlForRoom(room, roomSharedKey)}
className={styles.callTileLink}
>
<Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} />
<div className={styles.callInfo}>
<Body overflowEllipsis fontWeight="semiBold">
{name}
@@ -78,7 +84,7 @@ function CallTile({ name, avatarUrl, roomId }: CallTileProps) {
<CopyButton
className={styles.copyButton}
variant="icon"
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
value={getAbsoluteRoomUrlForRoom(room, roomSharedKey)}
/>
</div>
);

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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();

View File

@@ -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<CreateRoomResult> {
// 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<CreateRoomResult> {
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<void>((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(

View File

@@ -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<Props> = ({ roomId }) => {
export const AppSelectionModal: FC<Props> = ({ roomId, roomAlias }) => {
const { t } = useTranslation();
const [open, setOpen] = useState(true);
@@ -46,9 +47,12 @@ export const AppSelectionModal: FC<Props> = ({ 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<Props> = ({ roomId }) => {
const result = new URL("element://call");
result.searchParams.set("url", url.toString());
return result.toString();
}, [roomId, roomSharedKey]);
}, [roomId, roomAlias, roomSharedKey]);
return (
<Modal className={styles.modal} title={t("Select app")} open={open}>

View File

@@ -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 <ErrorView error={groupCallState.error} />;
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
return (
<FullScreenView>
<h1>{t("Call not found")}</h1>
<p>
{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."
)}
</p>
{/* 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. */}
<Link href="/" onClick={onHomeClick}>
{t("Home")}
</Link>
</FullScreenView>
);
} else {
return <ErrorView error={groupCallState.error} />;
}
}
}

View File

@@ -301,7 +301,7 @@ export function GroupCallView({
const shareModal = (
<ShareModal
roomId={rtcSession.room.roomId}
room={rtcSession.room}
open={shareModalOpen}
onDismiss={onDismissShareModal}
/>

View File

@@ -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 && (
<AppSelectionModal roomId={roomId} />
<AppSelectionModal
roomId={roomId ?? undefined}
roomAlias={roomAlias ?? undefined}
/>
)}
</>
);

View File

@@ -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<Props> = ({ roomId, open, onDismiss }) => {
export const ShareModal: FC<Props> = ({ room, open, onDismiss }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId);
const roomSharedKey = useRoomSharedKey(room.roomId) ?? undefined;
return (
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
value={getAbsoluteRoomUrlForRoom(room, roomSharedKey)}
data-testid="modal_inviteLink"
/>
</Modal>

View File

@@ -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<Room> => {
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<MatrixRTCSession> => {
@@ -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;
};