diff --git a/package.json b/package.json index 5934f89f..74f15cd8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "i18next": "^21.10.0", "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", - "livekit-client": "1.12.0", + "livekit-client": "1.12.1", "lodash": "^4.17.21", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9", "matrix-widget-api": "^1.3.1", @@ -83,12 +83,12 @@ "@storybook/react": "^6.5.0-alpha.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", - "@types/node": "^18.13.0", - "@types/request": "^2.48.8", "@types/content-type": "^1.1.5", "@types/dom-screen-wake-lock": "^1.0.1", "@types/grecaptcha": "^3.0.4", + "@types/node": "^18.13.0", "@types/react-router-dom": "^5.3.3", + "@types/request": "^2.48.8", "@types/sdp-transform": "^2.4.5", "@types/uuid": "9", "@typescript-eslint/eslint-plugin": "^6.1.0", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 99866dc5..60d68ea1 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -116,5 +116,6 @@ "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Mit einem Klick auf „Los geht’s“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung und <6>Nutzungsbedingungen. <9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", - "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." } diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index d697a70d..34d84534 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -39,6 +39,7 @@ "Download debug logs": "Download debug logs", "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.", + "Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Feedback": "Feedback", @@ -73,13 +74,16 @@ "Not registered yet? <2>Create an account": "Not registered yet? <2>Create an account", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}", "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", "Recaptcha not loaded": "Recaptcha not loaded", + "Reconnect": "Reconnect", "Register": "Register", "Registering…": "Registering…", "Remove": "Remove", + "Retry sending logs": "Retry sending logs", "Return to home screen": "Return to home screen", "Select an option": "Select an option", "Send debug logs": "Send debug logs", @@ -99,7 +103,7 @@ "Submitting…": "Submitting…", "Take me Home": "Take me Home", "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 site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)", "Turn off camera": "Turn off camera", @@ -116,6 +120,7 @@ "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.", "Yes, join call": "Yes, join call", + "You were disconnected from the call": "You were disconnected from the call", "Your feedback": "Your feedback", "Your recent calls": "Your recent calls" } diff --git a/public/locales/et/app.json b/public/locales/et/app.json index d8659dbf..8d934ccc 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -116,5 +116,6 @@ "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)", "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika ning <6>Teenusetingimused.<9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega", - "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." } diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 6f588a91..52668084 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -116,5 +116,6 @@ "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "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)", "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google a <6>Podmienky poskytovania služieb.<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)", - "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." } diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index cdbc0297..4b9a1e2b 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -22,11 +22,10 @@ import * as Sentry from "@sentry/react"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; import { LinkButton, Button } from "./button"; -import { useSubmitRageshake } from "./settings/submit-rageshake"; -import { ErrorMessage } from "./input/Input"; import styles from "./FullScreenView.module.css"; -import { translatedError, TranslatedError } from "./TranslatedError"; +import { TranslatedError } from "./TranslatedError"; import { Config } from "./config/Config"; +import { RageshakeButton } from "./settings/RageshakeButton"; interface FullScreenViewProps { className?: string; @@ -99,37 +98,11 @@ export function ErrorView({ error }: ErrorViewProps) { export function CrashView() { const { t } = useTranslation(); - const { submitRageshake, sending, sent, error } = useSubmitRageshake(); - - const sendDebugLogs = useCallback(() => { - submitRageshake({ - description: "**Soft Crash**", - sendLogs: true, - }); - }, [submitRageshake]); const onReload = useCallback(() => { window.location.href = "/"; }, []); - let logsComponent: JSX.Element | null = null; - if (sent) { - logsComponent =
{t("Thanks! We'll get right on it.")}
; - } else if (sending) { - logsComponent =
{t("Sending…")}
; - } else if (Config.get().rageshake?.submit_url) { - logsComponent = ( - - ); - } - return ( @@ -141,10 +114,7 @@ export function CrashView() { )} -
{logsComponent}
- {error && ( - - )} + +
+ +
+ + + + + {t("Return to home screen")} + + + + ); + } else { + return ( + <> +
+ + {surveySubmitted + ? t("{{displayName}}, your call has ended.", { + displayName, + }) + : t("{{displayName}}, your call has ended.", { + displayName, + }) + + "\n" + + t("How did it go?")} + + {!surveySubmitted && PosthogAnalytics.instance.isEnabled() + ? qualitySurveyDialog + : createAccountDialog} +
+ + + {t("Not now, return to home screen")} + + + + ); + } + }; + return ( <>
@@ -146,29 +205,7 @@ export function CallEndedView({
-
-
- - {surveySubmitted - ? t("{{displayName}}, your call has ended.", { - displayName, - }) - : t("{{displayName}}, your call has ended.", { - displayName, - }) + - "\n" + - t("How did it go?")} - - {!surveySubmitted && PosthogAnalytics.instance.isEnabled() - ? qualitySurveyDialog - : createAccountDialog} -
- - - {t("Not now, return to home screen")} - - -
+
{renderBody()}
); } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index bd222c93..9181e4eb 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -32,10 +32,17 @@ import { CallEndedView } from "./CallEndedView"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useProfile } from "../profile/useProfile"; -import { UserChoices } from "../livekit/useLiveKit"; +import { E2EEConfig, UserChoices } from "../livekit/useLiveKit"; import { findDeviceByName } from "../media-utils"; import { OpenIDLoader } from "../livekit/OpenIDLoader"; 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 { interface Window { @@ -164,42 +171,47 @@ export function GroupCallView({ useSentryGroupCallHandler(groupCall); const [left, setLeft] = useState(false); + const [leaveError, setLeaveError] = useState(undefined); const history = useHistory(); - const onLeave = useCallback(async () => { - setLeft(true); + const onLeave = useCallback( + async (leaveError?: Error) => { + setLeaveError(leaveError); + setLeft(true); - let participantCount = 0; - for (const deviceMap of groupCall.participants.values()) { - participantCount += deviceMap.size; - } + let participantCount = 0; + for (const deviceMap of groupCall.participants.values()) { + participantCount += deviceMap.size; + } - // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, - // therefore we want the event to be sent instantly without getting queued/batched. - const sendInstantly = !!widget; - PosthogAnalytics.instance.eventCallEnded.track( - groupCall.groupCallId, - participantCount, - sendInstantly - ); + // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, + // therefore we want the event to be sent instantly without getting queued/batched. + const sendInstantly = !!widget; + PosthogAnalytics.instance.eventCallEnded.track( + groupCall.groupCallId, + participantCount, + sendInstantly + ); - leave(); - if (widget) { - // we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked. - await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms - widget.api.setAlwaysOnScreen(false); - PosthogAnalytics.instance.logout(); - widget.api.transport.send(ElementWidgetActions.HangupCall, {}); - } + leave(); + if (widget) { + // we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked. + await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms + widget.api.setAlwaysOnScreen(false); + PosthogAnalytics.instance.logout(); + widget.api.transport.send(ElementWidgetActions.HangupCall, {}); + } - if ( - !isPasswordlessUser && - !isEmbedded && - !PosthogAnalytics.instance.isEnabled() - ) { - history.push("/"); - } - }, [groupCall, leave, isPasswordlessUser, isEmbedded, history]); + if ( + !isPasswordlessUser && + !isEmbedded && + !PosthogAnalytics.instance.isEnabled() + ) { + history.push("/"); + } + }, + [groupCall, leave, isPasswordlessUser, isEmbedded, history] + ); useEffect(() => { if (widget && state === GroupCallState.Entered) { @@ -218,6 +230,21 @@ export function GroupCallView({ const [userChoices, setUserChoices] = useState( undefined ); + const [e2eeConfig, setE2EEConfig] = useState( + undefined + ); + + const onReconnect = useCallback(() => { + setLeft(false); + setLeaveError(undefined); + groupCall.enter(); + }, [groupCall]); + + const livekitServiceURL = + groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url; + if (!livekitServiceURL) { + return ; + } if (error) { return ; @@ -236,6 +263,7 @@ export function GroupCallView({ unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} userChoices={userChoices} + e2eeConfig={e2eeConfig} otelGroupCallMembership={otelGroupCallMembership} /> @@ -249,13 +277,16 @@ export function GroupCallView({ // submitting anything. if ( isPasswordlessUser || - (PosthogAnalytics.instance.isEnabled() && !isEmbedded) + (PosthogAnalytics.instance.isEnabled() && !isEmbedded) || + leaveError ) { return ( ); } else { @@ -276,11 +307,12 @@ export function GroupCallView({ return ( { + onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => { setUserChoices(choices); + setE2EEConfig(e2eeConfig); enter(); }} - muteAudio={participants.size > 8} + initWithMutedAudio={participants.size > MUTE_PARTICIPANT_COUNT} isEmbedded={isEmbedded} hideHeader={hideHeader} /> diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 58812bdf..ef0b8c47 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -24,7 +24,7 @@ import { } from "@livekit/components-react"; import { usePreventScroll } from "@react-aria/overlays"; 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 { RoomMember } from "matrix-js-sdk/src/models/room-member"; 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 { OverlayTriggerState } from "@react-stately/overlays"; 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 { @@ -77,12 +78,13 @@ import { SettingsModal } from "../settings/SettingsModal"; import { InviteModal } from "./InviteModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; -import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; +import { E2EEConfig, UserChoices, useLiveKit } from "../livekit/useLiveKit"; import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; import { useSFUConfig } from "../livekit/OpenIDLoader"; import { E2EELock } from "../E2EELock"; +import { useEventEmitterThree } from "../useEvents"; import { useWakeLock } from "../useWakeLock"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -93,16 +95,25 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export interface ActiveCallProps extends Omit { userChoices: UserChoices; + e2eeConfig?: E2EEConfig; } export function ActiveCall(props: ActiveCallProps) { const sfuConfig = useSFUConfig(); - const livekitRoom = useLiveKit(props.userChoices, sfuConfig); + const livekitRoom = useLiveKit( + props.userChoices, + sfuConfig, + props.e2eeConfig + ); if (!livekitRoom) { return null; } + if (props.e2eeConfig && !livekitRoom.isE2EEEnabled) { + livekitRoom.setE2EEEnabled(!!props.e2eeConfig); + } + return ( @@ -115,7 +126,7 @@ export interface InCallViewProps { groupCall: GroupCall; livekitRoom: Room; participants: Map>; - onLeave: () => void; + onLeave: (error?: Error) => void; unencryptedEventsFromUsers: Set; hideHeader: boolean; otelGroupCallMembership?: OTelGroupCallMembership; @@ -190,6 +201,23 @@ export function InCallView({ 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(() => { widget?.api.transport.send( layout === "freedom" @@ -386,7 +414,7 @@ export function InCallView({ } buttons.push( - + ); footer =
{buttons}
; } diff --git a/src/room/LobbyView.module.css b/src/room/LobbyView.module.css index 55b7b5b0..3bb0162e 100644 --- a/src/room/LobbyView.module.css +++ b/src/room/LobbyView.module.css @@ -66,3 +66,9 @@ limitations under the License. .copyButton:last-child { margin-bottom: 0; } + +.passwordField { + width: 320px !important; + margin-bottom: 20px; + flex: 0; +} diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index eb725295..082c547c 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 styles from "./LobbyView.module.css"; @@ -25,21 +25,25 @@ import { UserMenuContainer } from "../UserMenuContainer"; import { Body, Link } from "../typography/Typography"; import { useLocationNavigation } from "../useLocationNavigation"; 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 { matrixInfo: MatrixInfo; - onEnter: (userChoices: UserChoices) => void; + onEnter: (userChoices: UserChoices, e2eeConfig?: E2EEConfig) => void; isEmbedded: boolean; hideHeader: boolean; - muteAudio: boolean; + initWithMutedAudio: boolean; } export function LobbyView(props: Props) { const { t } = useTranslation(); useLocationNavigation(); + const [enableE2EE] = useEnableE2EE(); + const joinCallButtonRef = useRef(null); useEffect(() => { if (joinCallButtonRef.current) { @@ -50,6 +54,17 @@ export function LobbyView(props: Props) { const [userChoices, setUserChoices] = useState( undefined ); + const [e2eeSharedKey, setE2EESharedKey] = useState( + undefined + ); + + const onE2EESharedKeyChanged = useCallback( + (event: ChangeEvent) => { + const value = event.target.value; + setE2EESharedKey(value === "" ? undefined : value); + }, + [setE2EESharedKey] + ); return (
@@ -67,15 +82,29 @@ export function LobbyView(props: Props) {
+ {enableE2EE && ( + + )} + ); + } + + return
{logsComponent}
; +}; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 44f10d96..3ebb215c 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -34,6 +34,7 @@ import { useOptInAnalytics, useDeveloperSettingsTab, useShowConnectionStats, + useEnableE2EE, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; @@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => { useDeveloperSettingsTab(); const [showConnectionStats, setShowConnectionStats] = useShowConnectionStats(); + const [enableE2EE, setEnableE2EE] = useEnableE2EE(); const downloadDebugLog = useDownloadDebugLog(); @@ -255,6 +257,18 @@ export const SettingsModal = (props: Props) => { } /> + + ) => + setEnableE2EE(e.target.checked) + } + /> + diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 63f1e23a..8d3f074f 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -104,6 +104,9 @@ export const useDeveloperSettingsTab = () => export const useShowConnectionStats = () => useSetting("show-connection-stats", false); +export const useEnableE2EE = () => + useSetting("enable-end-to-end-encryption", false); + export const useDefaultDevices = () => useSetting("defaultDevices", { audioinput: "", diff --git a/src/useEvents.ts b/src/useEvents.ts index 91a03abd..669cf863 100644 --- a/src/useEvents.ts +++ b/src/useEvents.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { useEffect } from "react"; +import EventEmitter from "eventemitter3"; import type { Listener, @@ -59,3 +60,20 @@ export const useTypedEventEmitter = < }; }, [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 +>( + emitter: EventEmitter, + eventType: T, + listener: EventEmitter.EventListener +) => { + useEffect(() => { + emitter.on(eventType, listener); + return () => { + emitter.off(eventType, listener); + }; + }, [emitter, eventType, listener]); +}; diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index 63ceddf2..f15052e0 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -121,6 +121,15 @@ export const VideoTile = forwardRef( const toolbarButtons: JSX.Element[] = []; if (!sfuParticipant.isLocal) { + toolbarButtons.push( + + ); + if (content === TileContent.ScreenShare) { toolbarButtons.push( ( onPress={onFullscreen} /> ); - } else { - // Due to the LK SDK this sadly only works for user-media atm - toolbarButtons.push( - - ); } } diff --git a/src/video-grid/VideoTileSettingsModal.tsx b/src/video-grid/VideoTileSettingsModal.tsx index 00d41c50..0c77075a 100644 --- a/src/video-grid/VideoTileSettingsModal.tsx +++ b/src/video-grid/VideoTileSettingsModal.tsx @@ -16,29 +16,36 @@ limitations under the License. import React, { ChangeEvent, useState } from "react"; import { useTranslation } from "react-i18next"; -import { RemoteParticipant } from "livekit-client"; +import { RemoteParticipant, Track } from "livekit-client"; import { FieldRow } from "../input/Input"; import { Modal } from "../Modal"; import styles from "./VideoTileSettingsModal.module.css"; import { VolumeIcon } from "../button/VolumeIcon"; -import { ItemData } from "./VideoTile"; +import { ItemData, TileContent } from "./VideoTile"; interface LocalVolumeProps { participant: RemoteParticipant; + content: TileContent; } const LocalVolume: React.FC = ({ participant, + content, }: LocalVolumeProps) => { + const source = + content === TileContent.UserMedia + ? Track.Source.Microphone + : Track.Source.ScreenShareAudio; + const [localVolume, setLocalVolume] = useState( - participant.getVolume() ?? 0 + participant.getVolume(source) ?? 0 ); const onLocalVolumeChanged = (event: ChangeEvent) => { const value: number = +event.target.value; setLocalVolume(value); - participant.setVolume(value); + participant.setVolume(value, source); }; return ( @@ -78,7 +85,10 @@ export const VideoTileSettingsModal = ({ data, onClose, ...rest }: Props) => { {...rest} >
- +
); diff --git a/yarn.lock b/yarn.lock index 5941702c..68fa7316 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2217,7 +2217,7 @@ "@react-hook/latest" "^1.0.3" 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" 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== @@ -6450,6 +6450,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" 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: version "3.0.3" 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" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -livekit-client@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-1.12.0.tgz#4b4f18087331d4893adaccb148e33fe870eb9a2e" - integrity sha512-G1KHNMSaEMXtPIKxTQt+WH/uRvhkQ0tQaZ5Kkem2CvB/5I4KPrV4DSmhBKP1P+8reHcaBBiye8V9bfpD69aHyQ== +livekit-client@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-1.12.1.tgz#b927b4fb07d2d64543d25a99db36ffbb7caa23e6" + integrity sha512-/mob04a/Mb0D+4sIzB7/pqakpJMCORSK+Qu5oTIcuSpgL+eBYGzHPE2sutGCGoe3Ns9sITAqUTyiui5+GN3i2w== dependencies: eventemitter3 "^5.0.1" 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" dependencies: "@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" bs58 "^5.0.0" 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" 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: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"