|
|
|
@@ -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?
|
|
|
|
|