Prevent showing calling view when disconnected from Livekit. (#3491)
* Refactor disconnection handling * Use "unknown" * Update signature * Add tests * Expose livekitConnectionState directly * fix whoopsie
This commit is contained in:
@@ -17,7 +17,7 @@ import { act, render, type RenderResult } from "@testing-library/react";
|
|||||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
||||||
import { ConnectionState, type LocalParticipant } from "livekit-client";
|
import { type LocalParticipant } from "livekit-client";
|
||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
@@ -180,7 +180,6 @@ function createInCallView(): RenderResult & {
|
|||||||
onLeave={function (): void {
|
onLeave={function (): void {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
}}
|
}}
|
||||||
connState={ConnectionState.Connected}
|
|
||||||
onShareClick={null}
|
onShareClick={null}
|
||||||
/>
|
/>
|
||||||
</RoomContext>
|
</RoomContext>
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import useMeasure from "react-use-measure";
|
|||||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
import { useObservable, useSubscription } from "observable-hooks";
|
import {
|
||||||
|
useObservable,
|
||||||
|
useObservableEagerState,
|
||||||
|
useSubscription,
|
||||||
|
} from "observable-hooks";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||||
import {
|
import {
|
||||||
@@ -63,7 +67,6 @@ import { type MuteStates } from "./MuteStates";
|
|||||||
import { type MatrixInfo } from "./VideoPreview";
|
import { type MatrixInfo } from "./VideoPreview";
|
||||||
import { InviteButton } from "../button/InviteButton";
|
import { InviteButton } from "../button/InviteButton";
|
||||||
import { LayoutToggle } from "./LayoutToggle";
|
import { LayoutToggle } from "./LayoutToggle";
|
||||||
import { type ECConnectionState } from "../livekit/useECConnectionState";
|
|
||||||
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||||
import {
|
import {
|
||||||
CallViewModel,
|
CallViewModel,
|
||||||
@@ -212,12 +215,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<RoomContext value={livekitRoom}>
|
<RoomContext value={livekitRoom}>
|
||||||
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
||||||
<InCallView
|
<InCallView {...props} vm={vm} livekitRoom={livekitRoom} />
|
||||||
{...props}
|
|
||||||
vm={vm}
|
|
||||||
livekitRoom={livekitRoom}
|
|
||||||
connState={connState}
|
|
||||||
/>
|
|
||||||
</ReactionsSenderProvider>
|
</ReactionsSenderProvider>
|
||||||
</RoomContext>
|
</RoomContext>
|
||||||
);
|
);
|
||||||
@@ -235,7 +233,6 @@ export interface InCallViewProps {
|
|||||||
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
||||||
header: HeaderStyle;
|
header: HeaderStyle;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
connState: ECConnectionState;
|
|
||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +246,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
muteStates,
|
muteStates,
|
||||||
onLeave,
|
onLeave,
|
||||||
header: headerStyle,
|
header: headerStyle,
|
||||||
connState,
|
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -257,10 +253,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
useReactionsSender();
|
useReactionsSender();
|
||||||
|
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
const connectionState = useObservableEagerState(vm.livekitConnectionState$);
|
||||||
|
|
||||||
// annoyingly we don't get the disconnection reason this way,
|
// annoyingly we don't get the disconnection reason this way,
|
||||||
// only by listening for the emitted event
|
// only by listening for the emitted event
|
||||||
if (connState === ConnectionState.Disconnected)
|
if (connectionState === ConnectionState.Disconnected)
|
||||||
throw new ConnectionLostError();
|
throw new ConnectionLostError();
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
|
|||||||
@@ -1291,6 +1291,51 @@ describe("waitForCallPickup$", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ringing -> unknown if we get disconnected", () => {
|
||||||
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
|
const connectionState$ = new BehaviorSubject(ConnectionState.Connected);
|
||||||
|
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
|
||||||
|
withCallViewModel(
|
||||||
|
{
|
||||||
|
remoteParticipants$: behavior("a 19ms b", {
|
||||||
|
a: [],
|
||||||
|
b: [aliceParticipant],
|
||||||
|
}),
|
||||||
|
rtcMembers$: behavior("a 19ms b", {
|
||||||
|
a: [localRtcMember],
|
||||||
|
b: [localRtcMember, aliceRtcMember],
|
||||||
|
}),
|
||||||
|
connectionState$,
|
||||||
|
},
|
||||||
|
(vm, rtcSession) => {
|
||||||
|
// Notify at 5ms so we enter ringing, then get disconnected 5ms later
|
||||||
|
schedule(" 5ms r 5ms d", {
|
||||||
|
r: () => {
|
||||||
|
rtcSession.emit(
|
||||||
|
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||||
|
mockRingEvent("$notif2", 100),
|
||||||
|
mockLegacyRingEvent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
d: () => {
|
||||||
|
connectionState$.next(ConnectionState.Disconnected);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectObservable(vm.callPickupState$).toBe("a 4ms b 5ms c", {
|
||||||
|
a: "unknown",
|
||||||
|
b: "ringing",
|
||||||
|
c: "unknown",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waitForCallPickup: true,
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("success when someone joins before we notify", () => {
|
test("success when someone joins before we notify", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Join at 10ms, notify later at 20ms (state should stay success)
|
// Join at 10ms, notify later at 20ms (state should stay success)
|
||||||
|
|||||||
@@ -947,6 +947,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
* The current call pickup state of the call.
|
* The current call pickup state of the call.
|
||||||
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
|
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
|
||||||
* Then we can conclude if we were the first one to join or not.
|
* Then we can conclude if we were the first one to join or not.
|
||||||
|
* This may also be set if we are disconnected.
|
||||||
* - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening).
|
* - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening).
|
||||||
* - "timeout": No-one picked up in the defined time this call should be ringing on others devices.
|
* - "timeout": No-one picked up in the defined time this call should be ringing on others devices.
|
||||||
* The call failed. If desired this can be used as a trigger to exit the call.
|
* The call failed. If desired this can be used as a trigger to exit the call.
|
||||||
@@ -959,13 +960,20 @@ export class CallViewModel extends ViewModel {
|
|||||||
? this.scope.behavior<
|
? this.scope.behavior<
|
||||||
"unknown" | "ringing" | "timeout" | "decline" | "success"
|
"unknown" | "ringing" | "timeout" | "decline" | "success"
|
||||||
>(
|
>(
|
||||||
this.someoneElseJoined$.pipe(
|
combineLatest([
|
||||||
switchMap((someoneElseJoined) =>
|
this.livekitConnectionState$,
|
||||||
someoneElseJoined
|
this.someoneElseJoined$,
|
||||||
? of("success" as const)
|
]).pipe(
|
||||||
: // Show the ringing state of the most recent ringing attempt.
|
switchMap(([livekitConnectionState, someoneElseJoined]) => {
|
||||||
this.ring$.pipe(switchAll()),
|
if (livekitConnectionState === ConnectionState.Disconnected) {
|
||||||
),
|
// Do not ring until we're connected.
|
||||||
|
return of("unknown" as const);
|
||||||
|
} else if (someoneElseJoined) {
|
||||||
|
return of("success" as const);
|
||||||
|
}
|
||||||
|
// Show the ringing state of the most recent ringing attempt.
|
||||||
|
return this.ring$.pipe(switchAll());
|
||||||
|
}),
|
||||||
// The state starts as 'unknown' because we don't know if the RTC
|
// The state starts as 'unknown' because we don't know if the RTC
|
||||||
// session will actually send a notify event yet. It will only be
|
// session will actually send a notify event yet. It will only be
|
||||||
// known once we send our own membership and see that we were the
|
// known once we send our own membership and see that we were the
|
||||||
@@ -1682,7 +1690,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly livekitRoom: LivekitRoom,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
private readonly mediaDevices: MediaDevices,
|
private readonly mediaDevices: MediaDevices,
|
||||||
private readonly options: CallViewModelOptions,
|
private readonly options: CallViewModelOptions,
|
||||||
private readonly livekitConnectionState$: Observable<ECConnectionState>,
|
public readonly livekitConnectionState$: Observable<ECConnectionState>,
|
||||||
private readonly handsRaisedSubject$: Observable<
|
private readonly handsRaisedSubject$: Observable<
|
||||||
Record<string, RaisedHandInfo>
|
Record<string, RaisedHandInfo>
|
||||||
>,
|
>,
|
||||||
|
|||||||
Reference in New Issue
Block a user