Make UI react instantly to hanging up but also wait for leave sound
This ensures that we don't see a mistaken 'reconnecting' toast while we're hanging up (and also that the leave sound gets a chance to play in widgets once again).
This commit is contained in:
@@ -41,7 +41,6 @@ import { ActiveCall } from "./InCallView";
|
|||||||
import { type MuteStates } from "../state/MuteStates";
|
import { type MuteStates } from "../state/MuteStates";
|
||||||
import { useMediaDevices } from "../MediaDevicesContext";
|
import { useMediaDevices } from "../MediaDevicesContext";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
import { leaveRTCSession } from "../rtcSessionHelpers";
|
|
||||||
import {
|
import {
|
||||||
saveKeyForRoom,
|
saveKeyForRoom,
|
||||||
useRoomEncryptionSystem,
|
useRoomEncryptionSystem,
|
||||||
@@ -50,7 +49,12 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
|||||||
import { useRoomName } from "./useRoomName";
|
import { useRoomName } from "./useRoomName";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
import {
|
||||||
|
getUrlParams,
|
||||||
|
HeaderStyle,
|
||||||
|
type UrlParams,
|
||||||
|
useUrlParams,
|
||||||
|
} from "../UrlParams";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
import {
|
import {
|
||||||
@@ -322,37 +326,62 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
setJoined(false);
|
setJoined(false);
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
const audioPromise = leaveSoundContext.current?.playSound(playSound);
|
const audioPromise = leaveSoundContext.current?.playSound(playSound);
|
||||||
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
// We need to wait until the callEnded event is tracked on PostHog,
|
||||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
// otherwise the iframe may get killed first.
|
||||||
const sendInstantly = !!widget;
|
|
||||||
// we need to wait until the callEnded event is tracked on posthog.
|
|
||||||
// Otherwise the iFrame gets killed before the callEnded event got tracked.
|
|
||||||
const posthogRequest = new Promise((resolve) => {
|
const posthogRequest = new Promise((resolve) => {
|
||||||
|
// To increase the likelihood of the PostHog event being sent out in
|
||||||
|
// widget mode before the iframe is killed, we ask it to skip the
|
||||||
|
// usual queuing/batching of requests.
|
||||||
|
const sendInstantly = widget !== null;
|
||||||
PosthogAnalytics.instance.eventCallEnded.track(
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
rtcSession.memberships.length,
|
rtcSession.memberships.length,
|
||||||
sendInstantly,
|
sendInstantly,
|
||||||
|
|
||||||
rtcSession,
|
rtcSession,
|
||||||
);
|
);
|
||||||
|
// Unfortunately the PostHog library provides no way to await the
|
||||||
|
// tracking of an event, but we don't really want it to hold up the
|
||||||
|
// closing of the widget that long anyway, so giving it 10 ms will do.
|
||||||
window.setTimeout(resolve, 10);
|
window.setTimeout(resolve, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
void Promise.all([audioPromise, posthogRequest])
|
void Promise.all([audioPromise, posthogRequest])
|
||||||
.then(() => {
|
.catch((e) =>
|
||||||
|
logger.error(
|
||||||
|
"Failed to play leave audio and/or send PostHog leave event",
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
if (
|
if (
|
||||||
!isPasswordlessUser &&
|
!isPasswordlessUser &&
|
||||||
!confineToRoom &&
|
!confineToRoom &&
|
||||||
!PosthogAnalytics.instance.isEnabled()
|
!PosthogAnalytics.instance.isEnabled()
|
||||||
) {
|
)
|
||||||
void navigate("/");
|
void navigate("/");
|
||||||
|
|
||||||
|
if (widget) {
|
||||||
|
// After this point the iframe could die at any moment!
|
||||||
|
try {
|
||||||
|
await widget.api.setAlwaysOnScreen(false);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to set call widget `alwaysOnScreen` to false",
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// On a normal user hangup we can shut down and close the widget. But if an
|
||||||
|
// error occurs we should keep the widget open until the user reads it.
|
||||||
|
if (reason === "user" && !getUrlParams().returnToLobby) {
|
||||||
|
try {
|
||||||
|
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to send close action", e);
|
||||||
|
}
|
||||||
|
widget.api.transport.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.catch(() =>
|
|
||||||
logger.error(
|
|
||||||
"could failed to play leave audio or send posthog leave event",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setJoined,
|
setJoined,
|
||||||
@@ -367,24 +396,11 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && joined) {
|
if (widget && joined)
|
||||||
// set widget to sticky once joined.
|
// set widget to sticky once joined.
|
||||||
widget.api.setAlwaysOnScreen(true).catch((e) => {
|
widget.api.setAlwaysOnScreen(true).catch((e) => {
|
||||||
logger.error("Error calling setAlwaysOnScreen(true)", e);
|
logger.error("Error calling setAlwaysOnScreen(true)", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHangup = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
|
||||||
widget.api.transport.reply(ev.detail, {});
|
|
||||||
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
|
||||||
leaveRTCSession(rtcSession, "user").catch((e) => {
|
|
||||||
logger.error("Failed to leave RTC session", e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
|
||||||
return (): void => {
|
|
||||||
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [widget, joined, rtcSession]);
|
}, [widget, joined, rtcSession]);
|
||||||
|
|
||||||
const joinRule = useJoinRule(room);
|
const joinRule = useJoinRule(room);
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
);
|
);
|
||||||
setVm(vm);
|
setVm(vm);
|
||||||
|
|
||||||
const sub = vm.left$.subscribe(props.onLeft);
|
const sub = vm.leave$.subscribe(props.onLeft);
|
||||||
return (): void => {
|
return (): void => {
|
||||||
vm.destroy();
|
vm.destroy();
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
@@ -798,7 +798,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
<EndCallButton
|
<EndCallButton
|
||||||
key="end_call"
|
key="end_call"
|
||||||
onClick={function (): void {
|
onClick={function (): void {
|
||||||
vm.leave();
|
vm.hangup();
|
||||||
}}
|
}}
|
||||||
onTouchEnd={onControlsTouchEnd}
|
onTouchEnd={onControlsTouchEnd}
|
||||||
data-testid="incall_leave"
|
data-testid="incall_leave"
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
switchScan,
|
switchScan,
|
||||||
take,
|
take,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
|
tap,
|
||||||
throttleTime,
|
throttleTime,
|
||||||
timer,
|
timer,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
@@ -117,16 +118,16 @@ import { constant, type Behavior } from "./Behavior";
|
|||||||
import {
|
import {
|
||||||
enterRTCSession,
|
enterRTCSession,
|
||||||
getLivekitAlias,
|
getLivekitAlias,
|
||||||
leaveRTCSession,
|
|
||||||
makeFocus,
|
makeFocus,
|
||||||
} from "../rtcSessionHelpers";
|
} from "../rtcSessionHelpers";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||||
import { Connection, PublishConnection } from "./Connection";
|
import { Connection, PublishConnection } from "./Connection";
|
||||||
import { type MuteStates } from "./MuteStates";
|
import { type MuteStates } from "./MuteStates";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
||||||
|
import { ElementWidgetActions, widget } from "../widget";
|
||||||
|
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
|
||||||
export interface CallViewModelOptions {
|
export interface CallViewModelOptions {
|
||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
@@ -554,19 +555,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.join$.next();
|
this.join$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly leave$ = new Subject<
|
|
||||||
"decline" | "timeout" | "user" | "allOthersLeft"
|
|
||||||
>();
|
|
||||||
|
|
||||||
public leave(): void {
|
|
||||||
this.leave$.next("user");
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly _left$ = new Subject<
|
|
||||||
"decline" | "timeout" | "user" | "allOthersLeft"
|
|
||||||
>();
|
|
||||||
public left$ = this._left$.asObservable();
|
|
||||||
|
|
||||||
private readonly connectionInstructions$ = this.join$.pipe(
|
private readonly connectionInstructions$ = this.join$.pipe(
|
||||||
switchMap(() => this.remoteConnections$),
|
switchMap(() => this.remoteConnections$),
|
||||||
startWith(new Map<string, Connection>()),
|
startWith(new Map<string, Connection>()),
|
||||||
@@ -1154,7 +1142,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
private readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
||||||
map(({ userIds, leftUserIds }) => {
|
filter(({ userIds, leftUserIds }) => {
|
||||||
if (!this.userId) {
|
if (!this.userId) {
|
||||||
logger.warn("Could not access user ID to compute allOthersLeft");
|
logger.warn("Could not access user ID to compute allOthersLeft");
|
||||||
return false;
|
return false;
|
||||||
@@ -1163,12 +1151,40 @@ export class CallViewModel extends ViewModel {
|
|||||||
userIds.size === 1 && userIds.has(this.userId) && leftUserIds.size > 0
|
userIds.size === 1 && userIds.has(this.userId) && leftUserIds.size > 0
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
startWith(false),
|
map(() => "allOthersLeft" as const),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly autoLeave$ = this.options.autoLeaveWhenOthersLeft
|
// Public for testing
|
||||||
? this.allOthersLeft$
|
public readonly autoLeave$ = merge(
|
||||||
: NEVER;
|
this.options.autoLeaveWhenOthersLeft ? this.allOthersLeft$ : NEVER,
|
||||||
|
this.callPickupState$.pipe(
|
||||||
|
filter((state) => state === "timeout" || state === "decline"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly userHangup$ = new Subject<void>();
|
||||||
|
public hangup(): void {
|
||||||
|
this.userHangup$.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly widgetHangup$ =
|
||||||
|
widget === null
|
||||||
|
? NEVER
|
||||||
|
: (
|
||||||
|
fromEvent(
|
||||||
|
widget.lazyActions,
|
||||||
|
ElementWidgetActions.HangupCall,
|
||||||
|
) as Observable<[CustomEvent<IWidgetApiRequest>]>
|
||||||
|
).pipe(tap(([ev]) => widget!.api.transport.reply(ev.detail, {})));
|
||||||
|
|
||||||
|
public readonly leave$: Observable<
|
||||||
|
"user" | "timeout" | "decline" | "allOthersLeft"
|
||||||
|
> = merge(
|
||||||
|
this.autoLeave$,
|
||||||
|
merge(this.userHangup$, this.widgetHangup$).pipe(
|
||||||
|
map(() => "user" as const),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of MediaItems that we want to display, that are of type ScreenShare
|
* List of MediaItems that we want to display, that are of type ScreenShare
|
||||||
@@ -1929,34 +1945,17 @@ export class CallViewModel extends ViewModel {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.allOthersLeft$
|
this.leave$.pipe(this.scope.bind()).subscribe(() => {
|
||||||
.pipe(
|
// Only sends Matrix leave event. The LiveKit session will disconnect once, uh...
|
||||||
this.scope.bind(),
|
// (TODO-MULTI-SFU does anything actually cause it to disconnect?)
|
||||||
filter((l) => (l && this.options.autoLeaveWhenOthersLeft) ?? false),
|
void this.matrixRTCSession
|
||||||
distinctUntilChanged(),
|
.leaveRoomSession()
|
||||||
)
|
.catch((e) => logger.error("Error leaving RTC session", e))
|
||||||
.subscribe(() => {
|
.then(async () =>
|
||||||
this.leave$.next("allOthersLeft");
|
widget?.api.transport
|
||||||
});
|
.send(ElementWidgetActions.HangupCall, {})
|
||||||
|
.catch((e) => logger.error("Failed to send hangup action", e)),
|
||||||
this.callPickupState$.pipe(this.scope.bind()).subscribe((state) => {
|
);
|
||||||
if (state === "timeout" || state === "decline") {
|
|
||||||
this.leave$.next(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.leave$.pipe(this.scope.bind()).subscribe((reason) => {
|
|
||||||
const { confineToRoom } = this.urlParams;
|
|
||||||
leaveRTCSession(this.matrixRTCSession, "user")
|
|
||||||
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
|
||||||
.then(() => {
|
|
||||||
if (!confineToRoom && !PosthogAnalytics.instance.isEnabled()) {
|
|
||||||
this._left$.next(reason);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error("Error leaving RTC session", e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pause upstream of all local media tracks when we're disconnected from
|
// Pause upstream of all local media tracks when we're disconnected from
|
||||||
|
|||||||
Reference in New Issue
Block a user