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-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",

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 \"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>",
"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",
"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</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",
"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</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",
@@ -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"
}

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 \"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>",
"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 \"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>",
"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 { 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 = <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 (
<FullScreenView>
<Trans>
@@ -141,10 +114,7 @@ export function CrashView() {
</Trans>
)}
<div className={styles.sendLogsSection}>{logsComponent}</div>
{error && (
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
)}
<RageshakeButton description="***Soft Crash***" />
<Button
size="lg"
variant="default"

View File

@@ -30,6 +30,7 @@ import {
MuteMicrophoneTracker,
UndecryptableToDeviceEventTracker,
QualitySurveyEventTracker,
CallDisconnectedEventTracker,
} from "./PosthogEvents";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
@@ -437,4 +438,5 @@ export class PosthogAnalytics {
public eventMuteCamera = new MuteCameraTracker();
public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker();
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.
*/
import { DisconnectReason } from "livekit-client";
import {
IPosthogEvent,
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 { useMemo } from "react";
import { useEffect, useMemo } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU";
@@ -15,12 +22,34 @@ export type DeviceChoices = {
enabled: boolean;
};
export type E2EEConfig = {
sharedKey: string;
};
setLogLevel("debug");
export function useLiveKit(
userChoices: UserChoices,
sfuConfig?: SFUConfig
sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig
): 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 options = defaultLiveKitOptions;
options.videoCaptureDefaults = {
@@ -31,15 +60,22 @@ export function useLiveKit(
...options.audioCaptureDefaults,
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({
token: sfuConfig?.jwt,
serverUrl: sfuConfig?.url,
audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false,
options: roomOptions,
room: roomWithoutProps,
});
return room;

View File

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

View File

@@ -28,15 +28,20 @@ import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { FieldRow, InputField } from "../input/Input";
import { StarRatingInput } from "../input/StarRatingInput";
import { RageshakeButton } from "../settings/RageshakeButton";
export function CallEndedView({
client,
isPasswordlessUser,
endedCallId,
leaveError,
reconnect,
}: {
client: MatrixClient;
isPasswordlessUser: boolean;
endedCallId: string;
leaveError?: Error;
reconnect: () => void;
}) {
const { t } = useTranslation();
const history = useHistory();
@@ -76,6 +81,7 @@ export function CallEndedView({
},
[endedCallId, history, isPasswordlessUser, starRating]
);
const createAccountDialog = isPasswordlessUser && (
<div className={styles.callEndedContent}>
<Trans>
@@ -138,6 +144,59 @@ export function CallEndedView({
</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 (
<>
<main className={styles.main}>
<Headline className={styles.headline}>
{surveySubmitted
? t("{{displayName}}, your call has ended.", {
displayName,
})
: t("{{displayName}}, your call has ended.", {
displayName,
}) +
"\n" +
t("How did it go?")}
</Headline>
{!surveySubmitted && PosthogAnalytics.instance.isEnabled()
? qualitySurveyDialog
: createAccountDialog}
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">
{t("Not now, return to home screen")}
</Link>
</Body>
</>
);
}
};
return (
<>
<Header>
@@ -146,29 +205,7 @@ export function CallEndedView({
</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>
<main className={styles.main}>
<Headline className={styles.headline}>
{surveySubmitted
? t("{{displayName}}, your call has ended.", {
displayName,
})
: t("{{displayName}}, your call has ended.", {
displayName,
}) +
"\n" +
t("How did it go?")}
</Headline>
{!surveySubmitted && PosthogAnalytics.instance.isEnabled()
? qualitySurveyDialog
: createAccountDialog}
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">
{t("Not now, return to home screen")}
</Link>
</Body>
</div>
<div className={styles.container}>{renderBody()}</div>
</>
);
}

View File

@@ -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<Error | undefined>(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<UserChoices | 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) {
return <ErrorView error={error} />;
@@ -236,6 +263,7 @@ export function GroupCallView({
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
userChoices={userChoices}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership}
/>
</OpenIDLoader>
@@ -249,13 +277,16 @@ export function GroupCallView({
// submitting anything.
if (
isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && !isEmbedded)
(PosthogAnalytics.instance.isEnabled() && !isEmbedded) ||
leaveError
) {
return (
<CallEndedView
endedCallId={groupCall.groupCallId}
client={client}
isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError}
reconnect={onReconnect}
/>
);
} else {
@@ -276,11 +307,12 @@ export function GroupCallView({
return (
<LobbyView
matrixInfo={matrixInfo}
onEnter={(choices: UserChoices) => {
onEnter={(choices: UserChoices, e2eeConfig?: E2EEConfig) => {
setUserChoices(choices);
setE2EEConfig(e2eeConfig);
enter();
}}
muteAudio={participants.size > 8}
initWithMutedAudio={participants.size > MUTE_PARTICIPANT_COUNT}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>

View File

@@ -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<InCallViewProps, "livekitRoom"> {
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 (
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} />
@@ -115,7 +126,7 @@ export interface InCallViewProps {
groupCall: GroupCall;
livekitRoom: Room;
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
onLeave: () => void;
onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set<string>;
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(
<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>;
}

View File

@@ -66,3 +66,9 @@ limitations under the License.
.copyButton:last-child {
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.
*/
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<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
@@ -50,6 +54,17 @@ export function LobbyView(props: Props) {
const [userChoices, setUserChoices] = useState<UserChoices | 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 (
<div className={styles.room}>
@@ -67,15 +82,29 @@ export function LobbyView(props: Props) {
<div className={styles.joinRoomContent}>
<VideoPreview
matrixInfo={props.matrixInfo}
muteAudio={props.muteAudio}
initWithMutedAudio={props.initWithMutedAudio}
onUserChoicesChanged={setUserChoices}
/>
{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={() => props.onEnter(userChoices!)}
onPress={() =>
props.onEnter(
userChoices!,
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
)
}
data-testid="lobby_joinCall"
>
Join call now

View File

@@ -41,13 +41,13 @@ export type MatrixInfo = {
interface Props {
matrixInfo: MatrixInfo;
muteAudio: boolean;
initWithMutedAudio: boolean;
onUserChoicesChanged: (choices: UserChoices) => void;
}
export function VideoPreview({
matrixInfo,
muteAudio,
initWithMutedAudio,
onUserChoicesChanged,
}: Props) {
const { client } = useClient();
@@ -70,13 +70,9 @@ export function VideoPreview({
// Create local media tracks.
const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
const [audioEnabled, setAudioEnabled] = useState<boolean>(!muteAudio);
useEffect(() => {
if (muteAudio) {
setAudioEnabled(false);
}
}, [muteAudio]);
const [audioEnabled, setAudioEnabled] = useState<boolean>(
!initWithMutedAudio
);
// 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.

View File

@@ -171,7 +171,7 @@ export function useGroupCall(
isScreensharing: false,
screenshareFeeds: [],
requestingScreenshare: false,
participants: new Map(),
participants: getParticipants(groupCall),
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,
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) => {
}
/>
</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>
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
</FieldRow>

View File

@@ -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: "",

View File

@@ -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<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[] = [];
if (!sfuParticipant.isLocal) {
toolbarButtons.push(
<AudioButton
key="localVolume"
className={styles.button}
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
onPress={onOptionsPress}
/>
);
if (content === TileContent.ScreenShare) {
toolbarButtons.push(
<FullscreenButton
@@ -130,16 +139,6 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
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 { 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<LocalVolumeProps> = ({
participant,
content,
}: LocalVolumeProps) => {
const source =
content === TileContent.UserMedia
? Track.Source.Microphone
: Track.Source.ScreenShareAudio;
const [localVolume, setLocalVolume] = useState<number>(
participant.getVolume() ?? 0
participant.getVolume(source) ?? 0
);
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => {
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}
>
<div className={styles.content}>
<LocalVolume participant={data.sfuParticipant as RemoteParticipant} />
<LocalVolume
participant={data.sfuParticipant as RemoteParticipant}
content={data.content}
/>
</div>
</Modal>
);

View File

@@ -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"