mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
1 Commits
v0.21.0
...
dbkr/alias
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e17fd4b68e |
@@ -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