mirror of
https://github.com/vector-im/element-call.git
synced 2026-04-03 07:10:26 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ export function GroupCallView({
|
||||
|
||||
const shareModal = (
|
||||
<ShareModal
|
||||
roomId={rtcSession.room.roomId}
|
||||
room={rtcSession.room}
|
||||
open={shareModalOpen}
|
||||
onDismiss={onDismissShareModal}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user