Merge branch 'livekit' into copy-alias

This commit is contained in:
Robin Townsend
2023-07-26 10:50:29 -04:00
25 changed files with 459 additions and 145 deletions

View File

@@ -55,7 +55,7 @@
"i18next": "^21.10.0", "i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8", "i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4", "i18next-http-backend": "^1.4.4",
"livekit-client": "1.12.0", "livekit-client": "1.12.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-widget-api": "^1.3.1", "matrix-widget-api": "^1.3.1",
@@ -83,12 +83,12 @@
"@storybook/react": "^6.5.0-alpha.5", "@storybook/react": "^6.5.0-alpha.5",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@types/node": "^18.13.0",
"@types/request": "^2.48.8",
"@types/content-type": "^1.1.5", "@types/content-type": "^1.1.5",
"@types/dom-screen-wake-lock": "^1.0.1", "@types/dom-screen-wake-lock": "^1.0.1",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
"@types/node": "^18.13.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/request": "^2.48.8",
"@types/sdp-transform": "^2.4.5", "@types/sdp-transform": "^2.4.5",
"@types/uuid": "9", "@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",

View File

@@ -116,5 +116,6 @@
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>", "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Los gehts“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Los gehts“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"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>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>", "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>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen." "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen.",
"Connectivity to the server has been lost.": "Die Verbindung zum Server wurde getrennt."
} }

View File

@@ -39,6 +39,7 @@
"Download debug logs": "Download debug logs", "Download debug logs": "Download debug logs",
"Element Call Home": "Element Call Home", "Element Call Home": "Element Call Home",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.", "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
"Exit full screen": "Exit full screen", "Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback", "Feedback": "Feedback",
@@ -73,13 +74,16 @@
"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",
"Recaptcha not loaded": "Recaptcha not loaded", "Recaptcha not loaded": "Recaptcha not loaded",
"Reconnect": "Reconnect",
"Register": "Register", "Register": "Register",
"Registering…": "Registering…", "Registering…": "Registering…",
"Remove": "Remove", "Remove": "Remove",
"Retry sending logs": "Retry sending logs",
"Return to home screen": "Return to home screen", "Return to home screen": "Return to home screen",
"Select an option": "Select an option", "Select an option": "Select an option",
"Send debug logs": "Send debug logs", "Send debug logs": "Send debug logs",
@@ -99,7 +103,7 @@
"Submitting…": "Submitting…", "Submitting…": "Submitting…",
"Take me Home": "Take me Home", "Take me Home": "Take me Home",
"Thanks, we received your feedback!": "Thanks, we received your feedback!", "Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.", "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 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",
@@ -116,6 +120,7 @@
"Walkie-talkie call name": "Walkie-talkie call name", "Walkie-talkie call name": "Walkie-talkie call name",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.", "WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
"Yes, join call": "Yes, join call", "Yes, join call": "Yes, join call",
"You were disconnected from the call": "You were disconnected from the call",
"Your feedback": "Your feedback", "Your feedback": "Your feedback",
"Your recent calls": "Your recent calls" "Your recent calls": "Your recent calls"
} }

View File

@@ -116,5 +116,6 @@
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>", "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"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>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</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>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole läbivalt krüptitud." "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole läbivalt krüptitud.",
"Connectivity to the server has been lost.": "Võrguühendus serveriga on katkenud."
} }

View File

@@ -116,5 +116,6 @@
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>", "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"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>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google</2> a <6>Podmienky poskytovania služieb</6>.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (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>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google</2> a <6>Podmienky poskytovania služieb</6>.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť." "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť.",
"Connectivity to the server has been lost.": "Spojenie so serverom sa stratilo."
} }

View File

@@ -22,11 +22,10 @@ import * as Sentry from "@sentry/react";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import { LinkButton, Button } from "./button"; import { LinkButton, Button } from "./button";
import { useSubmitRageshake } from "./settings/submit-rageshake";
import { ErrorMessage } from "./input/Input";
import styles from "./FullScreenView.module.css"; import styles from "./FullScreenView.module.css";
import { translatedError, TranslatedError } from "./TranslatedError"; import { TranslatedError } from "./TranslatedError";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { RageshakeButton } from "./settings/RageshakeButton";
interface FullScreenViewProps { interface FullScreenViewProps {
className?: string; className?: string;
@@ -99,37 +98,11 @@ export function ErrorView({ error }: ErrorViewProps) {
export function CrashView() { export function CrashView() {
const { t } = useTranslation(); const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendDebugLogs = useCallback(() => {
submitRageshake({
description: "**Soft Crash**",
sendLogs: true,
});
}, [submitRageshake]);
const onReload = useCallback(() => { const onReload = useCallback(() => {
window.location.href = "/"; window.location.href = "/";
}, []); }, []);
let logsComponent: JSX.Element | null = null;
if (sent) {
logsComponent = <div>{t("Thanks! We'll get right on it.")}</div>;
} else if (sending) {
logsComponent = <div>{t("Sending…")}</div>;
} else if (Config.get().rageshake?.submit_url) {
logsComponent = (
<Button
size="lg"
variant="default"
onPress={sendDebugLogs}
className={styles.wideButton}
>
{t("Send debug logs")}
</Button>
);
}
return ( return (
<FullScreenView> <FullScreenView>
<Trans> <Trans>
@@ -141,10 +114,7 @@ export function CrashView() {
</Trans> </Trans>
)} )}
<div className={styles.sendLogsSection}>{logsComponent}</div> <RageshakeButton description="***Soft Crash***" />
{error && (
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
)}
<Button <Button
size="lg" size="lg"
variant="default" variant="default"

View File

@@ -30,6 +30,7 @@ import {
MuteMicrophoneTracker, MuteMicrophoneTracker,
UndecryptableToDeviceEventTracker, UndecryptableToDeviceEventTracker,
QualitySurveyEventTracker, QualitySurveyEventTracker,
CallDisconnectedEventTracker,
} from "./PosthogEvents"; } from "./PosthogEvents";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
@@ -437,4 +438,5 @@ export class PosthogAnalytics {
public eventMuteCamera = new MuteCameraTracker(); public eventMuteCamera = new MuteCameraTracker();
public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker(); public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker();
public eventQualitySurvey = new QualitySurveyEventTracker(); public eventQualitySurvey = new QualitySurveyEventTracker();
public eventCallDisconnected = new CallDisconnectedEventTracker();
} }

View File

@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { DisconnectReason } from "livekit-client";
import { import {
IPosthogEvent, IPosthogEvent,
PosthogAnalytics, PosthogAnalytics,
@@ -181,3 +183,17 @@ export class QualitySurveyEventTracker {
}); });
} }
} }
interface CallDisconnectedEvent {
eventName: "CallDisconnected";
reason?: DisconnectReason;
}
export class CallDisconnectedEventTracker {
track(reason?: DisconnectReason) {
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
eventName: "CallDisconnected",
reason,
});
}
}

View File

@@ -1,6 +1,13 @@
import { Room, RoomOptions, setLogLevel } from "livekit-client"; import {
E2EEOptions,
ExternalE2EEKeyProvider,
Room,
RoomOptions,
setLogLevel,
} from "livekit-client";
import { useLiveKitRoom } from "@livekit/components-react"; import { useLiveKitRoom } from "@livekit/components-react";
import { useMemo } from "react"; import { useEffect, useMemo } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { defaultLiveKitOptions } from "./options"; import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU"; import { SFUConfig } from "./openIDSFU";
@@ -15,12 +22,34 @@ export type DeviceChoices = {
enabled: boolean; enabled: boolean;
}; };
export type E2EEConfig = {
sharedKey: string;
};
setLogLevel("debug"); setLogLevel("debug");
export function useLiveKit( export function useLiveKit(
userChoices: UserChoices, userChoices: UserChoices,
sfuConfig?: SFUConfig sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig
): Room | undefined { ): Room | undefined {
const e2eeOptions = useMemo(() => {
if (!e2eeConfig?.sharedKey) return undefined;
return {
keyProvider: new ExternalE2EEKeyProvider(),
worker: new E2EEWorker(),
} as E2EEOptions;
}, [e2eeConfig]);
useEffect(() => {
if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
e2eeConfig?.sharedKey
);
}, [e2eeOptions, e2eeConfig?.sharedKey]);
const roomOptions = useMemo((): RoomOptions => { const roomOptions = useMemo((): RoomOptions => {
const options = defaultLiveKitOptions; const options = defaultLiveKitOptions;
options.videoCaptureDefaults = { options.videoCaptureDefaults = {
@@ -31,15 +60,22 @@ export function useLiveKit(
...options.audioCaptureDefaults, ...options.audioCaptureDefaults,
deviceId: userChoices.audio?.selectedId, deviceId: userChoices.audio?.selectedId,
}; };
return options;
}, [userChoices.video, userChoices.audio]);
options.e2ee = e2eeOptions;
return options;
}, [userChoices.video, userChoices.audio, e2eeOptions]);
// We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled
const roomWithoutProps = useMemo(() => new Room(roomOptions), [roomOptions]);
const { room } = useLiveKitRoom({ const { room } = useLiveKitRoom({
token: sfuConfig?.jwt, token: sfuConfig?.jwt,
serverUrl: sfuConfig?.url, serverUrl: sfuConfig?.url,
audio: userChoices.audio?.enabled ?? false, audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false, video: userChoices.video?.enabled ?? false,
options: roomOptions, room: roomWithoutProps,
}); });
return room; return room;

View File

@@ -31,6 +31,15 @@ limitations under the License.
margin-bottom: 32px; margin-bottom: 32px;
} }
.disconnectedButtons {
display: grid;
gap: 50px;
}
.rageshakeButton {
grid-column: 2;
}
.callEndedButton { .callEndedButton {
margin-top: 54px; margin-top: 54px;
margin-left: 30px; margin-left: 30px;

View File

@@ -28,15 +28,20 @@ import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { StarRatingInput } from "../input/StarRatingInput"; import { StarRatingInput } from "../input/StarRatingInput";
import { RageshakeButton } from "../settings/RageshakeButton";
export function CallEndedView({ export function CallEndedView({
client, client,
isPasswordlessUser, isPasswordlessUser,
endedCallId, endedCallId,
leaveError,
reconnect,
}: { }: {
client: MatrixClient; client: MatrixClient;
isPasswordlessUser: boolean; isPasswordlessUser: boolean;
endedCallId: string; endedCallId: string;
leaveError?: Error;
reconnect: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
@@ -76,6 +81,7 @@ export function CallEndedView({
}, },
[endedCallId, history, isPasswordlessUser, starRating] [endedCallId, history, isPasswordlessUser, starRating]
); );
const createAccountDialog = isPasswordlessUser && ( const createAccountDialog = isPasswordlessUser && (
<div className={styles.callEndedContent}> <div className={styles.callEndedContent}>
<Trans> <Trans>
@@ -138,15 +144,33 @@ export function CallEndedView({
</div> </div>
); );
const renderBody = () => {
if (leaveError) {
return (
<>
<main className={styles.main}>
<Headline className={styles.headline}>
<Trans>You were disconnected from the call</Trans>
</Headline>
<div className={styles.disconnectedButtons}>
<Button size="lg" variant="default" onClick={reconnect}>
{t("Reconnect")}
</Button>
<div className={styles.rageshakeButton}>
<RageshakeButton description="***Call disconnected***" />
</div>
</div>
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">
{t("Return to home screen")}
</Link>
</Body>
</>
);
} else {
return ( return (
<> <>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>
<main className={styles.main}> <main className={styles.main}>
<Headline className={styles.headline}> <Headline className={styles.headline}>
{surveySubmitted {surveySubmitted
@@ -168,7 +192,20 @@ export function CallEndedView({
{t("Not now, return to home screen")} {t("Not now, return to home screen")}
</Link> </Link>
</Body> </Body>
</div> </>
);
}
};
return (
<>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>{renderBody()}</div>
</> </>
); );
} }

View File

@@ -32,10 +32,17 @@ import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { UserChoices } from "../livekit/useLiveKit"; import { E2EEConfig, UserChoices } 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";
import { Config } from "../config/Config";
/**
* If there already is this many participants in the call, we automatically mute
* the user
*/
const MUTE_PARTICIPANT_COUNT = 8;
declare global { declare global {
interface Window { interface Window {
@@ -164,9 +171,12 @@ export function GroupCallView({
useSentryGroupCallHandler(groupCall); useSentryGroupCallHandler(groupCall);
const [left, setLeft] = useState(false); const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
const history = useHistory(); const history = useHistory();
const onLeave = useCallback(async () => { const onLeave = useCallback(
async (leaveError?: Error) => {
setLeaveError(leaveError);
setLeft(true); setLeft(true);
let participantCount = 0; let participantCount = 0;
@@ -199,7 +209,9 @@ export function GroupCallView({
) { ) {
history.push("/"); history.push("/");
} }
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]); },
[groupCall, leave, isPasswordlessUser, isEmbedded, history]
);
useEffect(() => { useEffect(() => {
if (widget && state === GroupCallState.Entered) { if (widget && state === GroupCallState.Entered) {
@@ -218,6 +230,21 @@ export function GroupCallView({
const [userChoices, setUserChoices] = useState<UserChoices | undefined>( const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
undefined undefined
); );
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined
);
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
groupCall.enter();
}, [groupCall]);
const livekitServiceURL =
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
if (error) { if (error) {
return <ErrorView error={error} />; return <ErrorView error={error} />;
@@ -236,6 +263,7 @@ export function GroupCallView({
unencryptedEventsFromUsers={unencryptedEventsFromUsers} unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader} hideHeader={hideHeader}
userChoices={userChoices} userChoices={userChoices}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership} otelGroupCallMembership={otelGroupCallMembership}
/> />
</OpenIDLoader> </OpenIDLoader>
@@ -249,13 +277,16 @@ export function GroupCallView({
// submitting anything. // submitting anything.
if ( if (
isPasswordlessUser || isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && !isEmbedded) (PosthogAnalytics.instance.isEnabled() && !isEmbedded) ||
leaveError
) { ) {
return ( return (
<CallEndedView <CallEndedView
endedCallId={groupCall.groupCallId} endedCallId={groupCall.groupCallId}
client={client} client={client}
isPasswordlessUser={isPasswordlessUser} isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError}
reconnect={onReconnect}
/> />
); );
} else { } else {
@@ -276,11 +307,12 @@ export function GroupCallView({
return ( return (
<LobbyView <LobbyView
matrixInfo={matrixInfo} matrixInfo={matrixInfo}
onEnter={(choices: UserChoices) => { onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => {
setUserChoices(choices); setUserChoices(choices);
setE2EEConfig(e2eeConfig);
enter(); enter();
}} }}
muteAudio={participants.size > 8} initWithMutedAudio={participants.size > MUTE_PARTICIPANT_COUNT}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
hideHeader={hideHeader} hideHeader={hideHeader}
/> />

View File

@@ -24,7 +24,7 @@ import {
} from "@livekit/components-react"; } from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays"; import { usePreventScroll } from "@react-aria/overlays";
import classNames from "classnames"; import classNames from "classnames";
import { Room, Track } from "livekit-client"; import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
@@ -33,6 +33,7 @@ import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays"; import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
import { import {
@@ -77,12 +78,13 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal"; import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; import { E2EEConfig, UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher"; import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { useFullscreen } from "./useFullscreen"; import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout"; import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader"; import { useSFUConfig } from "../livekit/OpenIDLoader";
import { E2EELock } from "../E2EELock"; import { E2EELock } from "../E2EELock";
import { useEventEmitterThree } from "../useEvents";
import { useWakeLock } from "../useWakeLock"; import { useWakeLock } from "../useWakeLock";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -93,16 +95,25 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> { export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
userChoices: UserChoices; userChoices: UserChoices;
e2eeConfig?: E2EEConfig;
} }
export function ActiveCall(props: ActiveCallProps) { export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useSFUConfig(); const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.userChoices, sfuConfig); const livekitRoom = useLiveKit(
props.userChoices,
sfuConfig,
props.e2eeConfig
);
if (!livekitRoom) { if (!livekitRoom) {
return null; return null;
} }
if (props.e2eeConfig && !livekitRoom.isE2EEEnabled) {
livekitRoom.setE2EEEnabled(!!props.e2eeConfig);
}
return ( return (
<RoomContext.Provider value={livekitRoom}> <RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} /> <InCallView {...props} livekitRoom={livekitRoom} />
@@ -115,7 +126,7 @@ export interface InCallViewProps {
groupCall: GroupCall; groupCall: GroupCall;
livekitRoom: Room; livekitRoom: Room;
participants: Map<RoomMember, Map<string, ParticipantInfo>>; participants: Map<RoomMember, Map<string, ParticipantInfo>>;
onLeave: () => void; onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean; hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership; otelGroupCallMembership?: OTelGroupCallMembership;
@@ -190,6 +201,23 @@ export function InCallView({
async (muted) => await localParticipant.setMicrophoneEnabled(!muted) async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
); );
const onDisconnected = useCallback(
(reason?: DisconnectReason) => {
PosthogAnalytics.instance.eventCallDisconnected.track(reason);
logger.info("Disconnected from livekit call with reason ", reason);
onLeave(
new Error("Disconnected from LiveKit call with reason " + reason)
);
},
[onLeave]
);
const onLeavePress = useCallback(() => {
onLeave();
}, [onLeave]);
useEventEmitterThree(livekitRoom, RoomEvent.Disconnected, onDisconnected);
useEffect(() => { useEffect(() => {
widget?.api.transport.send( widget?.api.transport.send(
layout === "freedom" layout === "freedom"
@@ -386,7 +414,7 @@ export function InCallView({
} }
buttons.push( buttons.push(
<HangupButton key="6" onPress={onLeave} data-testid="incall_leave" /> <HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
); );
footer = <div className={styles.footer}>{buttons}</div>; footer = <div className={styles.footer}>{buttons}</div>;
} }

View File

@@ -66,3 +66,9 @@ limitations under the License.
.copyButton:last-child { .copyButton:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.passwordField {
width: 320px !important;
margin-bottom: 20px;
flex: 0;
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState, useCallback, ChangeEvent } 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";
@@ -25,21 +25,25 @@ 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 { UserChoices } from "../livekit/useLiveKit"; import { E2EEConfig, UserChoices } from "../livekit/useLiveKit";
import { InputField } from "../input/Input";
import { useEnableE2EE } from "../settings/useSetting";
interface Props { interface Props {
matrixInfo: MatrixInfo; matrixInfo: MatrixInfo;
onEnter: (userChoices: UserChoices) => void; onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void;
isEmbedded: boolean; isEmbedded: boolean;
hideHeader: boolean; hideHeader: boolean;
muteAudio: boolean; initWithMutedAudio: boolean;
} }
export function LobbyView(props: Props) { export function LobbyView(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
useLocationNavigation(); useLocationNavigation();
const [enableE2EE] = useEnableE2EE();
const joinCallButtonRef = useRef<HTMLButtonElement>(null); const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => { useEffect(() => {
if (joinCallButtonRef.current) { if (joinCallButtonRef.current) {
@@ -50,6 +54,17 @@ export function LobbyView(props: Props) {
const [userChoices, setUserChoices] = useState<UserChoices | undefined>( const [userChoices, setUserChoices] = useState<UserChoices | undefined>(
undefined undefined
); );
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}>
@@ -67,15 +82,29 @@ export function LobbyView(props: Props) {
<div className={styles.joinRoomContent}> <div className={styles.joinRoomContent}>
<VideoPreview <VideoPreview
matrixInfo={props.matrixInfo} matrixInfo={props.matrixInfo}
muteAudio={props.muteAudio} initWithMutedAudio={props.initWithMutedAudio}
onUserChoicesChanged={setUserChoices} onUserChoicesChanged={setUserChoices}
/> />
{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={() => props.onEnter(userChoices!)} onPress={() =>
props.onEnter(
userChoices!,
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
)
}
data-testid="lobby_joinCall" data-testid="lobby_joinCall"
> >
Join call now Join call now

View File

@@ -41,13 +41,13 @@ export type MatrixInfo = {
interface Props { interface Props {
matrixInfo: MatrixInfo; matrixInfo: MatrixInfo;
muteAudio: boolean; initWithMutedAudio: boolean;
onUserChoicesChanged: (choices: UserChoices) => void; onUserChoicesChanged: (choices: UserChoices) => void;
} }
export function VideoPreview({ export function VideoPreview({
matrixInfo, matrixInfo,
muteAudio, initWithMutedAudio,
onUserChoicesChanged, onUserChoicesChanged,
}: Props) { }: Props) {
const { client } = useClient(); const { client } = useClient();
@@ -70,13 +70,9 @@ export function VideoPreview({
// Create local media tracks. // Create local media tracks.
const [videoEnabled, setVideoEnabled] = useState<boolean>(true); const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
const [audioEnabled, setAudioEnabled] = useState<boolean>(!muteAudio); const [audioEnabled, setAudioEnabled] = useState<boolean>(
!initWithMutedAudio
useEffect(() => { );
if (muteAudio) {
setAudioEnabled(false);
}
}, [muteAudio]);
// The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value. // The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value.
// Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks. // Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks.

View File

@@ -171,7 +171,7 @@ export function useGroupCall(
isScreensharing: false, isScreensharing: false,
screenshareFeeds: [], screenshareFeeds: [],
requestingScreenshare: false, requestingScreenshare: false,
participants: new Map(), participants: getParticipants(groupCall),
hasLocalParticipant: false, hasLocalParticipant: false,
}); });

View File

@@ -0,0 +1,21 @@
/*
Copyright 2022 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.
*/
.rageshakeControl {
height: 50px;
text-align: center;
vertical-align: middle;
}

View File

@@ -0,0 +1,67 @@
/*
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 { useTranslation } from "react-i18next";
import { useCallback } from "react";
import { Button } from "../button";
import { Config } from "../config/Config";
import styles from "./RageshakeButton.module.css";
import { useSubmitRageshake } from "./submit-rageshake";
interface Props {
description: string;
}
export const RageshakeButton = ({ description }: Props) => {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const { t } = useTranslation();
const sendDebugLogs = useCallback(() => {
submitRageshake({
description,
sendLogs: true,
});
}, [submitRageshake, description]);
if (!Config.get().rageshake?.submit_url) return null;
let logsComponent: JSX.Element | null = null;
if (sending) {
logsComponent = <span>{t("Sending…")}</span>;
} else if (sent) {
logsComponent = <div>{t("Thanks!")}</div>;
} else {
let caption = t("Send debug logs");
if (error) {
caption = t("Retry sending logs");
}
logsComponent = (
<Button
size="lg"
variant="default"
onPress={sendDebugLogs}
className={styles.wideButton}
disabled={sending}
>
{caption}
</Button>
);
}
return <div className={styles.rageshakeControl}>{logsComponent}</div>;
};

View File

@@ -34,6 +34,7 @@ import {
useOptInAnalytics, useOptInAnalytics,
useDeveloperSettingsTab, useDeveloperSettingsTab,
useShowConnectionStats, useShowConnectionStats,
useEnableE2EE,
} from "./useSetting"; } from "./useSetting";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button"; import { Button } from "../button";
@@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => {
useDeveloperSettingsTab(); useDeveloperSettingsTab();
const [showConnectionStats, setShowConnectionStats] = const [showConnectionStats, setShowConnectionStats] =
useShowConnectionStats(); useShowConnectionStats();
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
const downloadDebugLog = useDownloadDebugLog(); const downloadDebugLog = useDownloadDebugLog();
@@ -255,6 +257,18 @@ export const SettingsModal = (props: Props) => {
} }
/> />
</FieldRow> </FieldRow>
<FieldRow>
<InputField
id="enableE2EE"
name="end-to-end-encryption"
label={t("Enable end-to-end encryption (password protected calls)")}
type="checkbox"
checked={enableE2EE}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEnableE2EE(e.target.checked)
}
/>
</FieldRow>
<FieldRow> <FieldRow>
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button> <Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
</FieldRow> </FieldRow>

View File

@@ -104,6 +104,9 @@ export const useDeveloperSettingsTab = () =>
export const useShowConnectionStats = () => export const useShowConnectionStats = () =>
useSetting("show-connection-stats", false); useSetting("show-connection-stats", false);
export const useEnableE2EE = () =>
useSetting("enable-end-to-end-encryption", false);
export const useDefaultDevices = () => export const useDefaultDevices = () =>
useSetting("defaultDevices", { useSetting("defaultDevices", {
audioinput: "", audioinput: "",

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/ */
import { useEffect } from "react"; import { useEffect } from "react";
import EventEmitter from "eventemitter3";
import type { import type {
Listener, Listener,
@@ -59,3 +60,20 @@ export const useTypedEventEmitter = <
}; };
}, [emitter, eventType, listener]); }, [emitter, eventType, listener]);
}; };
// Shortcut for registering a listener on an eventemitter3 EventEmitter (ie. what the LiveKit SDK uses)
export const useEventEmitterThree = <
EventType extends EventEmitter.ValidEventTypes,
T extends EventEmitter.EventNames<EventType>
>(
emitter: EventEmitter<EventType>,
eventType: T,
listener: EventEmitter.EventListener<EventType, T>
) => {
useEffect(() => {
emitter.on(eventType, listener);
return () => {
emitter.off(eventType, listener);
};
}, [emitter, eventType, listener]);
};

View File

@@ -121,6 +121,15 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
const toolbarButtons: JSX.Element[] = []; const toolbarButtons: JSX.Element[] = [];
if (!sfuParticipant.isLocal) { if (!sfuParticipant.isLocal) {
toolbarButtons.push(
<AudioButton
key="localVolume"
className={styles.button}
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
onPress={onOptionsPress}
/>
);
if (content === TileContent.ScreenShare) { if (content === TileContent.ScreenShare) {
toolbarButtons.push( toolbarButtons.push(
<FullscreenButton <FullscreenButton
@@ -130,16 +139,6 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
onPress={onFullscreen} onPress={onFullscreen}
/> />
); );
} else {
// Due to the LK SDK this sadly only works for user-media atm
toolbarButtons.push(
<AudioButton
key="localVolume"
className={styles.button}
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
onPress={onOptionsPress}
/>
);
} }
} }

View File

@@ -16,29 +16,36 @@ limitations under the License.
import React, { ChangeEvent, useState } from "react"; import React, { ChangeEvent, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { RemoteParticipant } from "livekit-client"; import { RemoteParticipant, Track } from "livekit-client";
import { FieldRow } from "../input/Input"; import { FieldRow } from "../input/Input";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import styles from "./VideoTileSettingsModal.module.css"; import styles from "./VideoTileSettingsModal.module.css";
import { VolumeIcon } from "../button/VolumeIcon"; import { VolumeIcon } from "../button/VolumeIcon";
import { ItemData } from "./VideoTile"; import { ItemData, TileContent } from "./VideoTile";
interface LocalVolumeProps { interface LocalVolumeProps {
participant: RemoteParticipant; participant: RemoteParticipant;
content: TileContent;
} }
const LocalVolume: React.FC<LocalVolumeProps> = ({ const LocalVolume: React.FC<LocalVolumeProps> = ({
participant, participant,
content,
}: LocalVolumeProps) => { }: LocalVolumeProps) => {
const source =
content === TileContent.UserMedia
? Track.Source.Microphone
: Track.Source.ScreenShareAudio;
const [localVolume, setLocalVolume] = useState<number>( const [localVolume, setLocalVolume] = useState<number>(
participant.getVolume() ?? 0 participant.getVolume(source) ?? 0
); );
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => { const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => {
const value: number = +event.target.value; const value: number = +event.target.value;
setLocalVolume(value); setLocalVolume(value);
participant.setVolume(value); participant.setVolume(value, source);
}; };
return ( return (
@@ -78,7 +85,10 @@ export const VideoTileSettingsModal = ({ data, onClose, ...rest }: Props) => {
{...rest} {...rest}
> >
<div className={styles.content}> <div className={styles.content}>
<LocalVolume participant={data.sfuParticipant as RemoteParticipant} /> <LocalVolume
participant={data.sfuParticipant as RemoteParticipant}
content={data.content}
/>
</div> </div>
</Modal> </Modal>
); );

View File

@@ -2217,7 +2217,7 @@
"@react-hook/latest" "^1.0.3" "@react-hook/latest" "^1.0.3"
clsx "^1.2.1" clsx "^1.2.1"
"@matrix-org/matrix-sdk-crypto-js@^0.1.0": "@matrix-org/matrix-sdk-crypto-js@^0.1.1":
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.4.tgz#c13c7c8c3a1d8da08e6ad195d25e5e61cc402df7" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.4.tgz#c13c7c8c3a1d8da08e6ad195d25e5e61cc402df7"
integrity sha512-OxG84iSeR89zYLFeb+DCaFtZT+DDiIu+kTkqY8OYfhE5vpGLFX2sDVBRrAdos1IUqEoboDloDBR9+yU7hNRyog== integrity sha512-OxG84iSeR89zYLFeb+DCaFtZT+DDiIu+kTkqY8OYfhE5vpGLFX2sDVBRrAdos1IUqEoboDloDBR9+yU7hNRyog==
@@ -6450,6 +6450,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0" randombytes "^2.0.0"
randomfill "^1.0.3" randomfill "^1.0.3"
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
css-blank-pseudo@^3.0.3: css-blank-pseudo@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561"
@@ -10751,10 +10756,10 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
livekit-client@1.12.0: livekit-client@1.12.1:
version "1.12.0" version "1.12.1"
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-1.12.0.tgz#4b4f18087331d4893adaccb148e33fe870eb9a2e" resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-1.12.1.tgz#b927b4fb07d2d64543d25a99db36ffbb7caa23e6"
integrity sha512-G1KHNMSaEMXtPIKxTQt+WH/uRvhkQ0tQaZ5Kkem2CvB/5I4KPrV4DSmhBKP1P+8reHcaBBiye8V9bfpD69aHyQ== integrity sha512-/mob04a/Mb0D+4sIzB7/pqakpJMCORSK+Qu5oTIcuSpgL+eBYGzHPE2sutGCGoe3Ns9sITAqUTyiui5+GN3i2w==
dependencies: dependencies:
eventemitter3 "^5.0.1" eventemitter3 "^5.0.1"
loglevel "^1.8.0" loglevel "^1.8.0"
@@ -11005,7 +11010,7 @@ matrix-events-sdk@0.0.1:
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0" "@matrix-org/matrix-sdk-crypto-js" "^0.1.1"
another-json "^0.2.0" another-json "^0.2.0"
bs58 "^5.0.0" bs58 "^5.0.0"
content-type "^1.0.4" content-type "^1.0.4"
@@ -11700,6 +11705,14 @@ objectorarray@^1.0.5:
resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5"
integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==
oidc-client-ts@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-2.2.4.tgz#7d86b5efe2248f3637a6f3a0ee1af86764aea125"
integrity sha512-nOZwIomju+AmXObl5Oq5PjrES/qTt8bLsENJCIydVgi9TEWk7SCkOU6X3RNkY7yfySRM1OJJvDKdREZdmnDT2g==
dependencies:
crypto-js "^4.1.1"
jwt-decode "^3.1.2"
on-finished@2.4.1: on-finished@2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"