mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-10 23:25:52 +00:00
Extract the layout decision into a pure function
This commit is contained in:
@@ -93,17 +93,16 @@ import { type HostBridge, nullHostBridge } from "../../HostBridge";
|
|||||||
import {
|
import {
|
||||||
layoutShallowEquals,
|
layoutShallowEquals,
|
||||||
type Alignment,
|
type Alignment,
|
||||||
type GridLayoutMedia,
|
|
||||||
type Layout,
|
type Layout,
|
||||||
type LayoutMedia,
|
type LayoutMedia,
|
||||||
type OneOnOneDesktopLayoutMedia,
|
|
||||||
type OneOnOneMobileLayoutMedia,
|
|
||||||
type SpotlightExpandedLayoutMedia,
|
|
||||||
type SpotlightLandscapeLayoutMedia,
|
|
||||||
type SpotlightPortraitLayoutMedia,
|
|
||||||
type WindowMode,
|
type WindowMode,
|
||||||
} from "../layout-types.ts";
|
} from "../layout-types.ts";
|
||||||
import { chooseSpotlightSpeaker, computeSpotlight } from "../layoutMedia.ts";
|
import {
|
||||||
|
chooseSpotlightSpeaker,
|
||||||
|
computeLayoutMedia,
|
||||||
|
computeSpotlight,
|
||||||
|
type OneOnOneMedia,
|
||||||
|
} from "../layoutMedia.ts";
|
||||||
import { ElementCallError, UnknownCallError } from "../../utils/errors.ts";
|
import { ElementCallError, UnknownCallError } from "../../utils/errors.ts";
|
||||||
import { type Epoch, type ObservableScope } from "../ObservableScope.ts";
|
import { type Epoch, type ObservableScope } from "../ObservableScope.ts";
|
||||||
import { createHomeserverConnected$ } from "./localMember/HomeserverConnected.ts";
|
import { createHomeserverConnected$ } from "./localMember/HomeserverConnected.ts";
|
||||||
@@ -147,7 +146,6 @@ import {
|
|||||||
import { Publisher } from "./localMember/Publisher.ts";
|
import { Publisher } from "./localMember/Publisher.ts";
|
||||||
import { type Connection } from "./remoteMembers/Connection.ts";
|
import { type Connection } from "./remoteMembers/Connection.ts";
|
||||||
import {
|
import {
|
||||||
type LayoutMode,
|
|
||||||
type LayoutSwitchViewModel,
|
type LayoutSwitchViewModel,
|
||||||
createLayoutSwitchViewModel,
|
createLayoutSwitchViewModel,
|
||||||
} from "../LayoutSwitchViewModel.ts";
|
} from "../LayoutSwitchViewModel.ts";
|
||||||
@@ -265,10 +263,6 @@ export function callViewModelOptionsFromParams(
|
|||||||
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
|
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
|
||||||
export const THROTTLE_SOUND_EFFECT_MS = 500;
|
export const THROTTLE_SOUND_EFFECT_MS = 500;
|
||||||
|
|
||||||
// This is the number of participants that we think constitutes a "small" call
|
|
||||||
// on mobile. No spotlight tile should be shown below this threshold.
|
|
||||||
const smallMobileCallThreshold = 3;
|
|
||||||
|
|
||||||
// How long the footer should be shown for when hovering over or interacting
|
// How long the footer should be shown for when hovering over or interacting
|
||||||
// with the interface
|
// with the interface
|
||||||
const showFooterMs = 4000;
|
const showFooterMs = 4000;
|
||||||
@@ -1773,133 +1767,3 @@ function getE2eeKeyProvider(
|
|||||||
return keyProvider;
|
return keyProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OneOnOneMedia {
|
|
||||||
local: LocalUserMediaViewModel;
|
|
||||||
remote: UserMediaViewModel | RingingMediaViewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LayoutMediaInputs {
|
|
||||||
windowMode: WindowMode;
|
|
||||||
/** The layout chosen with the layout switch. */
|
|
||||||
layoutMode: LayoutMode;
|
|
||||||
spotlightExpanded: boolean;
|
|
||||||
/** Set when the call qualifies for a one-on-one layout. */
|
|
||||||
oneOnOne: OneOnOneMedia | null;
|
|
||||||
localVideoEnabled: boolean;
|
|
||||||
spotlight: MediaViewModel[];
|
|
||||||
grid: UserMediaViewModel[];
|
|
||||||
pip: UserMediaViewModel | undefined;
|
|
||||||
desktop: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides which layout to use and which media goes where in it.
|
|
||||||
*/
|
|
||||||
function computeLayoutMedia({
|
|
||||||
windowMode,
|
|
||||||
layoutMode,
|
|
||||||
spotlightExpanded,
|
|
||||||
oneOnOne,
|
|
||||||
localVideoEnabled,
|
|
||||||
spotlight,
|
|
||||||
grid,
|
|
||||||
pip,
|
|
||||||
desktop,
|
|
||||||
}: LayoutMediaInputs): LayoutMedia {
|
|
||||||
switch (windowMode) {
|
|
||||||
case "normal":
|
|
||||||
if (layoutMode === "grid")
|
|
||||||
return oneOnOne === null
|
|
||||||
? gridLayoutMedia(spotlight, grid)
|
|
||||||
: oneOnOneDesktopLayoutMedia(oneOnOne);
|
|
||||||
return spotlightExpanded
|
|
||||||
? spotlightExpandedLayoutMedia(false, spotlight, pip)
|
|
||||||
: spotlightLandscapeLayoutMedia(false, spotlight, grid);
|
|
||||||
case "narrow":
|
|
||||||
if (oneOnOne !== null)
|
|
||||||
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
|
||||||
return grid.length > smallMobileCallThreshold || hasScreenShare(spotlight)
|
|
||||||
? spotlightPortraitLayoutMedia(spotlight, grid)
|
|
||||||
: gridLayoutMedia(spotlight, grid);
|
|
||||||
case "flat":
|
|
||||||
if (oneOnOne !== null)
|
|
||||||
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
|
||||||
// Yes, grid mode actually gets you a "spotlight" layout in this window
|
|
||||||
// mode.
|
|
||||||
return layoutMode === "grid"
|
|
||||||
? spotlightLandscapeLayoutMedia(true, spotlight, grid)
|
|
||||||
: spotlightExpandedLayoutMedia(true, spotlight, pip);
|
|
||||||
case "pip":
|
|
||||||
return { type: "pip", edgeToEdge: !desktop, spotlight };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasScreenShare(spotlight: MediaViewModel[]): boolean {
|
|
||||||
return spotlight.some((vm) => vm.type === "screen share");
|
|
||||||
}
|
|
||||||
|
|
||||||
function gridLayoutMedia(
|
|
||||||
spotlight: MediaViewModel[],
|
|
||||||
grid: UserMediaViewModel[],
|
|
||||||
): GridLayoutMedia {
|
|
||||||
return {
|
|
||||||
type: "grid",
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: hasScreenShare(spotlight) ? spotlight : undefined,
|
|
||||||
grid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function oneOnOneDesktopLayoutMedia(
|
|
||||||
media: OneOnOneMedia,
|
|
||||||
): OneOnOneDesktopLayoutMedia {
|
|
||||||
return media.remote.type === "ringing"
|
|
||||||
? {
|
|
||||||
type: "one-on-one-desktop",
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: media.local,
|
|
||||||
pip: media.remote,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "one-on-one-desktop",
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: media.remote,
|
|
||||||
pip: media.local,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function spotlightExpandedLayoutMedia(
|
|
||||||
edgeToEdge: boolean,
|
|
||||||
spotlight: MediaViewModel[],
|
|
||||||
pip: UserMediaViewModel | undefined,
|
|
||||||
): SpotlightExpandedLayoutMedia {
|
|
||||||
return { type: "spotlight-expanded", edgeToEdge, spotlight, pip };
|
|
||||||
}
|
|
||||||
|
|
||||||
function spotlightLandscapeLayoutMedia(
|
|
||||||
edgeToEdge: boolean,
|
|
||||||
spotlight: MediaViewModel[],
|
|
||||||
grid: UserMediaViewModel[],
|
|
||||||
): SpotlightLandscapeLayoutMedia {
|
|
||||||
return { type: "spotlight-landscape", edgeToEdge, spotlight, grid };
|
|
||||||
}
|
|
||||||
|
|
||||||
function oneOnOneMobileLayoutMedia(
|
|
||||||
media: OneOnOneMedia,
|
|
||||||
localVideoEnabled: boolean,
|
|
||||||
): OneOnOneMobileLayoutMedia {
|
|
||||||
return {
|
|
||||||
type: "one-on-one-mobile",
|
|
||||||
edgeToEdge: true,
|
|
||||||
spotlight: media.remote,
|
|
||||||
pip: localVideoEnabled ? media.local : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function spotlightPortraitLayoutMedia(
|
|
||||||
spotlight: MediaViewModel[],
|
|
||||||
grid: UserMediaViewModel[],
|
|
||||||
): SpotlightPortraitLayoutMedia {
|
|
||||||
return { type: "spotlight-portrait", edgeToEdge: false, spotlight, grid };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { chooseSpotlightSpeaker, computeSpotlight } from "./layoutMedia";
|
import {
|
||||||
|
chooseSpotlightSpeaker,
|
||||||
|
computeLayoutMedia,
|
||||||
|
computeSpotlight,
|
||||||
|
type LayoutMediaInputs,
|
||||||
|
} from "./layoutMedia";
|
||||||
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel";
|
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel";
|
||||||
import { type RemoteUserMediaViewModel } from "./media/RemoteUserMediaViewModel";
|
import { type RemoteUserMediaViewModel } from "./media/RemoteUserMediaViewModel";
|
||||||
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
||||||
@@ -110,3 +115,106 @@ describe("computeSpotlight", () => {
|
|||||||
).toEqual({ spotlight: [local], pip: undefined });
|
).toEqual({ spotlight: [local], pip: undefined });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("computeLayoutMedia", () => {
|
||||||
|
const base: LayoutMediaInputs = {
|
||||||
|
windowMode: "normal",
|
||||||
|
layoutMode: "grid",
|
||||||
|
spotlightExpanded: false,
|
||||||
|
oneOnOne: null,
|
||||||
|
localVideoEnabled: true,
|
||||||
|
spotlight: [alice],
|
||||||
|
grid: [local, alice, bob],
|
||||||
|
pip: local,
|
||||||
|
desktop: false,
|
||||||
|
};
|
||||||
|
const layout = (
|
||||||
|
overrides: Partial<LayoutMediaInputs>,
|
||||||
|
): ReturnType<typeof computeLayoutMedia> =>
|
||||||
|
computeLayoutMedia({ ...base, ...overrides });
|
||||||
|
|
||||||
|
it("uses a plain grid unless a screen share needs the spotlight", () => {
|
||||||
|
expect(layout({})).toEqual({
|
||||||
|
type: "grid",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: undefined,
|
||||||
|
grid: base.grid,
|
||||||
|
});
|
||||||
|
expect(layout({ spotlight: [screenShare] }).spotlight).toEqual([
|
||||||
|
screenShare,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches between landscape and expanded spotlight", () => {
|
||||||
|
expect(layout({ layoutMode: "spotlight" })).toMatchObject({
|
||||||
|
type: "spotlight-landscape",
|
||||||
|
edgeToEdge: false,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
layout({ layoutMode: "spotlight", spotlightExpanded: true }),
|
||||||
|
).toEqual({
|
||||||
|
type: "spotlight-expanded",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: [alice],
|
||||||
|
pip: local,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts the remote user in the desktop one-on-one spotlight, or the local user when ringing", () => {
|
||||||
|
expect(layout({ oneOnOne: { local, remote: alice } })).toEqual({
|
||||||
|
type: "one-on-one-desktop",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: alice,
|
||||||
|
pip: local,
|
||||||
|
});
|
||||||
|
expect(layout({ oneOnOne: { local, remote: ringing } })).toMatchObject({
|
||||||
|
spotlight: local,
|
||||||
|
pip: ringing,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a mobile one-on-one layout in narrow and flat windows, hiding the PiP without video", () => {
|
||||||
|
const oneOnOne = { local, remote: alice };
|
||||||
|
expect(layout({ windowMode: "narrow", oneOnOne })).toEqual({
|
||||||
|
type: "one-on-one-mobile",
|
||||||
|
edgeToEdge: true,
|
||||||
|
spotlight: alice,
|
||||||
|
pip: local,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
layout({ windowMode: "flat", oneOnOne, localVideoEnabled: false }),
|
||||||
|
).toMatchObject({ type: "one-on-one-mobile", pip: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a portrait spotlight in narrow windows for big calls or screen shares", () => {
|
||||||
|
expect(layout({ windowMode: "narrow" }).type).toBe("grid");
|
||||||
|
expect(
|
||||||
|
layout({ windowMode: "narrow", grid: [local, alice, bob, carol] }).type,
|
||||||
|
).toBe("spotlight-portrait");
|
||||||
|
expect(
|
||||||
|
layout({ windowMode: "narrow", spotlight: [screenShare] }).type,
|
||||||
|
).toBe("spotlight-portrait");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses edge-to-edge spotlight layouts in flat windows", () => {
|
||||||
|
expect(layout({ windowMode: "flat" })).toMatchObject({
|
||||||
|
type: "spotlight-landscape",
|
||||||
|
edgeToEdge: true,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
layout({ windowMode: "flat", layoutMode: "spotlight" }),
|
||||||
|
).toMatchObject({
|
||||||
|
type: "spotlight-expanded",
|
||||||
|
edgeToEdge: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the PiP layout, edge to edge except on desktop", () => {
|
||||||
|
expect(layout({ windowMode: "pip" })).toEqual({
|
||||||
|
type: "pip",
|
||||||
|
edgeToEdge: true,
|
||||||
|
spotlight: [alice],
|
||||||
|
});
|
||||||
|
expect(layout({ windowMode: "pip", desktop: true }).edgeToEdge).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,11 +5,29 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { type LayoutMode } from "./LayoutSwitchViewModel.ts";
|
||||||
|
import {
|
||||||
|
type GridLayoutMedia,
|
||||||
|
type LayoutMedia,
|
||||||
|
type OneOnOneDesktopLayoutMedia,
|
||||||
|
type OneOnOneMobileLayoutMedia,
|
||||||
|
type SpotlightExpandedLayoutMedia,
|
||||||
|
type SpotlightLandscapeLayoutMedia,
|
||||||
|
type SpotlightPortraitLayoutMedia,
|
||||||
|
type WindowMode,
|
||||||
|
} from "./layout-types.ts";
|
||||||
|
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel.ts";
|
||||||
import { type MediaViewModel } from "./media/MediaViewModel.ts";
|
import { type MediaViewModel } from "./media/MediaViewModel.ts";
|
||||||
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel.ts";
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel.ts";
|
||||||
import { type ScreenShareViewModel } from "./media/ScreenShareViewModel.ts";
|
import { type ScreenShareViewModel } from "./media/ScreenShareViewModel.ts";
|
||||||
import { type UserMediaViewModel } from "./media/UserMediaViewModel.ts";
|
import { type UserMediaViewModel } from "./media/UserMediaViewModel.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the number of participants that we think constitutes a "small" call
|
||||||
|
* on mobile. No spotlight tile should be shown below this threshold.
|
||||||
|
*/
|
||||||
|
const smallMobileCallThreshold = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks who to spotlight based on who is speaking, preferring the previous
|
* Picks who to spotlight based on who is speaking, preferring the previous
|
||||||
* pick so that the spotlight doesn't flip eagerly between speakers.
|
* pick so that the spotlight doesn't flip eagerly between speakers.
|
||||||
@@ -66,3 +84,133 @@ export function computeSpotlight({
|
|||||||
pip: localPip === speaker ? undefined : localPip,
|
pip: localPip === speaker ? undefined : localPip,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OneOnOneMedia {
|
||||||
|
local: LocalUserMediaViewModel;
|
||||||
|
remote: UserMediaViewModel | RingingMediaViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayoutMediaInputs {
|
||||||
|
windowMode: WindowMode;
|
||||||
|
/** The layout chosen with the layout switch. */
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
spotlightExpanded: boolean;
|
||||||
|
/** Set when the call qualifies for a one-on-one layout. */
|
||||||
|
oneOnOne: OneOnOneMedia | null;
|
||||||
|
localVideoEnabled: boolean;
|
||||||
|
spotlight: MediaViewModel[];
|
||||||
|
grid: UserMediaViewModel[];
|
||||||
|
pip: UserMediaViewModel | undefined;
|
||||||
|
desktop: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides which layout to use and which media goes where in it.
|
||||||
|
*/
|
||||||
|
export function computeLayoutMedia({
|
||||||
|
windowMode,
|
||||||
|
layoutMode,
|
||||||
|
spotlightExpanded,
|
||||||
|
oneOnOne,
|
||||||
|
localVideoEnabled,
|
||||||
|
spotlight,
|
||||||
|
grid,
|
||||||
|
pip,
|
||||||
|
desktop,
|
||||||
|
}: LayoutMediaInputs): LayoutMedia {
|
||||||
|
switch (windowMode) {
|
||||||
|
case "normal":
|
||||||
|
if (layoutMode === "grid")
|
||||||
|
return oneOnOne === null
|
||||||
|
? gridLayoutMedia(spotlight, grid)
|
||||||
|
: oneOnOneDesktopLayoutMedia(oneOnOne);
|
||||||
|
return spotlightExpanded
|
||||||
|
? spotlightExpandedLayoutMedia(false, spotlight, pip)
|
||||||
|
: spotlightLandscapeLayoutMedia(false, spotlight, grid);
|
||||||
|
case "narrow":
|
||||||
|
if (oneOnOne !== null)
|
||||||
|
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
||||||
|
return grid.length > smallMobileCallThreshold || hasScreenShare(spotlight)
|
||||||
|
? spotlightPortraitLayoutMedia(spotlight, grid)
|
||||||
|
: gridLayoutMedia(spotlight, grid);
|
||||||
|
case "flat":
|
||||||
|
if (oneOnOne !== null)
|
||||||
|
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
||||||
|
// Yes, grid mode actually gets you a "spotlight" layout in this window
|
||||||
|
// mode.
|
||||||
|
return layoutMode === "grid"
|
||||||
|
? spotlightLandscapeLayoutMedia(true, spotlight, grid)
|
||||||
|
: spotlightExpandedLayoutMedia(true, spotlight, pip);
|
||||||
|
case "pip":
|
||||||
|
return { type: "pip", edgeToEdge: !desktop, spotlight };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasScreenShare(spotlight: MediaViewModel[]): boolean {
|
||||||
|
return spotlight.some((vm) => vm.type === "screen share");
|
||||||
|
}
|
||||||
|
|
||||||
|
function gridLayoutMedia(
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): GridLayoutMedia {
|
||||||
|
return {
|
||||||
|
type: "grid",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: hasScreenShare(spotlight) ? spotlight : undefined,
|
||||||
|
grid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function oneOnOneDesktopLayoutMedia(
|
||||||
|
media: OneOnOneMedia,
|
||||||
|
): OneOnOneDesktopLayoutMedia {
|
||||||
|
return media.remote.type === "ringing"
|
||||||
|
? {
|
||||||
|
type: "one-on-one-desktop",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: media.local,
|
||||||
|
pip: media.remote,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "one-on-one-desktop",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: media.remote,
|
||||||
|
pip: media.local,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightExpandedLayoutMedia(
|
||||||
|
edgeToEdge: boolean,
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
pip: UserMediaViewModel | undefined,
|
||||||
|
): SpotlightExpandedLayoutMedia {
|
||||||
|
return { type: "spotlight-expanded", edgeToEdge, spotlight, pip };
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightLandscapeLayoutMedia(
|
||||||
|
edgeToEdge: boolean,
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): SpotlightLandscapeLayoutMedia {
|
||||||
|
return { type: "spotlight-landscape", edgeToEdge, spotlight, grid };
|
||||||
|
}
|
||||||
|
|
||||||
|
function oneOnOneMobileLayoutMedia(
|
||||||
|
media: OneOnOneMedia,
|
||||||
|
localVideoEnabled: boolean,
|
||||||
|
): OneOnOneMobileLayoutMedia {
|
||||||
|
return {
|
||||||
|
type: "one-on-one-mobile",
|
||||||
|
edgeToEdge: true,
|
||||||
|
spotlight: media.remote,
|
||||||
|
pip: localVideoEnabled ? media.local : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightPortraitLayoutMedia(
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): SpotlightPortraitLayoutMedia {
|
||||||
|
return { type: "spotlight-portrait", edgeToEdge: false, spotlight, grid };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user