@@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55Please see LICENSE in the repository root for full details.
66*/
77
8- import { type FC , useCallback , useEffect , useMemo , useState } from "react" ;
8+ import {
9+ type FC ,
10+ type ReactElement ,
11+ type ReactNode ,
12+ useCallback ,
13+ useEffect ,
14+ useMemo ,
15+ useState ,
16+ } from "react" ;
917import { type MatrixClient } from "matrix-js-sdk/src/client" ;
1018import {
1119 Room ,
@@ -14,24 +22,29 @@ import {
1422import { logger } from "matrix-js-sdk/src/logger" ;
1523import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession" ;
1624import { JoinRule } from "matrix-js-sdk/src/matrix" ;
17- import { WebBrowserIcon } from "@vector-im/compound-design-tokens/assets/web/icons" ;
25+ import {
26+ OfflineIcon ,
27+ WebBrowserIcon ,
28+ } from "@vector-im/compound-design-tokens/assets/web/icons" ;
1829import { useTranslation } from "react-i18next" ;
1930import { useNavigate } from "react-router-dom" ;
31+ import { ErrorBoundary } from "@sentry/react" ;
32+ import { Button } from "@vector-im/compound-web" ;
2033
2134import type { IWidgetApiRequest } from "matrix-widget-api" ;
2235import {
2336 ElementWidgetActions ,
2437 type JoinCallData ,
2538 type WidgetHelpers ,
2639} from "../widget" ;
27- import { FullScreenView } from "../FullScreenView" ;
40+ import { ErrorPage , FullScreenView } from "../FullScreenView" ;
2841import { LobbyView } from "./LobbyView" ;
2942import { type MatrixInfo } from "./VideoPreview" ;
3043import { CallEndedView } from "./CallEndedView" ;
3144import { PosthogAnalytics } from "../analytics/PosthogAnalytics" ;
3245import { useProfile } from "../profile/useProfile" ;
3346import { findDeviceByName } from "../utils/media" ;
34- import { ActiveCall } from "./InCallView" ;
47+ import { ActiveCall , ConnectionLostError } from "./InCallView" ;
3548import { MUTE_PARTICIPANT_COUNT , type MuteStates } from "./MuteStates" ;
3649import { useMediaDevices } from "../livekit/MediaDevicesContext" ;
3750import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships" ;
@@ -55,6 +68,11 @@ declare global {
5568 }
5669}
5770
71+ interface GroupCallErrorPageProps {
72+ error : Error | unknown ;
73+ resetError : ( ) => void ;
74+ }
75+
5876interface Props {
5977 client : MatrixClient ;
6078 isPasswordlessUser : boolean ;
@@ -229,16 +247,14 @@ export const GroupCallView: FC<Props> = ({
229247 ] ) ;
230248
231249 const [ left , setLeft ] = useState ( false ) ;
232- const [ leaveError , setLeaveError ] = useState < Error | undefined > ( undefined ) ;
233250 const navigate = useNavigate ( ) ;
234251
235252 const onLeave = useCallback (
236- ( leaveError ?: Error ) : void => {
253+ ( cause : "user" | "error" = "user" ) : void => {
237254 const audioPromise = leaveSoundContext . current ?. playSound ( "left" ) ;
238255 // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
239256 // therefore we want the event to be sent instantly without getting queued/batched.
240257 const sendInstantly = ! ! widget ;
241- setLeaveError ( leaveError ) ;
242258 setLeft ( true ) ;
243259 // we need to wait until the callEnded event is tracked on posthog.
244260 // Otherwise the iFrame gets killed before the callEnded event got tracked.
@@ -254,7 +270,7 @@ export const GroupCallView: FC<Props> = ({
254270
255271 leaveRTCSession (
256272 rtcSession ,
257- leaveError === undefined ? "user" : "error" ,
273+ cause ,
258274 // Wait for the sound in widget mode (it's not long)
259275 Promise . all ( [ audioPromise , posthogRequest ] ) ,
260276 )
@@ -303,14 +319,6 @@ export const GroupCallView: FC<Props> = ({
303319 }
304320 } , [ widget , isJoined , rtcSession ] ) ;
305321
306- const onReconnect = useCallback ( ( ) => {
307- setLeft ( false ) ;
308- setLeaveError ( undefined ) ;
309- enterRTCSession ( rtcSession , perParticipantE2EE ) . catch ( ( e ) => {
310- logger . error ( "Error re-entering RTC session on reconnect" , e ) ;
311- } ) ;
312- } , [ rtcSession , perParticipantE2EE ] ) ;
313-
314322 const joinRule = useJoinRule ( rtcSession . room ) ;
315323
316324 const [ shareModalOpen , setInviteModalOpen ] = useState ( false ) ;
@@ -327,6 +335,43 @@ export const GroupCallView: FC<Props> = ({
327335
328336 const { t } = useTranslation ( ) ;
329337
338+ const errorPage = useMemo ( ( ) => {
339+ function GroupCallErrorPage ( {
340+ error,
341+ resetError,
342+ } : GroupCallErrorPageProps ) : ReactElement {
343+ useEffect ( ( ) => {
344+ if ( rtcSession . isJoined ( ) ) onLeave ( "error" ) ;
345+ } , [ error ] ) ;
346+
347+ const onReconnect = useCallback ( ( ) => {
348+ setLeft ( false ) ;
349+ resetError ( ) ;
350+ enterRTCSession ( rtcSession , perParticipantE2EE ) . catch ( ( e ) => {
351+ logger . error ( "Error re-entering RTC session on reconnect" , e ) ;
352+ } ) ;
353+ } , [ resetError ] ) ;
354+
355+ return error instanceof ConnectionLostError ? (
356+ < FullScreenView >
357+ < ErrorView
358+ Icon = { OfflineIcon }
359+ title = { t ( "error.connection_lost" ) }
360+ rageshake
361+ >
362+ < p > { t ( "error.connection_lost_description" ) } </ p >
363+ < Button onClick = { onReconnect } >
364+ { t ( "call_ended_view.reconnect_button" ) }
365+ </ Button >
366+ </ ErrorView >
367+ </ FullScreenView >
368+ ) : (
369+ < ErrorPage error = { error } />
370+ ) ;
371+ }
372+ return GroupCallErrorPage ;
373+ } , [ onLeave , rtcSession , perParticipantE2EE , t ] ) ;
374+
330375 if ( ! isE2EESupportedBrowser ( ) && e2eeSystem . kind !== E2eeType . NONE ) {
331376 // If we have a encryption system but the browser does not support it.
332377 return (
@@ -361,8 +406,9 @@ export const GroupCallView: FC<Props> = ({
361406 </ >
362407 ) ;
363408
409+ let body : ReactNode ;
364410 if ( isJoined ) {
365- return (
411+ body = (
366412 < >
367413 { shareModal }
368414 < ActiveCall
@@ -390,36 +436,32 @@ export const GroupCallView: FC<Props> = ({
390436 // submitting anything.
391437 if (
392438 isPasswordlessUser ||
393- ( PosthogAnalytics . instance . isEnabled ( ) && widget === null ) ||
394- leaveError
439+ ( PosthogAnalytics . instance . isEnabled ( ) && widget === null )
395440 ) {
396- return (
397- < >
398- < CallEndedView
399- endedCallId = { rtcSession . room . roomId }
400- client = { client }
401- isPasswordlessUser = { isPasswordlessUser }
402- confineToRoom = { confineToRoom }
403- leaveError = { leaveError }
404- reconnect = { onReconnect }
405- />
406- ;
407- </ >
441+ body = (
442+ < CallEndedView
443+ endedCallId = { rtcSession . room . roomId }
444+ client = { client }
445+ isPasswordlessUser = { isPasswordlessUser }
446+ confineToRoom = { confineToRoom }
447+ />
408448 ) ;
409449 } else {
410450 // If the user is a regular user, we'll have sent them back to the homepage,
411451 // so just sit here & do nothing: otherwise we would (briefly) mount the
412452 // LobbyView again which would open capture devices again.
413- return null ;
453+ body = null ;
414454 }
415455 } else if ( left && widget !== null ) {
416456 // Left in widget mode:
417457 if ( ! returnToLobby ) {
418- return null ;
458+ body = null ;
419459 }
420460 } else if ( preload || skipLobby ) {
421- return null ;
461+ body = null ;
462+ } else {
463+ body = lobbyView ;
422464 }
423465
424- return lobbyView ;
466+ return < ErrorBoundary fallback = { errorPage } > { body } </ ErrorBoundary > ;
425467} ;
0 commit comments