Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession
This commit is contained in:
2
.github/workflows/publish.yaml
vendored
2
.github/workflows/publish.yaml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=sha,format=short,event=branch
|
type=sha,format=short,event=branch
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern=v{{version}}
|
||||||
type=raw,value=latest-ci,enable={{is_default_branch}}
|
type=raw,value=latest-ci,enable={{is_default_branch}}
|
||||||
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
|
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,6 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)",
|
|
||||||
"Passwords must match": "Passwords must match",
|
"Passwords must match": "Passwords must match",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Recaptcha dismissed": "Recaptcha dismissed",
|
"Recaptcha dismissed": "Recaptcha dismissed",
|
||||||
@@ -106,6 +105,7 @@
|
|||||||
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
|
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
|
||||||
"Thanks!": "Thanks!",
|
"Thanks!": "Thanks!",
|
||||||
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||||
|
"This call is not end-to-end encrypted.": "This call is not end-to-end encrypted.",
|
||||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
|
||||||
"Turn off camera": "Turn off camera",
|
"Turn off camera": "Turn off camera",
|
||||||
"Turn on camera": "Turn on camera",
|
"Turn on camera": "Turn on camera",
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ import { Trans } from "react-i18next";
|
|||||||
import { Banner } from "./Banner";
|
import { Banner } from "./Banner";
|
||||||
import styles from "./E2EEBanner.module.css";
|
import styles from "./E2EEBanner.module.css";
|
||||||
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
|
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
|
||||||
|
import { useEnableE2EE } from "./settings/useSetting";
|
||||||
|
|
||||||
export const E2EEBanner = () => {
|
export const E2EEBanner = () => {
|
||||||
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
if (e2eeEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<div className={styles.e2eeBanner}>
|
<div className={styles.e2eeBanner}>
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ import { TooltipTrigger } from "./Tooltip";
|
|||||||
export const E2EELock = () => {
|
export const E2EELock = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tooltip = useCallback(
|
const tooltip = useCallback(
|
||||||
() =>
|
() => t("This call is not end-to-end encrypted."),
|
||||||
t(
|
|
||||||
"Element Call is temporarily not end-to-end encrypted while we test scalability."
|
|
||||||
),
|
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { useLocation } from "react-router-dom";
|
|||||||
|
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
|
|
||||||
|
export const PASSWORD_STRING = "password=";
|
||||||
|
|
||||||
interface UrlParams {
|
interface UrlParams {
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
@@ -86,6 +88,10 @@ interface UrlParams {
|
|||||||
* user's homeserver doesn't provide any.
|
* user's homeserver doesn't provide any.
|
||||||
*/
|
*/
|
||||||
allowIceFallback: boolean;
|
allowIceFallback: boolean;
|
||||||
|
/**
|
||||||
|
* E2EE password
|
||||||
|
*/
|
||||||
|
password: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,9 +108,12 @@ export const getUrlParams = (
|
|||||||
pathname = window.location.pathname,
|
pathname = window.location.pathname,
|
||||||
hash = window.location.hash
|
hash = window.location.hash
|
||||||
): UrlParams => {
|
): UrlParams => {
|
||||||
|
// This is legacy code - we're moving away from using aliases
|
||||||
let roomAlias: string | null = null;
|
let roomAlias: string | null = null;
|
||||||
if (!ignoreRoomAlias) {
|
if (!ignoreRoomAlias) {
|
||||||
if (hash === "") {
|
// Here we handle the beginning of the alias and make sure it starts with a
|
||||||
|
// "#"
|
||||||
|
if (hash === "" || hash.startsWith("#?")) {
|
||||||
roomAlias = pathname.substring(1); // Strip the "/"
|
roomAlias = pathname.substring(1); // Strip the "/"
|
||||||
|
|
||||||
// Delete "/room/", if present
|
// Delete "/room/", if present
|
||||||
@@ -119,17 +128,17 @@ export const getUrlParams = (
|
|||||||
roomAlias = hash;
|
roomAlias = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add server part, if not present
|
|
||||||
if (!roomAlias.includes(":")) {
|
|
||||||
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete "?" and what comes afterwards
|
// Delete "?" and what comes afterwards
|
||||||
roomAlias = roomAlias.split("?")[0];
|
roomAlias = roomAlias.split("?")[0];
|
||||||
|
|
||||||
// Make roomAlias undefined, if empty
|
|
||||||
if (roomAlias.length <= 1) {
|
if (roomAlias.length <= 1) {
|
||||||
|
// Make roomAlias is null, if it only is a "#"
|
||||||
roomAlias = null;
|
roomAlias = null;
|
||||||
|
} else {
|
||||||
|
// Add server part, if not present
|
||||||
|
if (!roomAlias.includes(":")) {
|
||||||
|
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +173,7 @@ export const getUrlParams = (
|
|||||||
return {
|
return {
|
||||||
roomAlias,
|
roomAlias,
|
||||||
roomId,
|
roomId,
|
||||||
|
password: getParam("password"),
|
||||||
viaServers: getAllParams("via"),
|
viaServers: getAllParams("via"),
|
||||||
isEmbedded: hasParam("embed"),
|
isEmbedded: hasParam("embed"),
|
||||||
preload: hasParam("preload"),
|
preload: hasParam("preload"),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk";
|
|||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
import { widget } from "../widget";
|
import { widget } from "../widget";
|
||||||
import { getSetting, setSetting, settingsBus } from "../settings/useSetting";
|
import { getSetting, setSetting, getSettingKey } from "../settings/useSetting";
|
||||||
import {
|
import {
|
||||||
CallEndedTracker,
|
CallEndedTracker,
|
||||||
CallStartedTracker,
|
CallStartedTracker,
|
||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from "./PosthogEvents";
|
} from "./PosthogEvents";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
|
import { localStorageBus } from "../useLocalStorage";
|
||||||
|
|
||||||
/* Posthog analytics tracking.
|
/* Posthog analytics tracking.
|
||||||
*
|
*
|
||||||
@@ -413,7 +414,7 @@ export class PosthogAnalytics {
|
|||||||
// * When the user changes their preferences on this device
|
// * When the user changes their preferences on this device
|
||||||
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
||||||
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
||||||
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
|
localStorageBus.on(getSettingKey("opt-in-analytics"), (optInAnalytics) => {
|
||||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/e2ee/sharedKeyManagement.ts
Normal file
81
src/e2ee/sharedKeyManagement.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
|
import { useLocalStorage } from "../useLocalStorage";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
|
import { PASSWORD_STRING, useUrlParams } from "../UrlParams";
|
||||||
|
|
||||||
|
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
|
||||||
|
`room-shared-key-${roomId}`;
|
||||||
|
|
||||||
|
export const useInternalRoomSharedKey = (
|
||||||
|
roomId: string
|
||||||
|
): [string | null, (value: string) => void] => {
|
||||||
|
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
|
||||||
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
|
||||||
|
|
||||||
|
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRoomSharedKey = (roomId: string): string | null => {
|
||||||
|
return useInternalRoomSharedKey(roomId)[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useManageRoomSharedKey = (roomId: string): string | null => {
|
||||||
|
const { password } = useUrlParams();
|
||||||
|
const [e2eeSharedKey, setE2EESharedKey] = useInternalRoomSharedKey(roomId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!password) return;
|
||||||
|
if (password === "") return;
|
||||||
|
if (password === e2eeSharedKey) return;
|
||||||
|
|
||||||
|
setE2EESharedKey(password);
|
||||||
|
}, [password, e2eeSharedKey, setE2EESharedKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = location.hash;
|
||||||
|
|
||||||
|
if (!hash.includes("?")) return;
|
||||||
|
if (!hash.includes(PASSWORD_STRING)) return;
|
||||||
|
if (password !== e2eeSharedKey) return;
|
||||||
|
|
||||||
|
const [hashStart, passwordStart] = hash.split(PASSWORD_STRING);
|
||||||
|
const hashEnd = passwordStart.split("&")[1];
|
||||||
|
|
||||||
|
location.replace((hashStart ?? "") + (hashEnd ?? ""));
|
||||||
|
}, [password, e2eeSharedKey]);
|
||||||
|
|
||||||
|
return e2eeSharedKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsRoomE2EE = (roomId: string): boolean | null => {
|
||||||
|
const client = useClient();
|
||||||
|
const room = useMemo(
|
||||||
|
() => client.client?.getRoom(roomId) ?? null,
|
||||||
|
[roomId, client.client]
|
||||||
|
);
|
||||||
|
const isE2EE = useMemo(
|
||||||
|
() => (room ? !room?.getCanonicalAlias() : null),
|
||||||
|
[room]
|
||||||
|
);
|
||||||
|
|
||||||
|
return isE2EE;
|
||||||
|
};
|
||||||
@@ -23,8 +23,9 @@ import { Facepile } from "../Facepile";
|
|||||||
import { Avatar, Size } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import styles from "./CallList.module.css";
|
import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
import { GroupCallRoom } from "./useGroupCallRooms";
|
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||||
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface CallListProps {
|
interface CallListProps {
|
||||||
rooms: GroupCallRoom[];
|
rooms: GroupCallRoom[];
|
||||||
@@ -35,13 +36,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
|
{rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => (
|
||||||
<CallTile
|
<CallTile
|
||||||
key={roomAlias}
|
key={roomAlias}
|
||||||
client={client}
|
client={client}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
roomAlias={roomAlias}
|
roomId={room.roomId}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
disableFacepile={disableFacepile}
|
disableFacepile={disableFacepile}
|
||||||
/>
|
/>
|
||||||
@@ -59,7 +60,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
|||||||
interface CallTileProps {
|
interface CallTileProps {
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
roomAlias: string;
|
roomId: string;
|
||||||
participants: RoomMember[];
|
participants: RoomMember[];
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
disableFacepile?: boolean;
|
disableFacepile?: boolean;
|
||||||
@@ -67,17 +68,16 @@ interface CallTileProps {
|
|||||||
function CallTile({
|
function CallTile({
|
||||||
name,
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
roomAlias,
|
roomId,
|
||||||
participants,
|
participants,
|
||||||
client,
|
client,
|
||||||
disableFacepile,
|
disableFacepile,
|
||||||
}: CallTileProps) {
|
}: CallTileProps) {
|
||||||
|
const roomSharedKey = useRoomSharedKey(roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.callTile}>
|
<div className={styles.callTile}>
|
||||||
<Link
|
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
|
||||||
to={`/${roomAlias.substring(1).split(":")[0]}`}
|
|
||||||
className={styles.callTileLink}
|
|
||||||
>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
size={Size.LG}
|
size={Size.LG}
|
||||||
bgKey={name}
|
bgKey={name}
|
||||||
@@ -89,7 +89,6 @@ function CallTile({
|
|||||||
<Body overflowEllipsis fontWeight="semiBold">
|
<Body overflowEllipsis fontWeight="semiBold">
|
||||||
{name}
|
{name}
|
||||||
</Body>
|
</Body>
|
||||||
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
|
|
||||||
{participants && !disableFacepile && (
|
{participants && !disableFacepile && (
|
||||||
<Facepile
|
<Facepile
|
||||||
className={styles.facePile}
|
className={styles.facePile}
|
||||||
@@ -103,7 +102,7 @@ function CallTile({
|
|||||||
<CopyButton
|
<CopyButton
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
value={getRoomUrl(roomAlias)}
|
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
|
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -37,9 +38,11 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
|||||||
import { Caption, Title } from "../typography/Typography";
|
import { Caption, Title } from "../typography/Typography";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||||
import { useOptInAnalytics } from "../settings/useSetting";
|
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
|
||||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||||
import { E2EEBanner } from "../E2EEBanner";
|
import { E2EEBanner } from "../E2EEBanner";
|
||||||
|
import { setLocalStorageItem } from "../useLocalStorage";
|
||||||
|
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
@@ -54,6 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
(e: FormEvent) => {
|
(e: FormEvent) => {
|
||||||
@@ -70,16 +74,23 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const [roomAlias] = await createRoom(client, roomName, ptt);
|
const roomId = (
|
||||||
|
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
|
||||||
|
)[1];
|
||||||
|
|
||||||
if (roomAlias) {
|
if (e2eeEnabled) {
|
||||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
setLocalStorageItem(
|
||||||
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
|
randomString(32)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
history.push(`/room/#?roomId=${roomId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
if (error.errcode === "M_ROOM_IN_USE") {
|
if (error.errcode === "M_ROOM_IN_USE") {
|
||||||
setExistingRoomId(roomAliasLocalpartFromRoomName(roomName));
|
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
modalState.open();
|
modalState.open();
|
||||||
@@ -90,15 +101,15 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client, history, modalState, callType]
|
[client, history, modalState, callType, e2eeEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
const recentRooms = useGroupCallRooms(client);
|
||||||
|
|
||||||
const [existingRoomId, setExistingRoomId] = useState<string>();
|
const [existingAlias, setExistingAlias] = useState<string>();
|
||||||
const onJoinExistingRoom = useCallback(() => {
|
const onJoinExistingRoom = useCallback(() => {
|
||||||
history.push(`/${existingRoomId}`);
|
history.push(`/${existingAlias}`);
|
||||||
}, [history, existingRoomId]);
|
}, [history, existingAlias]);
|
||||||
|
|
||||||
const callNameLabel =
|
const callNameLabel =
|
||||||
callType === CallType.Video
|
callType === CallType.Video
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ import styles from "./UnauthenticatedView.module.css";
|
|||||||
import commonStyles from "./common.module.css";
|
import commonStyles from "./common.module.css";
|
||||||
import { generateRandomName } from "../auth/generateRandomName";
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||||
import { useOptInAnalytics } from "../settings/useSetting";
|
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { E2EEBanner } from "../E2EEBanner";
|
import { E2EEBanner } from "../E2EEBanner";
|
||||||
|
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
import { setLocalStorageItem } from "../useLocalStorage";
|
||||||
|
|
||||||
export const UnauthenticatedView: FC = () => {
|
export const UnauthenticatedView: FC = () => {
|
||||||
const { setClient } = useClient();
|
const { setClient } = useClient();
|
||||||
@@ -58,6 +60,8 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -79,9 +83,18 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
let roomAlias: string;
|
let roomId: string;
|
||||||
try {
|
try {
|
||||||
[roomAlias] = await createRoom(client, roomName, ptt);
|
roomId = (
|
||||||
|
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
|
||||||
|
)[1];
|
||||||
|
|
||||||
|
if (e2eeEnabled) {
|
||||||
|
setLocalStorageItem(
|
||||||
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
|
randomString(32)
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!setClient) {
|
if (!setClient) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -110,7 +123,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setClient({ client, session });
|
setClient({ client, session });
|
||||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
history.push(`/room/#?roomId=${roomId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
@@ -120,7 +133,16 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
reset();
|
reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[register, reset, execute, history, callType, modalState, setClient]
|
[
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
execute,
|
||||||
|
history,
|
||||||
|
callType,
|
||||||
|
modalState,
|
||||||
|
setClient,
|
||||||
|
e2eeEnabled,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const callNameLabel =
|
const callNameLabel =
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
|||||||
|
|
||||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
||||||
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
|
const sortedRooms = sortRooms(client, rooms);
|
||||||
const sortedRooms = sortRooms(client, filteredRooms);
|
|
||||||
const items = sortedRooms.map((room) => {
|
const items = sortedRooms.map((room) => {
|
||||||
const groupCall = client.getGroupCallForRoom(room.roomId)!;
|
const groupCall = client.getGroupCallForRoom(room.roomId)!;
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
|
||||||
import { loadOlm } from "./olm";
|
import { loadOlm } from "./olm";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
|
|
||||||
@@ -272,14 +272,15 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
|
|||||||
export async function createRoom(
|
export async function createRoom(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
name: string,
|
name: string,
|
||||||
ptt: boolean
|
ptt: boolean,
|
||||||
|
e2ee: boolean
|
||||||
): Promise<[string, string]> {
|
): Promise<[string, string]> {
|
||||||
logger.log(`Creating room for group call`);
|
logger.log(`Creating room for group call`);
|
||||||
const createPromise = client.createRoom({
|
const createPromise = client.createRoom({
|
||||||
visibility: Visibility.Private,
|
visibility: Visibility.Private,
|
||||||
preset: Preset.PublicChat,
|
preset: Preset.PublicChat,
|
||||||
name,
|
name,
|
||||||
room_alias_name: roomAliasLocalpartFromRoomName(name),
|
room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name),
|
||||||
power_level_content_override: {
|
power_level_content_override: {
|
||||||
invite: 100,
|
invite: 100,
|
||||||
kick: 100,
|
kick: 100,
|
||||||
@@ -341,15 +342,16 @@ export async function createRoom(
|
|||||||
return [fullAliasFromRoomName(name, client), result.room_id];
|
return [fullAliasFromRoomName(name, client), result.room_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a URL to that will load Element Call with the given room
|
/**
|
||||||
export function getRoomUrl(roomIdOrAlias: string): string {
|
* Returns a URL to that will load Element Call with the given room
|
||||||
if (roomIdOrAlias.startsWith("#")) {
|
* @param roomId of the room
|
||||||
return `${window.location.protocol}//${window.location.host}/${
|
* @param password
|
||||||
roomIdOrAlias.substring(1).split(":")[0]
|
* @returns
|
||||||
}`;
|
*/
|
||||||
} else {
|
export function getRoomUrl(roomId: string, password?: string): string {
|
||||||
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
|
return `${window.location.protocol}//${
|
||||||
}
|
window.location.host
|
||||||
|
}/room/#?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvatarUrl(
|
export function getAvatarUrl(
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import { MatrixInfo } from "./VideoPreview";
|
|||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
|
||||||
import { findDeviceByName } from "../media-utils";
|
import { findDeviceByName } from "../media-utils";
|
||||||
//import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
//import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall } from "./InCallView";
|
||||||
@@ -41,6 +40,11 @@ import { LivekitFocus } from "../livekit/LivekitFocus";
|
|||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
||||||
|
import {
|
||||||
|
useManageRoomSharedKey,
|
||||||
|
useIsRoomE2EE,
|
||||||
|
} from "../e2ee/sharedKeyManagement";
|
||||||
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -78,6 +82,9 @@ export function GroupCallView({
|
|||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||||
|
|
||||||
|
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
|
||||||
|
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -243,8 +250,11 @@ export function GroupCallView({
|
|||||||
}
|
}
|
||||||
}, [isJoined, rtcSession]);
|
}, [isJoined, rtcSession]);
|
||||||
|
|
||||||
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
undefined
|
|
||||||
|
const e2eeConfig = useMemo(
|
||||||
|
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
|
||||||
|
[e2eeSharedKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
@@ -266,6 +276,22 @@ export function GroupCallView({
|
|||||||
return <ErrorView error={new Error("Call focus is not compatible!")} />;
|
return <ErrorView error={new Error("Call focus is not compatible!")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
|
||||||
|
return (
|
||||||
|
<ErrorView
|
||||||
|
error={
|
||||||
|
new Error(
|
||||||
|
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e2eeEnabled && isRoomE2EE) {
|
||||||
|
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isJoined) {
|
if (isJoined) {
|
||||||
return (
|
return (
|
||||||
/*<OpenIDLoader
|
/*<OpenIDLoader
|
||||||
@@ -325,10 +351,7 @@ export function GroupCallView({
|
|||||||
<LobbyView
|
<LobbyView
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
onEnter={(e2eeConfig?: E2EEConfig) => {
|
onEnter={() => enter()}
|
||||||
setE2EEConfig(e2eeConfig);
|
|
||||||
enterRTCSession(rtcSession);
|
|
||||||
}}
|
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import { useEventEmitterThree } from "../useEvents";
|
|||||||
import { useWakeLock } from "../useWakeLock";
|
import { useWakeLock } from "../useWakeLock";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
|
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -133,6 +134,8 @@ export function InCallView({
|
|||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
|
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const boundsValid = bounds.height > 0;
|
const boundsValid = bounds.height > 0;
|
||||||
@@ -408,7 +411,7 @@ export function InCallView({
|
|||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={rtcSession.room.name} />
|
<RoomHeaderInfo roomName={rtcSession.room.name} />
|
||||||
<E2EELock />
|
{!isRoomE2EE && <E2EELock />}
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||||
@@ -445,12 +448,7 @@ export function InCallView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{inviteModalState.isOpen && (
|
{inviteModalState.isOpen && (
|
||||||
<InviteModal
|
<InviteModal roomId={rtcSession.room.roomId} {...inviteModalProps} />
|
||||||
roomIdOrAlias={
|
|
||||||
rtcSession.room.getCanonicalAlias() ?? rtcSession.room.roomId
|
|
||||||
}
|
|
||||||
{...inviteModalProps}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2022 New Vector Ltd
|
Copyright 2022 - 2023 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -21,13 +21,15 @@ import { Modal, ModalContent, ModalProps } from "../Modal";
|
|||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import styles from "./InviteModal.module.css";
|
import styles from "./InviteModal.module.css";
|
||||||
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||||
roomIdOrAlias: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const roomSharedKey = useRoomSharedKey(roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -40,7 +42,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
|||||||
<p>{t("Copy and share this call link")}</p>
|
<p>{t("Copy and share this call link")}</p>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
value={getRoomUrl(roomIdOrAlias)}
|
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||||
data-testid="modal_inviteLink"
|
data-testid="modal_inviteLink"
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -14,14 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { useRef, useEffect, FC } from "react";
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
ChangeEvent,
|
|
||||||
FC,
|
|
||||||
} from "react";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
@@ -32,15 +25,13 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
|||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
|
||||||
import { InputField } from "../input/Input";
|
|
||||||
import { useEnableE2EE } from "../settings/useSetting";
|
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
onEnter: (e2eeConfig?: E2EEConfig) => void;
|
onEnter: () => void;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
@@ -53,10 +44,9 @@ export const LobbyView: FC<Props> = ({
|
|||||||
hideHeader,
|
hideHeader,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
|
||||||
useLocationNavigation();
|
useLocationNavigation();
|
||||||
|
|
||||||
const [enableE2EE] = useEnableE2EE();
|
|
||||||
|
|
||||||
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
|
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (joinCallButtonRef.current) {
|
if (joinCallButtonRef.current) {
|
||||||
@@ -64,18 +54,6 @@ export const LobbyView: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [joinCallButtonRef]);
|
}, [joinCallButtonRef]);
|
||||||
|
|
||||||
const [e2eeSharedKey, setE2EESharedKey] = useState<string | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const onE2EESharedKeyChanged = useCallback(
|
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
setE2EESharedKey(value === "" ? undefined : value);
|
|
||||||
},
|
|
||||||
[setE2EESharedKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.room}>
|
<div className={styles.room}>
|
||||||
{!hideHeader && (
|
{!hideHeader && (
|
||||||
@@ -91,25 +69,12 @@ export const LobbyView: FC<Props> = ({
|
|||||||
<div className={styles.joinRoom}>
|
<div className={styles.joinRoom}>
|
||||||
<div className={styles.joinRoomContent}>
|
<div className={styles.joinRoomContent}>
|
||||||
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates} />
|
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates} />
|
||||||
{enableE2EE && (
|
|
||||||
<InputField
|
|
||||||
className={styles.passwordField}
|
|
||||||
label={t("Password (if none, E2EE is disabled)")}
|
|
||||||
type="text"
|
|
||||||
onChange={onE2EESharedKeyChanged}
|
|
||||||
value={e2eeSharedKey}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Trans>
|
<Trans>
|
||||||
<Button
|
<Button
|
||||||
ref={joinCallButtonRef}
|
ref={joinCallButtonRef}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
size="lg"
|
size="lg"
|
||||||
onPress={() =>
|
onPress={() => onEnter()}
|
||||||
onEnter(
|
|
||||||
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data-testid="lobby_joinCall"
|
data-testid="lobby_joinCall"
|
||||||
>
|
>
|
||||||
Join call now
|
Join call now
|
||||||
@@ -117,7 +82,7 @@ export const LobbyView: FC<Props> = ({
|
|||||||
<Body>Or</Body>
|
<Body>Or</Body>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
variant="secondaryCopy"
|
variant="secondaryCopy"
|
||||||
value={getRoomUrl(matrixInfo.roomAlias ?? matrixInfo.roomId)}
|
value={getRoomUrl(matrixInfo.roomId, roomSharedKey ?? undefined)}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
copiedMessage={t("Call link copied")}
|
copiedMessage={t("Call link copied")}
|
||||||
data-testid="lobby_inviteLink"
|
data-testid="lobby_inviteLink"
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
|||||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { setLocalStorageItem } from "../useLocalStorage";
|
||||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
|
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
export type GroupCallLoaded = {
|
export type GroupCallLoaded = {
|
||||||
kind: "loaded";
|
kind: "loaded";
|
||||||
@@ -58,6 +62,8 @@ export const useLoadGroupCall = (
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||||
|
|
||||||
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||||
try {
|
try {
|
||||||
@@ -95,8 +101,17 @@ export const useLoadGroupCall = (
|
|||||||
const [, roomId] = await createRoom(
|
const [, roomId] = await createRoom(
|
||||||
client,
|
client,
|
||||||
roomNameFromRoomId(roomIdOrAlias),
|
roomNameFromRoomId(roomIdOrAlias),
|
||||||
createPtt
|
createPtt,
|
||||||
|
e2eeEnabled ?? false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (e2eeEnabled) {
|
||||||
|
setLocalStorageItem(
|
||||||
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
|
randomString(32)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// likewise, wait for the room
|
// likewise, wait for the room
|
||||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||||
return client.getRoom(roomId)!;
|
return client.getRoom(roomId)!;
|
||||||
@@ -136,7 +151,7 @@ export const useLoadGroupCall = (
|
|||||||
.then(fetchOrCreateGroupCall)
|
.then(fetchOrCreateGroupCall)
|
||||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||||
.catch((error) => setState({ kind: "failed", error }));
|
.catch((error) => setState({ kind: "failed", error }));
|
||||||
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
|
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2022 New Vector Ltd
|
Copyright 2022 - 2023 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,59 +14,49 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
|
||||||
import { isE2EESupported } from "livekit-client";
|
import { isE2EESupported } from "livekit-client";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
|
import {
|
||||||
|
getLocalStorageItem,
|
||||||
|
setLocalStorageItem,
|
||||||
|
useLocalStorage,
|
||||||
|
} from "../useLocalStorage";
|
||||||
|
|
||||||
type Setting<T> = [T, (value: T) => void];
|
type Setting<T> = [T, (value: T) => void];
|
||||||
type DisableableSetting<T> = [T, ((value: T) => void) | null];
|
type DisableableSetting<T> = [T, ((value: T) => void) | null];
|
||||||
|
|
||||||
// Bus to notify other useSetting consumers when a setting is changed
|
export const getSettingKey = (name: string): string => {
|
||||||
export const settingsBus = new EventEmitter();
|
|
||||||
|
|
||||||
const getSettingKey = (name: string): string => {
|
|
||||||
return `matrix-setting-${name}`;
|
return `matrix-setting-${name}`;
|
||||||
};
|
};
|
||||||
// Like useState, but reads from and persists the value to localStorage
|
// Like useState, but reads from and persists the value to localStorage
|
||||||
const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
export const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
||||||
const key = useMemo(() => getSettingKey(name), [name]);
|
const key = useMemo(() => getSettingKey(name), [name]);
|
||||||
|
|
||||||
const [value, setValue] = useState<T>(() => {
|
const [item, setItem] = useLocalStorage(key);
|
||||||
const item = localStorage.getItem(key);
|
|
||||||
return item == null ? defaultValue : JSON.parse(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const value = useMemo(
|
||||||
settingsBus.on(name, setValue);
|
() => (item == null ? defaultValue : JSON.parse(item)),
|
||||||
return () => {
|
[item, defaultValue]
|
||||||
settingsBus.off(name, setValue);
|
);
|
||||||
};
|
const setValue = useCallback(
|
||||||
}, [name, setValue]);
|
(value: T) => {
|
||||||
|
setItem(JSON.stringify(value));
|
||||||
|
},
|
||||||
|
[setItem]
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [value, setValue];
|
||||||
value,
|
|
||||||
useCallback(
|
|
||||||
(newValue: T) => {
|
|
||||||
setValue(newValue);
|
|
||||||
localStorage.setItem(key, JSON.stringify(newValue));
|
|
||||||
settingsBus.emit(name, newValue);
|
|
||||||
},
|
|
||||||
[name, key, setValue]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSetting = <T>(name: string, defaultValue: T): T => {
|
export const getSetting = <T>(name: string, defaultValue: T): T => {
|
||||||
const item = localStorage.getItem(getSettingKey(name));
|
const item = getLocalStorageItem(getSettingKey(name));
|
||||||
return item === null ? defaultValue : JSON.parse(item);
|
return item === null ? defaultValue : JSON.parse(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSetting = <T>(name: string, newValue: T) => {
|
export const setSetting = <T>(name: string, newValue: T) =>
|
||||||
localStorage.setItem(getSettingKey(name), JSON.stringify(newValue));
|
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
|
||||||
settingsBus.emit(name, newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canEnableSpatialAudio = () => {
|
const canEnableSpatialAudio = () => {
|
||||||
const { userAgent } = navigator;
|
const { userAgent } = navigator;
|
||||||
|
|||||||
59
src/useLocalStorage.ts
Normal file
59
src/useLocalStorage.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
|
||||||
|
|
||||||
|
// Bus to notify other useLocalStorage consumers when an item is changed
|
||||||
|
export const localStorageBus = new EventEmitter();
|
||||||
|
|
||||||
|
// Like useState, but reads from and persists the value to localStorage
|
||||||
|
export const useLocalStorage = (
|
||||||
|
key: string
|
||||||
|
): [LocalStorageItem, (value: string) => void] => {
|
||||||
|
const [value, setValue] = useState<LocalStorageItem>(() =>
|
||||||
|
localStorage.getItem(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorageBus.on(key, setValue);
|
||||||
|
return () => {
|
||||||
|
localStorageBus.off(key, setValue);
|
||||||
|
};
|
||||||
|
}, [key, setValue]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
setValue(newValue);
|
||||||
|
localStorage.setItem(key, newValue);
|
||||||
|
localStorageBus.emit(key, newValue);
|
||||||
|
},
|
||||||
|
[key, setValue]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocalStorageItem = (key: string): LocalStorageItem =>
|
||||||
|
localStorage.getItem(key);
|
||||||
|
|
||||||
|
export const setLocalStorageItem = (key: string, value: string): void => {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
localStorageBus.emit(key, value);
|
||||||
|
};
|
||||||
@@ -34,7 +34,13 @@ describe("CallList", () => {
|
|||||||
|
|
||||||
it("should show room", async () => {
|
it("should show room", async () => {
|
||||||
const rooms = [
|
const rooms = [
|
||||||
{ roomName: "Room #1", roomAlias: "#room-name:server.org" },
|
{
|
||||||
|
roomName: "Room #1",
|
||||||
|
roomAlias: "#room-name:server.org",
|
||||||
|
room: {
|
||||||
|
roomId: "!roomId",
|
||||||
|
},
|
||||||
|
},
|
||||||
] as GroupCallRoom[];
|
] as GroupCallRoom[];
|
||||||
|
|
||||||
const result = renderComponent(rooms);
|
const result = renderComponent(rooms);
|
||||||
|
|||||||
Reference in New Issue
Block a user