@@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
5
Please see LICENSE in the repository root for full details.
6
6
*/
7
7
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" ;
9
17
import { type MatrixClient } from "matrix-js-sdk/src/client" ;
10
18
import {
11
19
Room ,
@@ -14,24 +22,29 @@ import {
14
22
import { logger } from "matrix-js-sdk/src/logger" ;
15
23
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession" ;
16
24
import { 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" ;
18
29
import { useTranslation } from "react-i18next" ;
19
30
import { useNavigate } from "react-router-dom" ;
31
+ import { ErrorBoundary } from "@sentry/react" ;
32
+ import { Button } from "@vector-im/compound-web" ;
20
33
21
34
import type { IWidgetApiRequest } from "matrix-widget-api" ;
22
35
import {
23
36
ElementWidgetActions ,
24
37
type JoinCallData ,
25
38
type WidgetHelpers ,
26
39
} from "../widget" ;
27
- import { FullScreenView } from "../FullScreenView" ;
40
+ import { ErrorPage , FullScreenView } from "../FullScreenView" ;
28
41
import { LobbyView } from "./LobbyView" ;
29
42
import { type MatrixInfo } from "./VideoPreview" ;
30
43
import { CallEndedView } from "./CallEndedView" ;
31
44
import { PosthogAnalytics } from "../analytics/PosthogAnalytics" ;
32
45
import { useProfile } from "../profile/useProfile" ;
33
46
import { findDeviceByName } from "../utils/media" ;
34
- import { ActiveCall } from "./InCallView" ;
47
+ import { ActiveCall , ConnectionLostError } from "./InCallView" ;
35
48
import { MUTE_PARTICIPANT_COUNT , type MuteStates } from "./MuteStates" ;
36
49
import { useMediaDevices } from "../livekit/MediaDevicesContext" ;
37
50
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships" ;
@@ -55,6 +68,11 @@ declare global {
55
68
}
56
69
}
57
70
71
+ interface GroupCallErrorPageProps {
72
+ error : Error | unknown ;
73
+ resetError : ( ) => void ;
74
+ }
75
+
58
76
interface Props {
59
77
client : MatrixClient ;
60
78
isPasswordlessUser : boolean ;
@@ -229,16 +247,14 @@ export const GroupCallView: FC<Props> = ({
229
247
] ) ;
230
248
231
249
const [ left , setLeft ] = useState ( false ) ;
232
- const [ leaveError , setLeaveError ] = useState < Error | undefined > ( undefined ) ;
233
250
const navigate = useNavigate ( ) ;
234
251
235
252
const onLeave = useCallback (
236
- ( leaveError ?: Error ) : void => {
253
+ ( cause : "user" | "error" = "user" ) : void => {
237
254
const audioPromise = leaveSoundContext . current ?. playSound ( "left" ) ;
238
255
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
239
256
// therefore we want the event to be sent instantly without getting queued/batched.
240
257
const sendInstantly = ! ! widget ;
241
- setLeaveError ( leaveError ) ;
242
258
setLeft ( true ) ;
243
259
// we need to wait until the callEnded event is tracked on posthog.
244
260
// Otherwise the iFrame gets killed before the callEnded event got tracked.
@@ -254,7 +270,7 @@ export const GroupCallView: FC<Props> = ({
254
270
255
271
leaveRTCSession (
256
272
rtcSession ,
257
- leaveError === undefined ? "user" : "error" ,
273
+ cause ,
258
274
// Wait for the sound in widget mode (it's not long)
259
275
Promise . all ( [ audioPromise , posthogRequest ] ) ,
260
276
)
@@ -303,14 +319,6 @@ export const GroupCallView: FC<Props> = ({
303
319
}
304
320
} , [ widget , isJoined , rtcSession ] ) ;
305
321
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
-
314
322
const joinRule = useJoinRule ( rtcSession . room ) ;
315
323
316
324
const [ shareModalOpen , setInviteModalOpen ] = useState ( false ) ;
@@ -327,6 +335,43 @@ export const GroupCallView: FC<Props> = ({
327
335
328
336
const { t } = useTranslation ( ) ;
329
337
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
+
330
375
if ( ! isE2EESupportedBrowser ( ) && e2eeSystem . kind !== E2eeType . NONE ) {
331
376
// If we have a encryption system but the browser does not support it.
332
377
return (
@@ -361,8 +406,9 @@ export const GroupCallView: FC<Props> = ({
361
406
</ >
362
407
) ;
363
408
409
+ let body : ReactNode ;
364
410
if ( isJoined ) {
365
- return (
411
+ body = (
366
412
< >
367
413
{ shareModal }
368
414
< ActiveCall
@@ -390,36 +436,32 @@ export const GroupCallView: FC<Props> = ({
390
436
// submitting anything.
391
437
if (
392
438
isPasswordlessUser ||
393
- ( PosthogAnalytics . instance . isEnabled ( ) && widget === null ) ||
394
- leaveError
439
+ ( PosthogAnalytics . instance . isEnabled ( ) && widget === null )
395
440
) {
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
+ />
408
448
) ;
409
449
} else {
410
450
// If the user is a regular user, we'll have sent them back to the homepage,
411
451
// so just sit here & do nothing: otherwise we would (briefly) mount the
412
452
// LobbyView again which would open capture devices again.
413
- return null ;
453
+ body = null ;
414
454
}
415
455
} else if ( left && widget !== null ) {
416
456
// Left in widget mode:
417
457
if ( ! returnToLobby ) {
418
- return null ;
458
+ body = null ;
419
459
}
420
460
} else if ( preload || skipLobby ) {
421
- return null ;
461
+ body = null ;
462
+ } else {
463
+ body = lobbyView ;
422
464
}
423
465
424
- return lobbyView ;
466
+ return < ErrorBoundary fallback = { errorPage } > { body } </ ErrorBoundary > ;
425
467
} ;
0 commit comments