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.
|
// A trivial implementation of Active call to ensure we are testing GroupCallView exclusively here.
|
||||||
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(
|
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(
|
||||||
({ onLeave }) => {
|
({ onLeft: onLeave }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => onLeave("user")}>Leave</button>
|
<button onClick={() => onLeave("user")}>Leave</button>
|
||||||
|
|||||||
@@ -184,9 +184,6 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
} = useUrlParams();
|
} = useUrlParams();
|
||||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||||
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
||||||
const [useExperimentalToDeviceTransport] = useSetting(
|
|
||||||
useExperimentalToDeviceTransportSetting,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the password once we start the groupCallView
|
// Save the password once we start the groupCallView
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -223,12 +220,6 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
try {
|
try {
|
||||||
setJoined(true);
|
setJoined(true);
|
||||||
// TODO-MULTI-SFU what to do with error handling now that we don't use this function?
|
// 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) {
|
} catch (e) {
|
||||||
if (e instanceof ElementCallError) {
|
if (e instanceof ElementCallError) {
|
||||||
setExternalError(e);
|
setExternalError(e);
|
||||||
@@ -322,16 +313,16 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onLeave = useCallback(
|
const onLeft = useCallback(
|
||||||
(
|
(reason: "timeout" | "user" | "allOthersLeft" | "decline"): void => {
|
||||||
cause: "user" | "error" = "user",
|
let playSound: CallEventSounds = "left";
|
||||||
playSound: CallEventSounds = "left",
|
if (reason === "timeout" || reason === "decline") playSound = reason;
|
||||||
): void => {
|
|
||||||
|
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,
|
// 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.
|
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||||
const sendInstantly = !!widget;
|
const sendInstantly = !!widget;
|
||||||
setLeft(true);
|
|
||||||
// we need to wait until the callEnded event is tracked on posthog.
|
// we need to wait until the callEnded event is tracked on posthog.
|
||||||
// Otherwise the iFrame gets killed before the callEnded event got tracked.
|
// Otherwise the iFrame gets killed before the callEnded event got tracked.
|
||||||
const posthogRequest = new Promise((resolve) => {
|
const posthogRequest = new Promise((resolve) => {
|
||||||
@@ -339,37 +330,33 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
room.roomId,
|
room.roomId,
|
||||||
rtcSession.memberships.length,
|
rtcSession.memberships.length,
|
||||||
sendInstantly,
|
sendInstantly,
|
||||||
|
|
||||||
rtcSession,
|
rtcSession,
|
||||||
);
|
);
|
||||||
window.setTimeout(resolve, 10);
|
window.setTimeout(resolve, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO-MULTI-SFU find a solution if this is supposed to happen here or in the view model.
|
void Promise.all([audioPromise, posthogRequest])
|
||||||
leaveRTCSession(
|
.then(() => {
|
||||||
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 () => {
|
|
||||||
if (
|
if (
|
||||||
!isPasswordlessUser &&
|
!isPasswordlessUser &&
|
||||||
!confineToRoom &&
|
!confineToRoom &&
|
||||||
!PosthogAnalytics.instance.isEnabled()
|
!PosthogAnalytics.instance.isEnabled()
|
||||||
) {
|
) {
|
||||||
await navigate("/");
|
void navigate("/");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(() =>
|
||||||
logger.error("Error leaving RTC session", e);
|
logger.error(
|
||||||
});
|
"could failed to play leave audio or send posthog leave event",
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
leaveSoundContext,
|
leaveSoundContext,
|
||||||
widget,
|
widget,
|
||||||
rtcSession,
|
|
||||||
room.roomId,
|
room.roomId,
|
||||||
|
rtcSession,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
navigate,
|
navigate,
|
||||||
@@ -457,7 +444,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
rtcSession={rtcSession as MatrixRTCSession}
|
rtcSession={rtcSession as MatrixRTCSession}
|
||||||
matrixRoom={room}
|
matrixRoom={room}
|
||||||
onLeave={onLeave}
|
onLeft={onLeft}
|
||||||
header={header}
|
header={header}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
e2eeSystem={e2eeSystem}
|
e2eeSystem={e2eeSystem}
|
||||||
@@ -518,7 +505,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
onError={
|
onError={
|
||||||
(/**error*/) => {
|
(/**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}
|
matrixRoom={room}
|
||||||
livekitRoom={livekitRoom}
|
livekitRoom={livekitRoom}
|
||||||
onLeave={function (): void {
|
participantCount={0}
|
||||||
|
onLeft={function (): void {
|
||||||
throw new Error("Function not implemented.");
|
throw new Error("Function not implemented.");
|
||||||
}}
|
}}
|
||||||
onShareClick={null}
|
onShareClick={null}
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ 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 {
|
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||||
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 {
|
||||||
@@ -94,10 +90,7 @@ import {
|
|||||||
} from "../reactions/useReactionsSender";
|
} from "../reactions/useReactionsSender";
|
||||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||||
import {
|
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||||
CallEventAudioRenderer,
|
|
||||||
type CallEventSounds,
|
|
||||||
} from "./CallEventAudioRenderer";
|
|
||||||
import {
|
import {
|
||||||
debugTileLayout as debugTileLayoutSetting,
|
debugTileLayout as debugTileLayoutSetting,
|
||||||
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
|
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
|
||||||
@@ -129,6 +122,8 @@ const maxTapDurationMs = 400;
|
|||||||
export interface ActiveCallProps
|
export interface ActiveCallProps
|
||||||
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
|
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
|
||||||
e2eeSystem: EncryptionSystem;
|
e2eeSystem: EncryptionSystem;
|
||||||
|
// TODO refactor those reasons into an enum
|
||||||
|
onLeft: (reason: "user" | "timeout" | "decline" | "allOthersLeft") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||||
@@ -154,8 +149,11 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
);
|
);
|
||||||
setVm(vm);
|
setVm(vm);
|
||||||
|
|
||||||
|
const sub = vm.left$.subscribe(props.onLeft);
|
||||||
return (): void => {
|
return (): void => {
|
||||||
vm.destroy();
|
vm.destroy();
|
||||||
|
sub.unsubscribe();
|
||||||
reactionsReader.destroy();
|
reactionsReader.destroy();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -167,6 +165,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
autoLeaveWhenOthersLeft,
|
autoLeaveWhenOthersLeft,
|
||||||
sendNotificationType,
|
sendNotificationType,
|
||||||
waitForCallPickup,
|
waitForCallPickup,
|
||||||
|
props.onLeft,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (vm === null) return null;
|
if (vm === null) return null;
|
||||||
@@ -185,8 +184,6 @@ export interface InCallViewProps {
|
|||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
matrixRoom: MatrixRoom;
|
matrixRoom: MatrixRoom;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
/** Function to call when the user explicitly ends the call */
|
|
||||||
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
|
||||||
header: HeaderStyle;
|
header: HeaderStyle;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
@@ -199,7 +196,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
rtcSession,
|
rtcSession,
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
muteStates,
|
muteStates,
|
||||||
onLeave,
|
|
||||||
header: headerStyle,
|
header: headerStyle,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -295,7 +292,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
const showFooter = useBehavior(vm.showFooter$);
|
const showFooter = useBehavior(vm.showFooter$);
|
||||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
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.
|
// We need to set the proper timings on the animation based upon the sound length.
|
||||||
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
||||||
@@ -316,16 +312,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
};
|
};
|
||||||
}, [pickupPhaseAudio?.soundDuration, ringDuration]);
|
}, [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
|
// When waiting for pickup, loop a waiting sound
|
||||||
useEffect((): void | (() => void) => {
|
useEffect((): void | (() => void) => {
|
||||||
if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
|
if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
|
||||||
@@ -343,6 +329,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
if (callPickupState !== "ringing") return null;
|
if (callPickupState !== "ringing") return null;
|
||||||
|
|
||||||
// Use room state for other participants data (the one that we likely want to reach)
|
// 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 = [
|
const roomOthers = [
|
||||||
...matrixRoom.getMembersWithMembership("join"),
|
...matrixRoom.getMembersWithMembership("join"),
|
||||||
...matrixRoom.getMembersWithMembership("invite"),
|
...matrixRoom.getMembersWithMembership("invite"),
|
||||||
@@ -816,7 +803,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
<EndCallButton
|
<EndCallButton
|
||||||
key="end_call"
|
key="end_call"
|
||||||
onClick={function (): void {
|
onClick={function (): void {
|
||||||
onLeave("user");
|
vm.leave();
|
||||||
}}
|
}}
|
||||||
onTouchEnd={onControlsTouchEnd}
|
onTouchEnd={onControlsTouchEnd}
|
||||||
data-testid="incall_leave"
|
data-testid="incall_leave"
|
||||||
|
|||||||
@@ -112,17 +112,19 @@ import { observeSpeaker$ } from "./observeSpeaker";
|
|||||||
import { shallowEquals } from "../utils/array";
|
import { shallowEquals } from "../utils/array";
|
||||||
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
||||||
import { type MediaDevices } from "./MediaDevices";
|
import { type MediaDevices } from "./MediaDevices";
|
||||||
import { type Behavior } from "./Behavior";
|
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 { type ECConnectionState } from "../livekit/useECConnectionState";
|
|
||||||
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";
|
||||||
|
|
||||||
export interface CallViewModelOptions {
|
export interface CallViewModelOptions {
|
||||||
encryptionSystem: EncryptionSystem;
|
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)
|
// 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.
|
// 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!
|
// there should be no other call to: this.matrixRTCSession.memberships!
|
||||||
@@ -541,12 +550,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.join$.next();
|
this.join$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly leave$ = new Subject<void>();
|
private readonly leave$ = new Subject<
|
||||||
|
"decline" | "timeout" | "user" | "allOthersLeft"
|
||||||
|
>();
|
||||||
|
|
||||||
public leave(): void {
|
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(
|
private readonly connectionInstructions$ = this.join$.pipe(
|
||||||
switchMap(() => this.remoteConnections$),
|
switchMap(() => this.remoteConnections$),
|
||||||
startWith(new Map<string, Connection>()),
|
startWith(new Map<string, Connection>()),
|
||||||
@@ -628,10 +644,9 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly connected$ = this.scope.behavior(
|
private readonly connected$ = this.scope.behavior(
|
||||||
and$(
|
and$(
|
||||||
this.matrixConnected$,
|
this.matrixConnected$,
|
||||||
// TODO-MULTI-SFU
|
this.livekitConnectionState$.pipe(
|
||||||
// this.livekitConnectionState$.pipe(
|
map((state) => state === ConnectionState.Connected),
|
||||||
// map((state) => state === ConnectionState.Connected),
|
),
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -663,7 +678,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
// in a split-brained state.
|
// in a split-brained state.
|
||||||
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
||||||
|
|
||||||
|
|
||||||
private readonly participants$ = this.scope.behavior<
|
private readonly participants$ = this.scope.behavior<
|
||||||
{
|
{
|
||||||
participant: LocalParticipant | RemoteParticipant;
|
participant: LocalParticipant | RemoteParticipant;
|
||||||
@@ -731,7 +745,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
// Handle room membership changes (and displayname updates)
|
// Handle room membership changes (and displayname updates)
|
||||||
fromEvent(this.matrixRoom, RoomStateEvent.Members),
|
fromEvent(this.matrixRoom, RoomStateEvent.Members),
|
||||||
// TODO: do we need: pauseWhen(this.pretendToBeDisconnected$),
|
// TODO: do we need: pauseWhen(this.pretendToBeDisconnected$),
|
||||||
|
|
||||||
).pipe(
|
).pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
map(() => {
|
map(() => {
|
||||||
@@ -759,7 +772,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return displaynameMap;
|
return displaynameMap;
|
||||||
},
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -971,21 +984,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.memberships$.pipe(map((ms) => ms.length)),
|
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(
|
private readonly didSendCallNotification$ = fromEvent(
|
||||||
this.matrixRTCSession,
|
this.matrixRTCSession,
|
||||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||||
@@ -994,6 +992,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whenever the RTC session tells us that it intends to ring the remote
|
* 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
|
* participant's devices, this emits an Observable tracking the current state of
|
||||||
@@ -1109,6 +1108,56 @@ export class CallViewModel extends ViewModel {
|
|||||||
map(() => {}),
|
map(() => {}),
|
||||||
throttleTime(THROTTLE_SOUND_EFFECT_MS),
|
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
|
* 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),
|
filter((v) => v.playSounds),
|
||||||
);
|
);
|
||||||
// TODO-REBASE: expose connection state observable
|
|
||||||
public readonly livekitConnectionState$: Observable<ECConnectionState>;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
// A call is permanently tied to a single Matrix room
|
// 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(() => {
|
this.allOthersLeft$
|
||||||
// TODO-MULTI-SFU: this makes no sense what so ever!!!
|
.pipe(
|
||||||
// need to look into this again.
|
this.scope.bind(),
|
||||||
// leaveRTCSession(
|
filter((l) => (l && this.options.autoLeaveWhenOthersLeft) ?? false),
|
||||||
// this.matrixRTCSession,
|
distinctUntilChanged(),
|
||||||
// "user", // TODO-MULTI-SFU ?
|
)
|
||||||
// // Wait for the sound in widget mode (it's not long)
|
.subscribe(() => {
|
||||||
// Promise.resolve(), // TODO-MULTI-SFU
|
this.leave$.next("allOthersLeft");
|
||||||
// //Promise.all([audioPromise, posthogRequest]),
|
});
|
||||||
// ).catch((e) => {
|
|
||||||
// logger.error("Error leaving RTC session", 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 } = 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
|
// Pause upstream of all local media tracks when we're disconnected from
|
||||||
|
|||||||
Reference in New Issue
Block a user