convert CallViewModel into create function pattern. (with much more

minimal changes thanks to the intermediate class refactor)
This commit is contained in:
Timo K
2025-11-17 18:22:25 +01:00
parent 16e1c59e11
commit 2e2c799f72
5 changed files with 1115 additions and 1123 deletions

View File

@@ -59,7 +59,8 @@ import { type MatrixInfo } from "./VideoPreview";
import { InviteButton } from "../button/InviteButton"; import { InviteButton } from "../button/InviteButton";
import { LayoutToggle } from "./LayoutToggle"; import { LayoutToggle } from "./LayoutToggle";
import { import {
CallViewModel, type CallViewModel,
createCallViewModel$,
type GridMode, type GridMode,
} from "../state/CallViewModel/CallViewModel.ts"; } from "../state/CallViewModel/CallViewModel.ts";
import { Grid, type TileProps } from "../grid/Grid"; import { Grid, type TileProps } from "../grid/Grid";
@@ -128,7 +129,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
const reactionsReader = new ReactionsReader(scope, props.rtcSession); const reactionsReader = new ReactionsReader(scope, props.rtcSession);
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } = const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
urlParams; urlParams;
const vm = new CallViewModel( const vm = createCallViewModel$(
scope, scope,
props.rtcSession, props.rtcSession,
props.matrixRoom, props.matrixRoom,

View File

@@ -37,7 +37,7 @@ import {
import { deepCompare } from "matrix-js-sdk/lib/utils"; import { deepCompare } from "matrix-js-sdk/lib/utils";
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
import { CallViewModel } from "./CallViewModel"; import { createCallViewModel$ } from "./CallViewModel";
import { type Layout } from "../layout-types.ts"; import { type Layout } from "../layout-types.ts";
import { import {
mockLocalParticipant, mockLocalParticipant,
@@ -277,7 +277,7 @@ describe("CallViewModel", () => {
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({}); vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
const callVM = new CallViewModel( const callVM = createCallViewModel$(
testScope(), testScope(),
fakeRtcSession.asMockedSession(), fakeRtcSession.asMockedSession(),
matrixRoom, matrixRoom,

View File

@@ -172,55 +172,56 @@ type AudioLivekitItem = {
}; };
/** /**
* A view model providing all the application logic needed to show the in-call * The return of createCallViewModel$
* UI (may eventually be expanded to cover the lobby and feedback screens in the * This interface represents the root snapshot for the call view. Snapshots in EC with rxjs behave like snapshot trees.
* future). * They are a list of observables and objects containing observables to allow for a very granular update mechanism.
*
* This allows to have one huge call view model that represents the entire view without a unnecessary amount of updates.
*
* (Mocking this interface should allow building a full view in all states.)
*/ */
// Throughout this class and related code we must distinguish between MatrixRTC export interface CallViewModel {
// state and LiveKit state. We use the common terminology of room "members", RTC
// "memberships", and LiveKit "participants".
export class CallViewModel {
// lifecycle // lifecycle
public autoLeave$: Observable<AutoLeaveReason>; autoLeave$: Observable<AutoLeaveReason>;
// TODO if we are in "unknown" state we need a loading rendering (or empty screen) // TODO if we are in "unknown" state we need a loading rendering (or empty screen)
// Otherwise it looks like we already connected and only than the ringing starts which is weird. // Otherwise it looks like we already connected and only than the ringing starts which is weird.
public callPickupState$: Behavior< callPickupState$: Behavior<
"unknown" | "ringing" | "timeout" | "decline" | "success" | null "unknown" | "ringing" | "timeout" | "decline" | "success" | null
>; >;
public leave$: Observable<"user" | AutoLeaveReason>; leave$: Observable<"user" | AutoLeaveReason>;
/** Call to initiate hangup. Use in conbination with connectino state track the async hangup process. */ /** Call to initiate hangup. Use in conbination with connectino state track the async hangup process. */
public hangup: () => void; hangup: () => void;
// joining // joining
public join: () => LocalMemberConnectionState; join: () => LocalMemberConnectionState;
// screen sharing // screen sharing
/** /**
* Callback to toggle screen sharing. If null, screen sharing is not possible. * Callback to toggle screen sharing. If null, screen sharing is not possible.
*/ */
public toggleScreenSharing: (() => void) | null; toggleScreenSharing: (() => void) | null;
/** /**
* Whether we are sharing our screen. * Whether we are sharing our screen.
*/ */
public sharingScreen$: Behavior<boolean>; sharingScreen$: Behavior<boolean>;
// UI interactions // UI interactions
/** /**
* Callback for when the user taps the call view. * Callback for when the user taps the call view.
*/ */
public tapScreen: () => void; tapScreen: () => void;
/** /**
* Callback for when the user taps the call's controls. * Callback for when the user taps the call's controls.
*/ */
public tapControls: () => void; tapControls: () => void;
/** /**
* Callback for when the user hovers over the call view. * Callback for when the user hovers over the call view.
*/ */
public hoverScreen: () => void; hoverScreen: () => void;
/** /**
* Callback for when the user stops hovering over the call view. * Callback for when the user stops hovering over the call view.
*/ */
public unhoverScreen: () => void; unhoverScreen: () => void;
// errors // errors
/** /**
@@ -228,7 +229,7 @@ export class CallViewModel {
* This is a fatal error that prevents the call from being created/joined. * This is a fatal error that prevents the call from being created/joined.
* Should render a blocking error screen. * Should render a blocking error screen.
*/ */
public configError$: Behavior<ElementCallError | null>; configError$: Behavior<ElementCallError | null>;
// participants and counts // participants and counts
/** /**
@@ -238,15 +239,15 @@ export class CallViewModel {
* - There can be multiple participants for one Matrix user if they join from * - There can be multiple participants for one Matrix user if they join from
* multiple devices. * multiple devices.
*/ */
public participantCount$: Behavior<number>; participantCount$: Behavior<number>;
/** Participants sorted by livekit room so they can be used in the audio rendering */ /** Participants sorted by livekit room so they can be used in the audio rendering */
public audioParticipants$: Behavior<AudioLivekitItem[]>; audioParticipants$: Behavior<AudioLivekitItem[]>;
/** List of participants raising their hand */ /** List of participants raising their hand */
public handsRaised$: Behavior<Record<string, RaisedHandInfo>>; handsRaised$: Behavior<Record<string, RaisedHandInfo>>;
/** List of reactions. Keys are: membership.membershipId (currently predefined as: `${membershipEvent.userId}:${membershipEvent.deviceId}`)*/ /** List of reactions. Keys are: membership.membershipId (currently predefined as: `${membershipEvent.userId}:${membershipEvent.deviceId}`)*/
public reactions$: Behavior<Record<string, ReactionOption>>; reactions$: Behavior<Record<string, ReactionOption>>;
public ringOverlay$: Behavior<null | { ringOverlay$: Behavior<null | {
name: string; name: string;
/** roomId or userId for the avatar generation. */ /** roomId or userId for the avatar generation. */
idForAvatar: string; idForAvatar: string;
@@ -254,27 +255,27 @@ export class CallViewModel {
avatarMxc?: string; avatarMxc?: string;
}>; }>;
// sounds and events // sounds and events
public joinSoundEffect$: Observable<void>; joinSoundEffect$: Observable<void>;
public leaveSoundEffect$: Observable<void>; leaveSoundEffect$: Observable<void>;
/** /**
* Emits an event every time a new hand is raised in * Emits an event every time a new hand is raised in
* the call. * the call.
*/ */
public newHandRaised$: Observable<{ value: number; playSounds: boolean }>; newHandRaised$: Observable<{ value: number; playSounds: boolean }>;
/** /**
* Emits an event every time a new screenshare is started in * Emits an event every time a new screenshare is started in
* the call. * the call.
*/ */
public newScreenShare$: Observable<{ value: number; playSounds: boolean }>; newScreenShare$: Observable<{ value: number; playSounds: boolean }>;
/** /**
* Emits an array of reactions that should be played. * Emits an array of reactions that should be played.
*/ */
public audibleReactions$: Observable<string[]>; audibleReactions$: Observable<string[]>;
/** /**
* Emits an array of reactions that should be visible on the screen. * Emits an array of reactions that should be visible on the screen.
*/ */
// DISCUSSION move this into a reaction file // DISCUSSION move this into a reaction file
public visibleReactions$: Behavior< visibleReactions$: Behavior<
{ sender: string; emoji: string; startX: number }[] { sender: string; emoji: string; startX: number }[]
>; >;
@@ -282,43 +283,43 @@ export class CallViewModel {
/** /**
* The general shape of the window. * The general shape of the window.
*/ */
public windowMode$: Behavior<WindowMode>; windowMode$: Behavior<WindowMode>;
public spotlightExpanded$: Behavior<boolean>; spotlightExpanded$: Behavior<boolean>;
public toggleSpotlightExpanded$: Behavior<(() => void) | null>; toggleSpotlightExpanded$: Behavior<(() => void) | null>;
public gridMode$: Behavior<GridMode>; gridMode$: Behavior<GridMode>;
public setGridMode: (value: GridMode) => void; setGridMode: (value: GridMode) => void;
// media view models and layout // media view models and layout
public grid$: Behavior<UserMediaViewModel[]>; grid$: Behavior<UserMediaViewModel[]>;
public spotlight$: Behavior<MediaViewModel[]>; spotlight$: Behavior<MediaViewModel[]>;
public pip$: Behavior<UserMediaViewModel | null>; pip$: Behavior<UserMediaViewModel | null>;
/** /**
* The layout of tiles in the call interface. * The layout of tiles in the call interface.
*/ */
public layout$: Behavior<Layout>; layout$: Behavior<Layout>;
/** /**
* The current generation of the tile store, exposed for debugging purposes. * The current generation of the tile store, exposed for debugging purposes.
*/ */
public tileStoreGeneration$: Behavior<number>; tileStoreGeneration$: Behavior<number>;
public showSpotlightIndicators$: Behavior<boolean>; showSpotlightIndicators$: Behavior<boolean>;
public showSpeakingIndicators$: Behavior<boolean>; showSpeakingIndicators$: Behavior<boolean>;
// header/footer visibility // header/footer visibility
public showHeader$: Behavior<boolean>; showHeader$: Behavior<boolean>;
public showFooter$: Behavior<boolean>; showFooter$: Behavior<boolean>;
// audio routing // audio routing
/** /**
* Whether audio is currently being output through the earpiece. * Whether audio is currently being output through the earpiece.
*/ */
public earpieceMode$: Behavior<boolean>; earpieceMode$: Behavior<boolean>;
/** /**
* Callback to toggle between the earpiece and the loudspeaker. * Callback to toggle between the earpiece and the loudspeaker.
* *
* This will be `null` in case the target does not exist in the list * This will be `null` in case the target does not exist in the list
* of available audio outputs. * of available audio outputs.
*/ */
public audioOutputSwitcher$: Behavior<{ audioOutputSwitcher$: Behavior<{
targetOutput: "earpiece" | "speaker"; targetOutput: "earpiece" | "speaker";
switch: () => void; switch: () => void;
} | null>; } | null>;
@@ -333,10 +334,17 @@ export class CallViewModel {
// down, for example, and we want to avoid making people worry that the app is // down, for example, and we want to avoid making people worry that the app is
// in a split-brained state. // in a split-brained state.
// DISCUSSION own membership manager ALSO this probably can be simplifis // DISCUSSION own membership manager ALSO this probably can be simplifis
public reconnecting$: Behavior<boolean>; reconnecting$: Behavior<boolean>;
}
// THIS has to be the last public field declaration /**
public constructor( * A view model providing all the application logic needed to show the in-call
* UI (may eventually be expanded to cover the lobby and feedback screens in the
* future).
*/
// Throughout this class and related code we must distinguish between MatrixRTC
// state and LiveKit state. We use the common terminology of room "members", RTC
// "memberships", and LiveKit "participants".
export function createCallViewModel$(
scope: ObservableScope, scope: ObservableScope,
// A call is permanently tied to a single Matrix room // A call is permanently tied to a single Matrix room
matrixRTCSession: MatrixRTCSession, matrixRTCSession: MatrixRTCSession,
@@ -347,7 +355,7 @@ export class CallViewModel {
handsRaisedSubject$: Observable<Record<string, RaisedHandInfo>>, handsRaisedSubject$: Observable<Record<string, RaisedHandInfo>>,
reactionsSubject$: Observable<Record<string, ReactionInfo>>, reactionsSubject$: Observable<Record<string, ReactionInfo>>,
trackProcessorState$: Behavior<ProcessorState>, trackProcessorState$: Behavior<ProcessorState>,
) { ): CallViewModel {
const userId = matrixRoom.client.getUserId()!; const userId = matrixRoom.client.getUserId()!;
const deviceId = matrixRoom.client.getDeviceId()!; const deviceId = matrixRoom.client.getDeviceId()!;
@@ -407,9 +415,7 @@ export class CallViewModel {
combineLatest( combineLatest(
[localTransport$, membershipsAndTransports.transports$], [localTransport$, membershipsAndTransports.transports$],
(localTransport, transports) => { (localTransport, transports) => {
const localTransportAsArray = localTransport const localTransportAsArray = localTransport ? [localTransport] : [];
? [localTransport]
: [];
return transports.mapInner((transports) => [ return transports.mapInner((transports) => [
...localTransportAsArray, ...localTransportAsArray,
...transports, ...transports,
@@ -457,8 +463,7 @@ export class CallViewModel {
(memberships) => (memberships) =>
memberships.value.find( memberships.value.find(
(membership) => (membership) =>
membership.userId === userId && membership.userId === userId && membership.deviceId === deviceId,
membership.deviceId === deviceId,
) ?? null, ) ?? null,
), ),
), ),
@@ -492,10 +497,7 @@ export class CallViewModel {
const { callPickupState$, autoLeave$ } = createCallNotificationLifecycle$({ const { callPickupState$, autoLeave$ } = createCallNotificationLifecycle$({
scope: scope, scope: scope,
memberships$: memberships$, memberships$: memberships$,
sentCallNotification$: createSentCallNotification$( sentCallNotification$: createSentCallNotification$(scope, matrixRTCSession),
scope,
matrixRTCSession,
),
receivedDecline$: createReceivedDecline$(matrixRoom), receivedDecline$: createReceivedDecline$(matrixRoom),
options: options, options: options,
localUser: { userId: userId, deviceId: deviceId }, localUser: { userId: userId, deviceId: deviceId },
@@ -516,8 +518,7 @@ export class CallViewModel {
matrixRoomMembers$.pipe( matrixRoomMembers$.pipe(
map( map(
(roomMembersMap) => (roomMembersMap) =>
roomMembersMap.size === 1 && roomMembersMap.size === 1 && roomMembersMap.get(userId) !== undefined,
roomMembersMap.get(userId) !== undefined,
), ),
), ),
); );
@@ -780,10 +781,7 @@ export class CallViewModel {
matrixLivekitMembers$.pipe(map((ms) => ms.value.length)), matrixLivekitMembers$.pipe(map((ms) => ms.value.length)),
); );
const leaveSoundEffect$ = combineLatest([ const leaveSoundEffect$ = combineLatest([callPickupState$, userMedia$]).pipe(
callPickupState$,
userMedia$,
]).pipe(
// Until the call is successful, do not play a leave sound. // Until the call is successful, do not play a leave sound.
// If callPickupState$ is null, then we always play the sound as it will not conflict with a decline sound. // If callPickupState$ is null, then we always play the sound as it will not conflict with a decline sound.
skipWhile(([c]) => c !== null && c !== "success"), skipWhile(([c]) => c !== null && c !== "success"),
@@ -869,9 +867,7 @@ export class CallViewModel {
return bins.length === 0 return bins.length === 0
? of([]) ? of([])
: combineLatest(bins, (...bins) => : combineLatest(bins, (...bins) =>
bins bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
.sort(([, bin1], [, bin2]) => bin1 - bin2)
.map(([m]) => m.vm),
); );
}), }),
distinctUntilChanged(shallowEquals), distinctUntilChanged(shallowEquals),
@@ -1092,9 +1088,7 @@ export class CallViewModel {
oneOnOne === null oneOnOne === null
? combineLatest([grid$, spotlight$], (grid, spotlight) => ? combineLatest([grid$, spotlight$], (grid, spotlight) =>
grid.length > smallMobileCallThreshold || grid.length > smallMobileCallThreshold ||
spotlight.some( spotlight.some((vm) => vm instanceof ScreenShareViewModel)
(vm) => vm instanceof ScreenShareViewModel,
)
? spotlightPortraitLayoutMedia$ ? spotlightPortraitLayoutMedia$
: gridLayoutMedia$, : gridLayoutMedia$,
).pipe(switchAll()) ).pipe(switchAll())
@@ -1130,9 +1124,7 @@ export class CallViewModel {
const visibleTiles$ = new Subject<number>(); const visibleTiles$ = new Subject<number>();
const setVisibleTiles = (value: number): void => visibleTiles$.next(value); const setVisibleTiles = (value: number): void => visibleTiles$.next(value);
const layoutInternals$ = scope.behavior< const layoutInternals$ = scope.behavior<LayoutScanState & { layout: Layout }>(
LayoutScanState & { layout: Layout }
>(
combineLatest([ combineLatest([
layoutMedia$, layoutMedia$,
visibleTiles$.pipe(startWith(0), distinctUntilChanged()), visibleTiles$.pipe(startWith(0), distinctUntilChanged()),
@@ -1309,10 +1301,7 @@ export class CallViewModel {
*/ */
const earpieceMode$ = scope.behavior<boolean>( const earpieceMode$ = scope.behavior<boolean>(
combineLatest( combineLatest(
[ [mediaDevices.audioOutput.available$, mediaDevices.audioOutput.selected$],
mediaDevices.audioOutput.available$,
mediaDevices.audioOutput.selected$,
],
(available, selected) => (available, selected) =>
selected !== undefined && selected !== undefined &&
available.get(selected.id)?.type === "earpiece", available.get(selected.id)?.type === "earpiece",
@@ -1330,10 +1319,7 @@ export class CallViewModel {
switch: () => void; switch: () => void;
} | null>( } | null>(
combineLatest( combineLatest(
[ [mediaDevices.audioOutput.available$, mediaDevices.audioOutput.selected$],
mediaDevices.audioOutput.available$,
mediaDevices.audioOutput.selected$,
],
(available, selected) => { (available, selected) => {
const selectionType = selected && available.get(selected.id)?.type; const selectionType = selected && available.get(selected.id)?.type;
@@ -1366,8 +1352,7 @@ export class CallViewModel {
Record<string, ReactionOption>, Record<string, ReactionOption>,
{ sender: string; emoji: string; startX: number }[] { sender: string; emoji: string; startX: number }[]
>((acc, latest) => { >((acc, latest) => {
const newSet: { sender: string; emoji: string; startX: number }[] = const newSet: { sender: string; emoji: string; startX: number }[] = [];
[];
for (const [sender, reaction] of Object.entries(latest)) { for (const [sender, reaction] of Object.entries(latest)) {
const startX = const startX =
acc.find((v) => v.sender === sender && v.emoji)?.startX ?? acc.find((v) => v.sender === sender && v.emoji)?.startX ??
@@ -1440,53 +1425,54 @@ export class CallViewModel {
const toggleScreenSharing = localMembership.toggleScreenSharing; const toggleScreenSharing = localMembership.toggleScreenSharing;
const join = localMembership.requestConnect; const join = localMembership.requestConnect;
join(); // TODO-MULTI-SFU: Use this view model for the lobby as well, and only call this once 'join' is clicked? // TODO-MULTI-SFU: Use this view model for the lobby as well, and only call this once 'join' is clicked?
join();
return {
autoLeave$: autoLeave$,
callPickupState$: callPickupState$,
ringOverlay$: ringOverlay$,
leave$: leave$,
hangup: (): void => userHangup$.next(),
join: join,
toggleScreenSharing: toggleScreenSharing,
sharingScreen$: sharingScreen$,
this.autoLeave$ = autoLeave$; tapScreen: (): void => screenTap$.next(),
this.callPickupState$ = callPickupState$; tapControls: (): void => controlsTap$.next(),
this.ringOverlay$ = ringOverlay$; hoverScreen: (): void => screenHover$.next(),
this.leave$ = leave$; unhoverScreen: (): void => screenUnhover$.next(),
this.hangup = (): void => userHangup$.next();
this.join = join;
this.toggleScreenSharing = toggleScreenSharing;
this.sharingScreen$ = sharingScreen$;
this.tapScreen = (): void => screenTap$.next(); configError$: localMembership.configError$,
this.tapControls = (): void => controlsTap$.next(); participantCount$: participantCount$,
this.hoverScreen = (): void => screenHover$.next(); audioParticipants$: audioParticipants$,
this.unhoverScreen = (): void => screenUnhover$.next();
this.configError$ = localMembership.configError$; handsRaised$: handsRaised$,
this.participantCount$ = participantCount$; reactions$: reactions$,
this.audioParticipants$ = audioParticipants$; joinSoundEffect$: joinSoundEffect$,
leaveSoundEffect$: leaveSoundEffect$,
newHandRaised$: newHandRaised$,
newScreenShare$: newScreenShare$,
audibleReactions$: audibleReactions$,
visibleReactions$: visibleReactions$,
this.handsRaised$ = handsRaised$; windowMode$: windowMode$,
this.reactions$ = reactions$; spotlightExpanded$: spotlightExpanded$,
this.joinSoundEffect$ = joinSoundEffect$; toggleSpotlightExpanded$: toggleSpotlightExpanded$,
this.leaveSoundEffect$ = leaveSoundEffect$; gridMode$: gridMode$,
this.newHandRaised$ = newHandRaised$; setGridMode: setGridMode,
this.newScreenShare$ = newScreenShare$; grid$: grid$,
this.audibleReactions$ = audibleReactions$; spotlight$: spotlight$,
this.visibleReactions$ = visibleReactions$; pip$: pip$,
layout$: layout$,
this.windowMode$ = windowMode$; tileStoreGeneration$: tileStoreGeneration$,
this.spotlightExpanded$ = spotlightExpanded$; showSpotlightIndicators$: showSpotlightIndicators$,
this.toggleSpotlightExpanded$ = toggleSpotlightExpanded$; showSpeakingIndicators$: showSpeakingIndicators$,
this.gridMode$ = gridMode$; showHeader$: showHeader$,
this.setGridMode = setGridMode; showFooter$: showFooter$,
this.grid$ = grid$; earpieceMode$: earpieceMode$,
this.spotlight$ = spotlight$; audioOutputSwitcher$: audioOutputSwitcher$,
this.pip$ = pip$; reconnecting$: reconnecting$,
this.layout$ = layout$; };
this.tileStoreGeneration$ = tileStoreGeneration$;
this.showSpotlightIndicators$ = showSpotlightIndicators$;
this.showSpeakingIndicators$ = showSpeakingIndicators$;
this.showHeader$ = showHeader$;
this.showFooter$ = showFooter$;
this.earpieceMode$ = earpieceMode$;
this.audioOutputSwitcher$ = audioOutputSwitcher$;
this.reconnecting$ = reconnecting$;
}
} }
// TODO-MULTI-SFU // Setup and update the keyProvider which was create by `createRoom` was a thing before. Now we never update if the E2EEsystem changes // TODO-MULTI-SFU // Setup and update the keyProvider which was create by `createRoom` was a thing before. Now we never update if the E2EEsystem changes
// do we need this? // do we need this?

View File

@@ -24,7 +24,11 @@ import * as ComponentsCore from "@livekit/components-core";
import type { CallMembership } from "matrix-js-sdk/lib/matrixrtc"; import type { CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { E2eeType } from "../../e2ee/e2eeType"; import { E2eeType } from "../../e2ee/e2eeType";
import { type RaisedHandInfo, type ReactionInfo } from "../../reactions"; import { type RaisedHandInfo, type ReactionInfo } from "../../reactions";
import { CallViewModel, type CallViewModelOptions } from "./CallViewModel"; import {
type CallViewModel,
createCallViewModel$,
type CallViewModelOptions,
} from "./CallViewModel";
import { import {
mockConfig, mockConfig,
mockLivekitRoom, mockLivekitRoom,
@@ -154,7 +158,7 @@ export function withCallViewModel(
const raisedHands$ = new BehaviorSubject<Record<string, RaisedHandInfo>>({}); const raisedHands$ = new BehaviorSubject<Record<string, RaisedHandInfo>>({});
const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({}); const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({});
const vm = new CallViewModel( const vm = createCallViewModel$(
testScope(), testScope(),
rtcSession.asMockedSession(), rtcSession.asMockedSession(),
room, room,

View File

@@ -20,7 +20,8 @@ import { ConnectionState, type Room as LivekitRoom } from "livekit-client";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { import {
CallViewModel, type CallViewModel,
createCallViewModel$,
type CallViewModelOptions, type CallViewModelOptions,
} from "../state/CallViewModel/CallViewModel"; } from "../state/CallViewModel/CallViewModel";
import { import {
@@ -145,7 +146,7 @@ export function getBasicCallViewModelEnvironment(
// const remoteParticipants$ = of([aliceParticipant]); // const remoteParticipants$ = of([aliceParticipant]);
const vm = new CallViewModel( const vm = createCallViewModel$(
testScope(), testScope(),
rtcSession.asMockedSession(), rtcSession.asMockedSession(),
matrixRoom, matrixRoom,