Add disconnected screen for when livekit disconnects from the call
eg. when it gives up retrying to connect Also refactors the rageshake button into a dedicated component that we can just put wherever we want. Fixes https://github.com/vector-im/element-call/issues/1268
This commit is contained in:
@@ -21,11 +21,10 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
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;
|
||||
@@ -97,37 +96,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>
|
||||
@@ -139,10 +112,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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,42 +163,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 +223,12 @@ export function GroupCallView({
|
||||
undefined
|
||||
);
|
||||
|
||||
const onReconnect = useCallback(() => {
|
||||
setLeft(false);
|
||||
setLeaveError(undefined);
|
||||
groupCall.enter();
|
||||
}, [groupCall]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} 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 (
|
||||
<CallEndedView
|
||||
endedCallId={groupCall.groupCallId}
|
||||
client={client}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
leaveError={leaveError}
|
||||
reconnect={onReconnect}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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 {
|
||||
@@ -114,7 +115,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;
|
||||
@@ -188,6 +189,28 @@ export function InCallView({
|
||||
async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
|
||||
);
|
||||
|
||||
const onDisconnected = useCallback(
|
||||
(reason?: DisconnectReason) => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
livekitRoom.on(RoomEvent.Disconnected, onDisconnected);
|
||||
|
||||
return () => {
|
||||
livekitRoom.off(RoomEvent.Disconnected, onDisconnected);
|
||||
};
|
||||
}, [onDisconnected, livekitRoom]);
|
||||
|
||||
useEffect(() => {
|
||||
widget?.api.transport.send(
|
||||
layout === "freedom"
|
||||
@@ -384,7 +407,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>;
|
||||
}
|
||||
|
||||
22
src/settings/RageshakeButton.module.css
Normal file
22
src/settings/RageshakeButton.module.css
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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;
|
||||
width: 176px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
67
src/settings/RageshakeButton.tsx
Normal file
67
src/settings/RageshakeButton.tsx
Normal 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 (sent) {
|
||||
logsComponent = <div>{t("Thanks!")}</div>;
|
||||
} else {
|
||||
let caption = t("Send debug logs");
|
||||
if (error) {
|
||||
caption = t("Retry sending logs");
|
||||
} else if (sending) {
|
||||
logsComponent = <span>{t("Sending…")}</span>;
|
||||
}
|
||||
|
||||
logsComponent = (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
onPress={sendDebugLogs}
|
||||
className={styles.wideButton}
|
||||
disabled={sending}
|
||||
>
|
||||
{caption}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={styles.rageshakeControl}>{logsComponent}</div>;
|
||||
};
|
||||
Reference in New Issue
Block a user