Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession

This commit is contained in:
David Baker
2023-08-16 18:53:00 +01:00
20 changed files with 334 additions and 150 deletions

View File

@@ -67,7 +67,7 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
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_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}

View File

@@ -75,7 +75,6 @@
"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>",
"Password": "Password",
"Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)",
"Passwords must match": "Passwords must match",
"Profile": "Profile",
"Recaptcha dismissed": "Recaptcha dismissed",
@@ -106,6 +105,7 @@
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks!": "Thanks!",
"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>",
"Turn off camera": "Turn off camera",
"Turn on camera": "Turn on camera",

View File

@@ -19,8 +19,12 @@ import { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => {
const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null;
return (
<Banner>
<div className={styles.e2eeBanner}>

View File

@@ -26,10 +26,7 @@ import { TooltipTrigger } from "./Tooltip";
export const E2EELock = () => {
const { t } = useTranslation();
const tooltip = useCallback(
() =>
t(
"Element Call is temporarily not end-to-end encrypted while we test scalability."
),
() => t("This call is not end-to-end encrypted."),
[t]
);

View File

@@ -19,6 +19,8 @@ import { useLocation } from "react-router-dom";
import { Config } from "./config/Config";
export const PASSWORD_STRING = "password=";
interface UrlParams {
roomAlias: string | null;
roomId: string | null;
@@ -86,6 +88,10 @@ interface UrlParams {
* user's homeserver doesn't provide any.
*/
allowIceFallback: boolean;
/**
* E2EE password
*/
password: string | null;
}
/**
@@ -102,9 +108,12 @@ export const getUrlParams = (
pathname = window.location.pathname,
hash = window.location.hash
): UrlParams => {
// This is legacy code - we're moving away from using aliases
let roomAlias: string | null = null;
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 "/"
// Delete "/room/", if present
@@ -119,17 +128,17 @@ export const getUrlParams = (
roomAlias = hash;
}
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
// Delete "?" and what comes afterwards
roomAlias = roomAlias.split("?")[0];
// Make roomAlias undefined, if empty
if (roomAlias.length <= 1) {
// Make roomAlias is null, if it only is a "#"
roomAlias = null;
} else {
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
}
}
@@ -164,6 +173,7 @@ export const getUrlParams = (
return {
roomAlias,
roomId,
password: getParam("password"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),

View File

@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk";
import { Buffer } from "buffer";
import { widget } from "../widget";
import { getSetting, setSetting, settingsBus } from "../settings/useSetting";
import { getSetting, setSetting, getSettingKey } from "../settings/useSetting";
import {
CallEndedTracker,
CallStartedTracker,
@@ -34,6 +34,7 @@ import {
} from "./PosthogEvents";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
import { localStorageBus } from "../useLocalStorage";
/* Posthog analytics tracking.
*
@@ -413,7 +414,7 @@ export class PosthogAnalytics {
// * When the user changes their preferences on this device
// 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)
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
localStorageBus.on(getSettingKey("opt-in-analytics"), (optInAnalytics) => {
this.updateAnonymityAndIdentifyUser(optInAnalytics);
});
}

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

View File

@@ -23,8 +23,9 @@ import { Facepile } from "../Facepile";
import { Avatar, Size } from "../Avatar";
import styles from "./CallList.module.css";
import { getRoomUrl } from "../matrix-utils";
import { Body, Caption } from "../typography/Typography";
import { Body } from "../typography/Typography";
import { GroupCallRoom } from "./useGroupCallRooms";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface CallListProps {
rooms: GroupCallRoom[];
@@ -35,13 +36,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
return (
<>
<div className={styles.callList}>
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
{rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => (
<CallTile
key={roomAlias}
client={client}
name={roomName}
avatarUrl={avatarUrl}
roomAlias={roomAlias}
roomId={room.roomId}
participants={participants}
disableFacepile={disableFacepile}
/>
@@ -59,7 +60,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
interface CallTileProps {
name: string;
avatarUrl: string;
roomAlias: string;
roomId: string;
participants: RoomMember[];
client: MatrixClient;
disableFacepile?: boolean;
@@ -67,17 +68,16 @@ interface CallTileProps {
function CallTile({
name,
avatarUrl,
roomAlias,
roomId,
participants,
client,
disableFacepile,
}: CallTileProps) {
const roomSharedKey = useRoomSharedKey(roomId);
return (
<div className={styles.callTile}>
<Link
to={`/${roomAlias.substring(1).split(":")[0]}`}
className={styles.callTileLink}
>
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
<Avatar
size={Size.LG}
bgKey={name}
@@ -89,7 +89,6 @@ function CallTile({
<Body overflowEllipsis fontWeight="semiBold">
{name}
</Body>
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
{participants && !disableFacepile && (
<Facepile
className={styles.facePile}
@@ -103,7 +102,7 @@ function CallTile({
<CopyButton
className={styles.copyButton}
variant="icon"
value={getRoomUrl(roomAlias)}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
/>
</div>
);

View File

@@ -17,6 +17,7 @@ limitations under the License.
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import {
@@ -37,9 +38,11 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption, Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
import { setLocalStorageItem } from "../useLocalStorage";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
interface Props {
client: MatrixClient;
@@ -54,6 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {
@@ -70,16 +74,23 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
setError(undefined);
setLoading(true);
const [roomAlias] = await createRoom(client, roomName, ptt);
const roomId = (
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
)[1];
if (roomAlias) {
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
history.push(`/room/#?roomId=${roomId}`);
}
submit().catch((error) => {
if (error.errcode === "M_ROOM_IN_USE") {
setExistingRoomId(roomAliasLocalpartFromRoomName(roomName));
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
setLoading(false);
setError(undefined);
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 [existingRoomId, setExistingRoomId] = useState<string>();
const [existingAlias, setExistingAlias] = useState<string>();
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
history.push(`/${existingAlias}`);
}, [history, existingAlias]);
const callNameLabel =
callType === CallType.Video

View File

@@ -40,9 +40,11 @@ import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
import { setLocalStorageItem } from "../useLocalStorage";
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
@@ -58,6 +60,8 @@ export const UnauthenticatedView: FC = () => {
const history = useHistory();
const { t } = useTranslation();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {
e.preventDefault();
@@ -79,9 +83,18 @@ export const UnauthenticatedView: FC = () => {
true
);
let roomAlias: string;
let roomId: string;
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) {
if (!setClient) {
throw error;
@@ -110,7 +123,7 @@ export const UnauthenticatedView: FC = () => {
}
setClient({ client, session });
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
history.push(`/room/#?roomId=${roomId}`);
}
submit().catch((error) => {
@@ -120,7 +133,16 @@ export const UnauthenticatedView: FC = () => {
reset();
});
},
[register, reset, execute, history, callType, modalState, setClient]
[
register,
reset,
execute,
history,
callType,
modalState,
setClient,
e2eeEnabled,
]
);
const callNameLabel =

View File

@@ -89,8 +89,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const groupCalls = client.groupCallEventHandler.groupCalls.values();
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, filteredRooms);
const sortedRooms = sortRooms(client, rooms);
const items = sortedRooms.map((room) => {
const groupCall = client.getGroupCallForRoom(room.roomId)!;

View File

@@ -32,7 +32,7 @@ import {
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 } from "./UrlParams";
import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
import { loadOlm } from "./olm";
import { Config } from "./config/Config";
@@ -272,14 +272,15 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
export async function createRoom(
client: MatrixClient,
name: string,
ptt: boolean
ptt: boolean,
e2ee: boolean
): Promise<[string, string]> {
logger.log(`Creating room for group call`);
const createPromise = client.createRoom({
visibility: Visibility.Private,
preset: Preset.PublicChat,
name,
room_alias_name: roomAliasLocalpartFromRoomName(name),
room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name),
power_level_content_override: {
invite: 100,
kick: 100,
@@ -341,15 +342,16 @@ export async function createRoom(
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 {
if (roomIdOrAlias.startsWith("#")) {
return `${window.location.protocol}//${window.location.host}/${
roomIdOrAlias.substring(1).split(":")[0]
}`;
} else {
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
}
/**
* Returns a URL to that will load Element Call with the given room
* @param roomId of the room
* @param password
* @returns
*/
export function getRoomUrl(roomId: string, password?: string): string {
return `${window.location.protocol}//${
window.location.host
}/room/#?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`;
}
export function getAvatarUrl(

View File

@@ -31,7 +31,6 @@ import { MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { E2EEConfig } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
//import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
@@ -41,6 +40,11 @@ import { LivekitFocus } from "../livekit/LivekitFocus";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import {
useManageRoomSharedKey,
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
declare global {
interface Window {
@@ -78,6 +82,9 @@ export function GroupCallView({
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
const { t } = useTranslation();
useEffect(() => {
@@ -243,8 +250,11 @@ export function GroupCallView({
}
}, [isJoined, rtcSession]);
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined
const [e2eeEnabled] = useEnableE2EE();
const e2eeConfig = useMemo(
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
[e2eeSharedKey]
);
const onReconnect = useCallback(() => {
@@ -266,6 +276,22 @@ export function GroupCallView({
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) {
return (
/*<OpenIDLoader
@@ -325,10 +351,7 @@ export function GroupCallView({
<LobbyView
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={(e2eeConfig?: E2EEConfig) => {
setE2EEConfig(e2eeConfig);
enterRTCSession(rtcSession);
}}
onEnter={() => enter()}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>

View File

@@ -78,6 +78,7 @@ import { useEventEmitterThree } from "../useEvents";
import { useWakeLock } from "../useWakeLock";
import { useMergedRefs } from "../useMergedRefs";
import { MuteStates } from "./MuteStates";
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -133,6 +134,8 @@ export function InCallView({
usePreventScroll();
useWakeLock();
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
const boundsValid = bounds.height > 0;
@@ -408,7 +411,7 @@ export function InCallView({
<Header>
<LeftNav>
<RoomHeaderInfo roomName={rtcSession.room.name} />
<E2EELock />
{!isRoomE2EE && <E2EELock />}
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
@@ -445,12 +448,7 @@ export function InCallView({
/>
)}
{inviteModalState.isOpen && (
<InviteModal
roomIdOrAlias={
rtcSession.room.getCanonicalAlias() ?? rtcSession.room.roomId
}
{...inviteModalProps}
/>
<InviteModal roomId={rtcSession.room.roomId} {...inviteModalProps} />
)}
</div>
);

View File

@@ -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");
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 { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
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 roomSharedKey = useRoomSharedKey(roomId);
return (
<Modal
@@ -40,7 +42,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
data-testid="modal_inviteLink"
/>
</ModalContent>

View File

@@ -14,14 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
useRef,
useEffect,
useState,
useCallback,
ChangeEvent,
FC,
} from "react";
import { useRef, useEffect, FC } from "react";
import { Trans, useTranslation } from "react-i18next";
import styles from "./LobbyView.module.css";
@@ -32,15 +25,13 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
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 { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props {
matrixInfo: MatrixInfo;
muteStates: MuteStates;
onEnter: (e2eeConfig?: E2EEConfig) => void;
onEnter: () => void;
isEmbedded: boolean;
hideHeader: boolean;
}
@@ -53,10 +44,9 @@ export const LobbyView: FC<Props> = ({
hideHeader,
}) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
useLocationNavigation();
const [enableE2EE] = useEnableE2EE();
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
@@ -64,18 +54,6 @@ export const LobbyView: FC<Props> = ({
}
}, [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 (
<div className={styles.room}>
{!hideHeader && (
@@ -91,25 +69,12 @@ export const LobbyView: FC<Props> = ({
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
<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>
<Button
ref={joinCallButtonRef}
className={styles.copyButton}
size="lg"
onPress={() =>
onEnter(
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
)
}
onPress={() => onEnter()}
data-testid="lobby_joinCall"
>
Join call now
@@ -117,7 +82,7 @@ export const LobbyView: FC<Props> = ({
<Body>Or</Body>
<CopyButton
variant="secondaryCopy"
value={getRoomUrl(matrixInfo.roomAlias ?? matrixInfo.roomId)}
value={getRoomUrl(matrixInfo.roomId, roomSharedKey ?? undefined)}
className={styles.copyButton}
copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink"

View File

@@ -20,10 +20,14 @@ 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 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";
@@ -58,6 +62,8 @@ export const useLoadGroupCall = (
const { t } = useTranslation();
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
const [e2eeEnabled] = useEnableE2EE();
useEffect(() => {
const fetchOrCreateRoom = async (): Promise<Room> => {
try {
@@ -95,8 +101,17 @@ export const useLoadGroupCall = (
const [, roomId] = await createRoom(
client,
roomNameFromRoomId(roomIdOrAlias),
createPtt
createPtt,
e2eeEnabled ?? false
);
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
// likewise, wait for the room
await client.waitUntilRoomReadyForGroupCalls(roomId);
return client.getRoom(roomId)!;
@@ -136,7 +151,7 @@ export const useLoadGroupCall = (
.then(fetchOrCreateGroupCall)
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
.catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
return state;
};

View File

@@ -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");
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.
*/
import { EventEmitter } from "events";
import { useMemo, useState, useEffect, useCallback } from "react";
import { useCallback, useMemo } from "react";
import { isE2EESupported } from "livekit-client";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import {
getLocalStorageItem,
setLocalStorageItem,
useLocalStorage,
} from "../useLocalStorage";
type Setting<T> = [T, (value: T) => void];
type DisableableSetting<T> = [T, ((value: T) => void) | null];
// Bus to notify other useSetting consumers when a setting is changed
export const settingsBus = new EventEmitter();
const getSettingKey = (name: string): string => {
export const getSettingKey = (name: string): string => {
return `matrix-setting-${name}`;
};
// 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 [value, setValue] = useState<T>(() => {
const item = localStorage.getItem(key);
return item == null ? defaultValue : JSON.parse(item);
});
const [item, setItem] = useLocalStorage(key);
useEffect(() => {
settingsBus.on(name, setValue);
return () => {
settingsBus.off(name, setValue);
};
}, [name, setValue]);
const value = useMemo(
() => (item == null ? defaultValue : JSON.parse(item)),
[item, defaultValue]
);
const setValue = useCallback(
(value: T) => {
setItem(JSON.stringify(value));
},
[setItem]
);
return [
value,
useCallback(
(newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
settingsBus.emit(name, newValue);
},
[name, key, setValue]
),
];
return [value, setValue];
};
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);
};
export const setSetting = <T>(name: string, newValue: T) => {
localStorage.setItem(getSettingKey(name), JSON.stringify(newValue));
settingsBus.emit(name, newValue);
};
export const setSetting = <T>(name: string, newValue: T) =>
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
const canEnableSpatialAudio = () => {
const { userAgent } = navigator;

59
src/useLocalStorage.ts Normal file
View 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);
};

View File

@@ -34,7 +34,13 @@ describe("CallList", () => {
it("should show room", async () => {
const rooms = [
{ roomName: "Room #1", roomAlias: "#room-name:server.org" },
{
roomName: "Room #1",
roomAlias: "#room-name:server.org",
room: {
roomId: "!roomId",
},
},
] as GroupCallRoom[];
const result = renderComponent(rooms);