mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-02 04:05:56 +00:00
move leave logic into view model
Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
@@ -103,7 +103,7 @@ beforeEach(() => {
|
||||
});
|
||||
// A trivial implementation of Active call to ensure we are testing GroupCallView exclusively here.
|
||||
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(
|
||||
({ onLeave }) => {
|
||||
({ onLeft: onLeave }) => {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onLeave("user")}>Leave</button>
|
||||
|
||||
@@ -184,9 +184,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
} = useUrlParams();
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
||||
const [useExperimentalToDeviceTransport] = useSetting(
|
||||
useExperimentalToDeviceTransportSetting,
|
||||
);
|
||||
|
||||
// Save the password once we start the groupCallView
|
||||
useEffect(() => {
|
||||
@@ -223,12 +220,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
try {
|
||||
setJoined(true);
|
||||
// TODO-MULTI-SFU what to do with error handling now that we don't use this function?
|
||||
// await enterRTCSession(
|
||||
// rtcSession,
|
||||
// perParticipantE2EE,
|
||||
// useNewMembershipManager,
|
||||
// useExperimentalToDeviceTransport,
|
||||
// );
|
||||
} catch (e) {
|
||||
if (e instanceof ElementCallError) {
|
||||
setExternalError(e);
|
||||
@@ -322,16 +313,16 @@ export const GroupCallView: FC<Props> = ({
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onLeave = useCallback(
|
||||
(
|
||||
cause: "user" | "error" = "user",
|
||||
playSound: CallEventSounds = "left",
|
||||
): void => {
|
||||
const onLeft = useCallback(
|
||||
(reason: "timeout" | "user" | "allOthersLeft" | "decline"): void => {
|
||||
let playSound: CallEventSounds = "left";
|
||||
if (reason === "timeout" || reason === "decline") playSound = reason;
|
||||
|
||||
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;
|
||||
setLeft(true);
|
||||
// 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) => {
|
||||
@@ -339,37 +330,33 @@ export const GroupCallView: FC<Props> = ({
|
||||
room.roomId,
|
||||
rtcSession.memberships.length,
|
||||
sendInstantly,
|
||||
|
||||
rtcSession,
|
||||
);
|
||||
window.setTimeout(resolve, 10);
|
||||
});
|
||||
|
||||
// TODO-MULTI-SFU find a solution if this is supposed to happen here or in the view model.
|
||||
leaveRTCSession(
|
||||
rtcSession,
|
||||
cause,
|
||||
// Wait for the sound in widget mode (it's not long)
|
||||
Promise.all([audioPromise, posthogRequest]),
|
||||
)
|
||||
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
||||
.then(async () => {
|
||||
void Promise.all([audioPromise, posthogRequest])
|
||||
.then(() => {
|
||||
if (
|
||||
!isPasswordlessUser &&
|
||||
!confineToRoom &&
|
||||
!PosthogAnalytics.instance.isEnabled()
|
||||
) {
|
||||
await navigate("/");
|
||||
void navigate("/");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error leaving RTC session", e);
|
||||
});
|
||||
.catch(() =>
|
||||
logger.error(
|
||||
"could failed to play leave audio or send posthog leave event",
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
leaveSoundContext,
|
||||
widget,
|
||||
rtcSession,
|
||||
room.roomId,
|
||||
rtcSession,
|
||||
isPasswordlessUser,
|
||||
confineToRoom,
|
||||
navigate,
|
||||
@@ -457,7 +444,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
matrixInfo={matrixInfo}
|
||||
rtcSession={rtcSession as MatrixRTCSession}
|
||||
matrixRoom={room}
|
||||
onLeave={onLeave}
|
||||
onLeft={onLeft}
|
||||
header={header}
|
||||
muteStates={muteStates}
|
||||
e2eeSystem={e2eeSystem}
|
||||
@@ -518,7 +505,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
}}
|
||||
onError={
|
||||
(/**error*/) => {
|
||||
if (rtcSession.isJoined()) onLeave("error");
|
||||
// TODO this should not be "user". It needs a new case
|
||||
if (rtcSession.isJoined()) onLeft("user");
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -177,7 +177,8 @@ function createInCallView(): RenderResult & {
|
||||
}}
|
||||
matrixRoom={room}
|
||||
livekitRoom={livekitRoom}
|
||||
onLeave={function (): void {
|
||||
participantCount={0}
|
||||
onLeft={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
onShareClick={null}
|
||||
|
||||
@@ -23,11 +23,7 @@ import useMeasure from "react-use-measure";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import {
|
||||
useObservable,
|
||||
useObservableEagerState,
|
||||
useSubscription,
|
||||
} from "observable-hooks";
|
||||
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import {
|
||||
@@ -94,10 +90,7 @@ import {
|
||||
} from "../reactions/useReactionsSender";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import {
|
||||
CallEventAudioRenderer,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
|
||||
@@ -129,6 +122,8 @@ const maxTapDurationMs = 400;
|
||||
export interface ActiveCallProps
|
||||
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
|
||||
e2eeSystem: EncryptionSystem;
|
||||
// TODO refactor those reasons into an enum
|
||||
onLeft: (reason: "user" | "timeout" | "decline" | "allOthersLeft") => void;
|
||||
}
|
||||
|
||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
@@ -154,8 +149,11 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
reactionsReader.reactions$,
|
||||
);
|
||||
setVm(vm);
|
||||
|
||||
const sub = vm.left$.subscribe(props.onLeft);
|
||||
return (): void => {
|
||||
vm.destroy();
|
||||
sub.unsubscribe();
|
||||
reactionsReader.destroy();
|
||||
};
|
||||
}, [
|
||||
@@ -167,6 +165,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
autoLeaveWhenOthersLeft,
|
||||
sendNotificationType,
|
||||
waitForCallPickup,
|
||||
props.onLeft,
|
||||
]);
|
||||
|
||||
if (vm === null) return null;
|
||||
@@ -185,8 +184,6 @@ export interface InCallViewProps {
|
||||
rtcSession: MatrixRTCSession;
|
||||
matrixRoom: MatrixRoom;
|
||||
muteStates: MuteStates;
|
||||
/** Function to call when the user explicitly ends the call */
|
||||
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
||||
header: HeaderStyle;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
onShareClick: (() => void) | null;
|
||||
@@ -199,7 +196,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
rtcSession,
|
||||
matrixRoom,
|
||||
muteStates,
|
||||
onLeave,
|
||||
|
||||
header: headerStyle,
|
||||
onShareClick,
|
||||
}) => {
|
||||
@@ -295,7 +292,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
useSubscription(vm.autoLeave$, () => onLeave("user"));
|
||||
|
||||
// We need to set the proper timings on the animation based upon the sound length.
|
||||
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
||||
@@ -316,16 +312,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
};
|
||||
}, [pickupPhaseAudio?.soundDuration, ringDuration]);
|
||||
|
||||
// When we enter timeout or decline we will leave the call.
|
||||
useEffect((): void | (() => void) => {
|
||||
if (callPickupState === "timeout") {
|
||||
onLeave("user", "timeout");
|
||||
}
|
||||
if (callPickupState === "decline") {
|
||||
onLeave("user", "decline");
|
||||
}
|
||||
}, [callPickupState, onLeave, pickupPhaseAudio]);
|
||||
|
||||
// When waiting for pickup, loop a waiting sound
|
||||
useEffect((): void | (() => void) => {
|
||||
if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
|
||||
@@ -343,6 +329,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
if (callPickupState !== "ringing") return null;
|
||||
|
||||
// Use room state for other participants data (the one that we likely want to reach)
|
||||
// TODO: this screams it wants to be a behavior in the vm.
|
||||
const roomOthers = [
|
||||
...matrixRoom.getMembersWithMembership("join"),
|
||||
...matrixRoom.getMembersWithMembership("invite"),
|
||||
@@ -816,7 +803,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
<EndCallButton
|
||||
key="end_call"
|
||||
onClick={function (): void {
|
||||
onLeave("user");
|
||||
vm.leave();
|
||||
}}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
data-testid="incall_leave"
|
||||
|
||||
@@ -112,17 +112,19 @@ import { observeSpeaker$ } from "./observeSpeaker";
|
||||
import { shallowEquals } from "../utils/array";
|
||||
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
||||
import { type MediaDevices } from "./MediaDevices";
|
||||
import { type Behavior } from "./Behavior";
|
||||
import { constant, type Behavior } from "./Behavior";
|
||||
import {
|
||||
enterRTCSession,
|
||||
getLivekitAlias,
|
||||
leaveRTCSession,
|
||||
makeFocus,
|
||||
} from "../rtcSessionHelpers";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||
import { type ECConnectionState } from "../livekit/useECConnectionState";
|
||||
import { Connection, PublishConnection } from "./Connection";
|
||||
import { type MuteStates } from "./MuteStates";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
|
||||
export interface CallViewModelOptions {
|
||||
encryptionSystem: EncryptionSystem;
|
||||
@@ -461,6 +463,13 @@ export class CallViewModel extends ViewModel {
|
||||
),
|
||||
);
|
||||
|
||||
public readonly livekitConnectionState$ = this.scope.behavior(
|
||||
combineLatest([this.localConnection]).pipe(
|
||||
switchMap(([c]) => c.connectionState$),
|
||||
startWith(ConnectionState.Disconnected),
|
||||
),
|
||||
);
|
||||
|
||||
// TODO-MULTI-SFU make sure that we consider the room memberships here as well (so that here we only have valid memberships)
|
||||
// this also makes it possible to use this memberships$ list in all observables based on it.
|
||||
// there should be no other call to: this.matrixRTCSession.memberships!
|
||||
@@ -541,12 +550,19 @@ export class CallViewModel extends ViewModel {
|
||||
this.join$.next();
|
||||
}
|
||||
|
||||
private readonly leave$ = new Subject<void>();
|
||||
private readonly leave$ = new Subject<
|
||||
"decline" | "timeout" | "user" | "allOthersLeft"
|
||||
>();
|
||||
|
||||
public leave(): void {
|
||||
this.leave$.next();
|
||||
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>()),
|
||||
@@ -628,10 +644,9 @@ export class CallViewModel extends ViewModel {
|
||||
private readonly connected$ = this.scope.behavior(
|
||||
and$(
|
||||
this.matrixConnected$,
|
||||
// TODO-MULTI-SFU
|
||||
// this.livekitConnectionState$.pipe(
|
||||
// map((state) => state === ConnectionState.Connected),
|
||||
// ),
|
||||
this.livekitConnectionState$.pipe(
|
||||
map((state) => state === ConnectionState.Connected),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -663,7 +678,6 @@ export class CallViewModel extends ViewModel {
|
||||
// in a split-brained state.
|
||||
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
||||
|
||||
|
||||
private readonly participants$ = this.scope.behavior<
|
||||
{
|
||||
participant: LocalParticipant | RemoteParticipant;
|
||||
@@ -731,7 +745,6 @@ export class CallViewModel extends ViewModel {
|
||||
// Handle room membership changes (and displayname updates)
|
||||
fromEvent(this.matrixRoom, RoomStateEvent.Members),
|
||||
// TODO: do we need: pauseWhen(this.pretendToBeDisconnected$),
|
||||
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => {
|
||||
@@ -759,7 +772,7 @@ export class CallViewModel extends ViewModel {
|
||||
);
|
||||
}
|
||||
return displaynameMap;
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -971,21 +984,6 @@ export class CallViewModel extends ViewModel {
|
||||
this.memberships$.pipe(map((ms) => ms.length)),
|
||||
);
|
||||
|
||||
private readonly allOthersLeft$ = this.memberships$.pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, current]) =>
|
||||
current.every((m) => m.sender === this.userId) &&
|
||||
prev.some((m) => m.sender !== this.userId),
|
||||
),
|
||||
map(() => {}),
|
||||
take(1),
|
||||
);
|
||||
|
||||
public readonly autoLeave$ = this.options.autoLeaveWhenOthersLeft
|
||||
? this.allOthersLeft$
|
||||
: NEVER;
|
||||
|
||||
private readonly didSendCallNotification$ = fromEvent(
|
||||
this.matrixRTCSession,
|
||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||
@@ -994,6 +992,7 @@ export class CallViewModel extends ViewModel {
|
||||
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Whenever the RTC session tells us that it intends to ring the remote
|
||||
* participant's devices, this emits an Observable tracking the current state of
|
||||
@@ -1109,6 +1108,56 @@ export class CallViewModel extends ViewModel {
|
||||
map(() => {}),
|
||||
throttleTime(THROTTLE_SOUND_EFFECT_MS),
|
||||
);
|
||||
/**
|
||||
* This observable tracks the matrix users that are currently in the call.
|
||||
* There can be just one matrix user with multiple participants (see also participantChanges$)
|
||||
*/
|
||||
public readonly matrixUserChanges$ = this.userMedia$.pipe(
|
||||
map(
|
||||
(mediaItems) =>
|
||||
new Set(
|
||||
mediaItems
|
||||
.map((m) => m.vm.member?.userId)
|
||||
.filter((id) => id !== undefined),
|
||||
),
|
||||
),
|
||||
scan<
|
||||
Set<string>,
|
||||
{
|
||||
userIds: Set<string>;
|
||||
joinedUserIds: Set<string>;
|
||||
leftUserIds: Set<string>;
|
||||
}
|
||||
>(
|
||||
(prevState, userIds) => {
|
||||
const left = new Set(
|
||||
[...prevState.userIds].filter((id) => !userIds.has(id)),
|
||||
);
|
||||
const joined = new Set(
|
||||
[...userIds].filter((id) => !prevState.userIds.has(id)),
|
||||
);
|
||||
return { userIds: userIds, joinedUserIds: joined, leftUserIds: left };
|
||||
},
|
||||
{ userIds: new Set(), joinedUserIds: new Set(), leftUserIds: new Set() },
|
||||
),
|
||||
);
|
||||
|
||||
private readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
||||
map(({ userIds, leftUserIds }) => {
|
||||
if (!this.userId) {
|
||||
logger.warn("Could not access user ID to compute allOthersLeft");
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
userIds.size === 1 && userIds.has(this.userId) && leftUserIds.size > 0
|
||||
);
|
||||
}),
|
||||
startWith(false),
|
||||
);
|
||||
|
||||
public readonly autoLeave$ = this.options.autoLeaveWhenOthersLeft
|
||||
? this.allOthersLeft$
|
||||
: NEVER;
|
||||
|
||||
/**
|
||||
* List of MediaItems that we want to display, that are of type ScreenShare
|
||||
@@ -1791,8 +1840,6 @@ export class CallViewModel extends ViewModel {
|
||||
),
|
||||
filter((v) => v.playSounds),
|
||||
);
|
||||
// TODO-REBASE: expose connection state observable
|
||||
public readonly livekitConnectionState$: Observable<ECConnectionState>;
|
||||
|
||||
public constructor(
|
||||
// A call is permanently tied to a single Matrix room
|
||||
@@ -1839,18 +1886,34 @@ export class CallViewModel extends ViewModel {
|
||||
);
|
||||
});
|
||||
|
||||
this.join$.pipe(this.scope.bind()).subscribe(() => {
|
||||
// TODO-MULTI-SFU: this makes no sense what so ever!!!
|
||||
// need to look into this again.
|
||||
// leaveRTCSession(
|
||||
// this.matrixRTCSession,
|
||||
// "user", // TODO-MULTI-SFU ?
|
||||
// // Wait for the sound in widget mode (it's not long)
|
||||
// Promise.resolve(), // TODO-MULTI-SFU
|
||||
// //Promise.all([audioPromise, posthogRequest]),
|
||||
// ).catch((e) => {
|
||||
// logger.error("Error leaving RTC session", e);
|
||||
// });
|
||||
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 } = getUrlParams();
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user