mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-05 04:15:58 +00:00
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 { useMediaDevices } from "../MediaDevicesContext";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
import { leaveRTCSession } from "../rtcSessionHelpers";
|
||||
import {
|
||||
saveKeyForRoom,
|
||||
useRoomEncryptionSystem,
|
||||
@@ -50,7 +49,12 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
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 { useAudioContext } from "../useAudioContext";
|
||||
import {
|
||||
@@ -322,37 +326,62 @@ export const GroupCallView: FC<Props> = ({
|
||||
setJoined(false);
|
||||
setLeft(true);
|
||||
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,
|
||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||
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.
|
||||
// We need to wait until the callEnded event is tracked on PostHog,
|
||||
// otherwise the iframe may get killed first.
|
||||
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(
|
||||
room.roomId,
|
||||
rtcSession.memberships.length,
|
||||
sendInstantly,
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 (
|
||||
!isPasswordlessUser &&
|
||||
!confineToRoom &&
|
||||
!PosthogAnalytics.instance.isEnabled()
|
||||
) {
|
||||
)
|
||||
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,
|
||||
@@ -367,24 +396,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget && joined) {
|
||||
if (widget && joined)
|
||||
// set widget to sticky once joined.
|
||||
widget.api.setAlwaysOnScreen(true).catch((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]);
|
||||
|
||||
const joinRule = useJoinRule(room);
|
||||
|
||||
@@ -151,7 +151,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
);
|
||||
setVm(vm);
|
||||
|
||||
const sub = vm.left$.subscribe(props.onLeft);
|
||||
const sub = vm.leave$.subscribe(props.onLeft);
|
||||
return (): void => {
|
||||
vm.destroy();
|
||||
sub.unsubscribe();
|
||||
@@ -798,7 +798,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
<EndCallButton
|
||||
key="end_call"
|
||||
onClick={function (): void {
|
||||
vm.leave();
|
||||
vm.hangup();
|
||||
}}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
data-testid="incall_leave"
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
switchScan,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
throttleTime,
|
||||
timer,
|
||||
} from "rxjs";
|
||||
@@ -117,16 +118,16 @@ import { constant, type Behavior } from "./Behavior";
|
||||
import {
|
||||
enterRTCSession,
|
||||
getLivekitAlias,
|
||||
leaveRTCSession,
|
||||
makeFocus,
|
||||
} from "../rtcSessionHelpers";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||
import { Connection, PublishConnection } from "./Connection";
|
||||
import { type MuteStates } from "./MuteStates";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
||||
import { ElementWidgetActions, widget } from "../widget";
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
export interface CallViewModelOptions {
|
||||
encryptionSystem: EncryptionSystem;
|
||||
@@ -554,19 +555,6 @@ export class CallViewModel extends ViewModel {
|
||||
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(
|
||||
switchMap(() => this.remoteConnections$),
|
||||
startWith(new Map<string, Connection>()),
|
||||
@@ -1154,7 +1142,7 @@ export class CallViewModel extends ViewModel {
|
||||
);
|
||||
|
||||
private readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
||||
map(({ userIds, leftUserIds }) => {
|
||||
filter(({ userIds, leftUserIds }) => {
|
||||
if (!this.userId) {
|
||||
logger.warn("Could not access user ID to compute allOthersLeft");
|
||||
return false;
|
||||
@@ -1163,12 +1151,40 @@ export class CallViewModel extends ViewModel {
|
||||
userIds.size === 1 && userIds.has(this.userId) && leftUserIds.size > 0
|
||||
);
|
||||
}),
|
||||
startWith(false),
|
||||
map(() => "allOthersLeft" as const),
|
||||
);
|
||||
|
||||
public readonly autoLeave$ = this.options.autoLeaveWhenOthersLeft
|
||||
? this.allOthersLeft$
|
||||
: NEVER;
|
||||
// Public for testing
|
||||
public readonly autoLeave$ = merge(
|
||||
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
|
||||
@@ -1929,34 +1945,17 @@ export class CallViewModel extends ViewModel {
|
||||
);
|
||||
});
|
||||
|
||||
this.allOthersLeft$
|
||||
.pipe(
|
||||
this.scope.bind(),
|
||||
filter((l) => (l && this.options.autoLeaveWhenOthersLeft) ?? false),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.leave$.next("allOthersLeft");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
this.leave$.pipe(this.scope.bind()).subscribe(() => {
|
||||
// Only sends Matrix leave event. The LiveKit session will disconnect once, uh...
|
||||
// (TODO-MULTI-SFU does anything actually cause it to disconnect?)
|
||||
void this.matrixRTCSession
|
||||
.leaveRoomSession()
|
||||
.catch((e) => logger.error("Error leaving RTC session", e))
|
||||
.then(async () =>
|
||||
widget?.api.transport
|
||||
.send(ElementWidgetActions.HangupCall, {})
|
||||
.catch((e) => logger.error("Failed to send hangup action", e)),
|
||||
);
|
||||
});
|
||||
|
||||
// Pause upstream of all local media tracks when we're disconnected from
|
||||
|
||||
Reference in New Issue
Block a user