diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json
index d697a70d..f63e9816 100644
--- a/public/locales/en-GB/app.json
+++ b/public/locales/en-GB/app.json
@@ -77,9 +77,11 @@
"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 +101,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 Policy2> and <6>Terms of Service6> 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 Policy2> and <6>Terms of Service6> 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 +118,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/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 = (
-
- {t("Send debug logs")}
-
- );
- }
-
return (
@@ -141,10 +114,7 @@ export function CrashView() {
)}
- {logsComponent}
- {error && (
-
- )}
+
({
+ eventName: "CallDisconnected",
+ reason,
+ });
+ }
+}
diff --git a/src/room/CallEndedView.module.css b/src/room/CallEndedView.module.css
index fd9bad99..9952a784 100644
--- a/src/room/CallEndedView.module.css
+++ b/src/room/CallEndedView.module.css
@@ -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;
diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx
index 26c1b1fc..69d2de79 100644
--- a/src/room/CallEndedView.tsx
+++ b/src/room/CallEndedView.tsx
@@ -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 && (
@@ -138,6 +144,59 @@ export function CallEndedView({
);
+ const renderBody = () => {
+ if (leaveError) {
+ return (
+ <>
+
+
+ You were disconnected from the call
+
+
+
+ {t("Reconnect")}
+
+
+
+
+
+
+
+
+ {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 89a72002..58fa0595 100644
--- a/src/room/GroupCallView.tsx
+++ b/src/room/GroupCallView.tsx
@@ -163,42 +163,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 +223,12 @@ export function GroupCallView({
undefined
);
+ const onReconnect = useCallback(() => {
+ setLeft(false);
+ setLeaveError(undefined);
+ groupCall.enter();
+ }, [groupCall]);
+
if (error) {
return ;
} else if (state === GroupCallState.Entered && userChoices) {
@@ -248,13 +259,16 @@ export function GroupCallView({
// submitting anything.
if (
isPasswordlessUser ||
- (PosthogAnalytics.instance.isEnabled() && !isEmbedded)
+ (PosthogAnalytics.instance.isEnabled() && !isEmbedded) ||
+ leaveError
) {
return (
);
} else {
diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx
index c45edbc2..863ea6a8 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 {
@@ -83,6 +84,7 @@ 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 ?? {});
@@ -115,7 +117,7 @@ export interface InCallViewProps {
groupCall: GroupCall;
livekitRoom: Room;
participants: Map>;
- onLeave: () => void;
+ onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
@@ -190,6 +192,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 +405,7 @@ export function InCallView({
}
buttons.push(
-
+
);
footer = {buttons}
;
}
diff --git a/src/settings/RageshakeButton.module.css b/src/settings/RageshakeButton.module.css
new file mode 100644
index 00000000..5a621b6c
--- /dev/null
+++ b/src/settings/RageshakeButton.module.css
@@ -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;
+}
diff --git a/src/settings/RageshakeButton.tsx b/src/settings/RageshakeButton.tsx
new file mode 100644
index 00000000..599c565d
--- /dev/null
+++ b/src/settings/RageshakeButton.tsx
@@ -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 = {t("Sending…")} ;
+ } else if (sent) {
+ logsComponent = {t("Thanks!")}
;
+ } else {
+ let caption = t("Send debug logs");
+ if (error) {
+ caption = t("Retry sending logs");
+ }
+
+ logsComponent = (
+
+ {caption}
+
+ );
+ }
+
+ return {logsComponent}
;
+};
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]);
+};