diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index 95a8a7ae..e3a5403c 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -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 =
{t("Thanks! We'll get right on it.")}
; - } else if (sending) { - logsComponent =
{t("Sending…")}
; - } else if (Config.get().rageshake?.submit_url) { - logsComponent = ( - - ); - } - return ( @@ -139,10 +112,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 29d14b82..da7cb768 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 b8284975..30458599 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 { @@ -114,7 +115,7 @@ export interface InCallViewProps { groupCall: GroupCall; livekitRoom: Room; participants: Map>; - onLeave: () => void; + onLeave: (error?: Error) => void; unencryptedEventsFromUsers: Set; 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( - + ); footer =
{buttons}
; } diff --git a/src/settings/RageshakeButton.module.css b/src/settings/RageshakeButton.module.css new file mode 100644 index 00000000..66d0e2d9 --- /dev/null +++ b/src/settings/RageshakeButton.module.css @@ -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; +} diff --git a/src/settings/RageshakeButton.tsx b/src/settings/RageshakeButton.tsx new file mode 100644 index 00000000..b6b045d8 --- /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 (sent) { + logsComponent =
{t("Thanks!")}
; + } else { + let caption = t("Send debug logs"); + if (error) { + caption = t("Retry sending logs"); + } else if (sending) { + logsComponent = {t("Sending…")}; + } + + logsComponent = ( + + ); + } + + return
{logsComponent}
; +};