mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Factor out a layout switch view + view model
So it can easily be shown and hidden wholesale.
This commit is contained in:
@@ -327,8 +327,8 @@ describe.each([
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
g: () => vm.setGridMode("grid"),
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
g: () => vm.layoutSwitchVm$.value!.setLayout("grid"),
|
||||
});
|
||||
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||
@@ -815,7 +815,9 @@ describe.each([
|
||||
]),
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
||||
schedule(modeInputMarbles, {
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
});
|
||||
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
@@ -1021,7 +1023,7 @@ describe.each([
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
});
|
||||
schedule(expandInputMarbles, {
|
||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||
@@ -1091,7 +1093,7 @@ describe.each([
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
});
|
||||
schedule(expandInputMarbles, {
|
||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||
@@ -1131,7 +1133,7 @@ describe.each([
|
||||
},
|
||||
(vm) => {
|
||||
schedule("s", {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
});
|
||||
schedule("a", {
|
||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||
@@ -1168,8 +1170,8 @@ describe.each([
|
||||
},
|
||||
(vm) => {
|
||||
schedule(modeInputMarbles, {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
g: () => vm.setGridMode("grid"),
|
||||
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||
g: () => vm.layoutSwitchVm$.value!.setLayout("grid"),
|
||||
});
|
||||
schedule(expandInputMarbles, {
|
||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||
@@ -1235,7 +1237,7 @@ describe.each([
|
||||
]),
|
||||
},
|
||||
(vm) => {
|
||||
vm.setGridMode("grid");
|
||||
vm.layoutSwitchVm$.value!.setLayout("grid");
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
@@ -1278,7 +1280,7 @@ describe.each([
|
||||
}),
|
||||
},
|
||||
(vm) => {
|
||||
vm.setGridMode("grid");
|
||||
vm.layoutSwitchVm$.value!.setLayout("grid");
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
|
||||
@@ -141,7 +141,10 @@ import {
|
||||
} from "./remoteMembers/MatrixMemberMetadata.ts";
|
||||
import { Publisher } from "./localMember/Publisher.ts";
|
||||
import { type Connection } from "./remoteMembers/Connection.ts";
|
||||
import { createLayoutModeSwitch } from "./LayoutSwitch.ts";
|
||||
import {
|
||||
type LayoutSwitchViewModel,
|
||||
createLayoutSwitchViewModel,
|
||||
} from "../LayoutSwitchViewModel.ts";
|
||||
import {
|
||||
createWrappedUserMedia,
|
||||
type WrappedUserMediaViewModel,
|
||||
@@ -201,8 +204,6 @@ const smallMobileCallThreshold = 3;
|
||||
// with the interface
|
||||
const showFooterMs = 4000;
|
||||
|
||||
export type GridMode = "grid" | "spotlight";
|
||||
|
||||
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
||||
|
||||
interface LayoutScanState {
|
||||
@@ -349,8 +350,7 @@ export interface CallViewModel {
|
||||
showNameTags$: Behavior<boolean>;
|
||||
spotlightExpanded$: Behavior<boolean>;
|
||||
toggleSpotlightExpanded$: Behavior<(() => void) | null>;
|
||||
gridMode$: Behavior<GridMode>;
|
||||
setGridMode: (value: GridMode) => void;
|
||||
layoutSwitchVm$: Behavior<LayoutSwitchViewModel | null>;
|
||||
|
||||
// header/footer visibility
|
||||
showHeader$: Behavior<boolean>;
|
||||
@@ -1056,7 +1056,7 @@ export function createCallViewModel$(
|
||||
spotlightExpandedToggle$,
|
||||
);
|
||||
|
||||
const { setGridMode, gridMode$ } = createLayoutModeSwitch(
|
||||
const layoutSwitchVm = createLayoutSwitchViewModel(
|
||||
scope,
|
||||
windowMode$,
|
||||
hasRemoteScreenShares$,
|
||||
@@ -1223,9 +1223,9 @@ export function createCallViewModel$(
|
||||
switchMap((windowMode) => {
|
||||
switch (windowMode) {
|
||||
case "normal":
|
||||
return gridMode$.pipe(
|
||||
switchMap((gridMode) => {
|
||||
switch (gridMode) {
|
||||
return layoutSwitchVm.layout$.pipe(
|
||||
switchMap((layout) => {
|
||||
switch (layout) {
|
||||
case "grid":
|
||||
return oneOnOneDesktopLayoutMedia$.pipe(
|
||||
switchMap((oneOnOne) =>
|
||||
@@ -1260,9 +1260,9 @@ export function createCallViewModel$(
|
||||
return oneOnOneMobileLayoutMedia$.pipe(
|
||||
switchMap((oneOnOne) =>
|
||||
oneOnOne === null
|
||||
? gridMode$.pipe(
|
||||
switchMap((gridMode) => {
|
||||
switch (gridMode) {
|
||||
? layoutSwitchVm.layout$.pipe(
|
||||
switchMap((layout) => {
|
||||
switch (layout) {
|
||||
case "grid":
|
||||
// Yes, grid mode actually gets you a "spotlight" layout in
|
||||
// this window mode.
|
||||
@@ -1775,8 +1775,7 @@ export function createCallViewModel$(
|
||||
|
||||
spotlightExpanded$: spotlightExpanded$,
|
||||
toggleSpotlightExpanded$: toggleSpotlightExpanded$,
|
||||
gridMode$: gridMode$,
|
||||
setGridMode: setGridMode,
|
||||
layoutSwitchVm$: constant(layoutSwitchVm),
|
||||
layout$: layout$,
|
||||
localMatrixLivekitMember$,
|
||||
remoteMatrixLivekitMembers$: scope.behavior(
|
||||
|
||||
@@ -7,31 +7,31 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { describe, test } from "vitest";
|
||||
|
||||
import { createLayoutModeSwitch } from "./LayoutSwitch";
|
||||
import { testScope, withTestScheduler } from "../../utils/test";
|
||||
import { createLayoutSwitchViewModel } from "./LayoutSwitchViewModel";
|
||||
import { testScope, withTestScheduler } from "../utils/test";
|
||||
|
||||
function testLayoutSwitch({
|
||||
windowMode = "n",
|
||||
hasScreenShares = "n",
|
||||
userSelection = "",
|
||||
expectedGridMode,
|
||||
expectedLayout,
|
||||
}: {
|
||||
windowMode?: string;
|
||||
hasScreenShares?: string;
|
||||
userSelection?: string;
|
||||
expectedGridMode: string;
|
||||
expectedLayout: string;
|
||||
}): void {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
const { gridMode$, setGridMode } = createLayoutModeSwitch(
|
||||
const { layout$, setLayout } = createLayoutSwitchViewModel(
|
||||
testScope(),
|
||||
behavior(windowMode, { n: "normal", N: "narrow", f: "flat" }),
|
||||
behavior(hasScreenShares, { y: true, n: false }),
|
||||
);
|
||||
schedule(userSelection, {
|
||||
g: () => setGridMode("grid"),
|
||||
s: () => setGridMode("spotlight"),
|
||||
g: () => setLayout("grid"),
|
||||
s: () => setLayout("spotlight"),
|
||||
});
|
||||
expectObservable(gridMode$).toBe(expectedGridMode, {
|
||||
expectObservable(layout$).toBe(expectedLayout, {
|
||||
g: "grid",
|
||||
s: "spotlight",
|
||||
});
|
||||
@@ -41,86 +41,86 @@ function testLayoutSwitch({
|
||||
describe("default mode", () => {
|
||||
test("uses grid layout in normal window", () =>
|
||||
testLayoutSwitch({
|
||||
windowMode: " n",
|
||||
expectedGridMode: "g",
|
||||
windowMode: " n",
|
||||
expectedLayout: "g",
|
||||
}));
|
||||
|
||||
test("uses grid layout in flat window", () =>
|
||||
testLayoutSwitch({
|
||||
windowMode: " f",
|
||||
expectedGridMode: "g",
|
||||
windowMode: " f",
|
||||
expectedLayout: "g",
|
||||
}));
|
||||
});
|
||||
|
||||
test("allows switching modes manually", () =>
|
||||
testLayoutSwitch({
|
||||
userSelection: " --sgs",
|
||||
expectedGridMode: "g-sgs",
|
||||
userSelection: " --sgs",
|
||||
expectedLayout: "g-sgs",
|
||||
}));
|
||||
|
||||
test("switches to spotlight mode when there is a remote screen share", () =>
|
||||
testLayoutSwitch({
|
||||
hasScreenShares: " n--y",
|
||||
expectedGridMode: "g--s",
|
||||
hasScreenShares: "n--y",
|
||||
expectedLayout: " g--s",
|
||||
}));
|
||||
|
||||
test("can manually switch to grid when there is a screenshare", () =>
|
||||
testLayoutSwitch({
|
||||
hasScreenShares: " n-y",
|
||||
userSelection: " ---g",
|
||||
expectedGridMode: "g-sg",
|
||||
hasScreenShares: "n-y",
|
||||
userSelection: " ---g",
|
||||
expectedLayout: " g-sg",
|
||||
}));
|
||||
|
||||
test("auto-switches after manually selecting grid", () =>
|
||||
testLayoutSwitch({
|
||||
// Two screenshares will happen in sequence. There is a screen share that
|
||||
// forces spotlight, then the user manually switches back to grid.
|
||||
hasScreenShares: " n-y-ny",
|
||||
userSelection: " ---g",
|
||||
expectedGridMode: "g-sg-s",
|
||||
hasScreenShares: "n-y-ny",
|
||||
userSelection: " ---g",
|
||||
expectedLayout: " g-sg-s",
|
||||
// If we did want to respect manual selection, the expectation would be: g-sg
|
||||
}));
|
||||
|
||||
test("switches back to grid mode when the remote screen share ends", () =>
|
||||
testLayoutSwitch({
|
||||
hasScreenShares: " n--y--n",
|
||||
expectedGridMode: "g--s--g",
|
||||
hasScreenShares: "n--y--n",
|
||||
expectedLayout: " g--s--g",
|
||||
}));
|
||||
|
||||
test("auto-switches to spotlight again after first screen share ends", () =>
|
||||
testLayoutSwitch({
|
||||
hasScreenShares: " nyny",
|
||||
expectedGridMode: "gsgs",
|
||||
hasScreenShares: "nyny",
|
||||
expectedLayout: " gsgs",
|
||||
}));
|
||||
|
||||
test("switches manually to grid after screen share while manually in spotlight", () =>
|
||||
testLayoutSwitch({
|
||||
// Initially, no one is sharing. Then the user manually switches to spotlight.
|
||||
// After a screen share starts, the user manually switches to grid.
|
||||
hasScreenShares: " n-y",
|
||||
userSelection: " -s-g",
|
||||
expectedGridMode: "gs-g",
|
||||
hasScreenShares: "n-y",
|
||||
userSelection: " -s-g",
|
||||
expectedLayout: " gs-g",
|
||||
}));
|
||||
|
||||
test("allows switching modes manually when in flat window mode", () =>
|
||||
testLayoutSwitch({
|
||||
// Window becomes flat, then user switches to spotlight and back.
|
||||
// Finally the window returns to a normal shape.
|
||||
windowMode: " nf--n",
|
||||
userSelection: " --sg",
|
||||
expectedGridMode: "g-sg",
|
||||
windowMode: " nf--n",
|
||||
userSelection: " --sg",
|
||||
expectedLayout: "g-sg",
|
||||
}));
|
||||
|
||||
test("switches to grid when in flat window mode even when there are screen shares", () =>
|
||||
testLayoutSwitch({
|
||||
windowMode: " nf",
|
||||
hasScreenShares: " y",
|
||||
expectedGridMode: "sg",
|
||||
windowMode: " nf",
|
||||
hasScreenShares: "y",
|
||||
expectedLayout: " sg",
|
||||
}));
|
||||
|
||||
test("ignores screen share until window mode returns to normal", () =>
|
||||
testLayoutSwitch({
|
||||
windowMode: " f-n",
|
||||
hasScreenShares: " ny-n",
|
||||
expectedGridMode: "g-sg",
|
||||
windowMode: " f-n",
|
||||
hasScreenShares: "ny-n",
|
||||
expectedLayout: " g-sg",
|
||||
}));
|
||||
@@ -11,15 +11,25 @@ import {
|
||||
Subject,
|
||||
startWith,
|
||||
skipWhile,
|
||||
switchMap,
|
||||
switchAll,
|
||||
} from "rxjs";
|
||||
|
||||
import { type GridMode, type WindowMode } from "./CallViewModel.ts";
|
||||
import { constant, type Behavior } from "../Behavior.ts";
|
||||
import { type ObservableScope } from "../ObservableScope.ts";
|
||||
import { type WindowMode } from "./CallViewModel/CallViewModel.ts";
|
||||
import { constant, type Behavior } from "./Behavior.ts";
|
||||
import { type ObservableScope } from "./ObservableScope.ts";
|
||||
|
||||
export type LayoutMode = "spotlight" | "grid";
|
||||
|
||||
export interface LayoutSwitchViewModel {
|
||||
/**
|
||||
* The layout mode of the call's media tiles.
|
||||
*/
|
||||
layout$: Behavior<LayoutMode>;
|
||||
setLayout: (value: LayoutMode) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a layout mode switch that allows switching between grid and spotlight modes.
|
||||
* Creates a layout mode switch that allows switching between grid and spotlight layouts.
|
||||
* The actual layout mode might switch automatically to spotlight if there is a
|
||||
* remote screen share active or if the window mode is flat.
|
||||
*
|
||||
@@ -27,31 +37,28 @@ import { type ObservableScope } from "../ObservableScope.ts";
|
||||
* @param windowMode$ - The current window mode.
|
||||
* @param hasRemoteScreenShares$ - A behavior indicating if there are remote screen shares active.
|
||||
*/
|
||||
export function createLayoutModeSwitch(
|
||||
export function createLayoutSwitchViewModel(
|
||||
scope: ObservableScope,
|
||||
windowMode$: Behavior<WindowMode>,
|
||||
hasRemoteScreenShares$: Behavior<boolean>,
|
||||
): {
|
||||
gridMode$: Behavior<GridMode>;
|
||||
setGridMode: (value: GridMode) => void;
|
||||
} {
|
||||
const userSelection$ = new Subject<GridMode>();
|
||||
// Callback to set the grid mode desired by the user.
|
||||
// Notice that this is only a preference, the actual grid mode can be overridden
|
||||
): LayoutSwitchViewModel {
|
||||
const userSelection$ = new Subject<LayoutMode>();
|
||||
// Callback to set the layout desired by the user.
|
||||
// Notice that this is only a preference, the actual layout can be overridden
|
||||
// if there is a remote screen share active.
|
||||
const setGridMode = (value: GridMode): void => userSelection$.next(value);
|
||||
const setLayout = (value: LayoutMode): void => userSelection$.next(value);
|
||||
|
||||
/**
|
||||
* The natural grid mode - the mode that the grid would prefer to be in,
|
||||
* The natural layout - the layout that the interface would prefer to be in,
|
||||
* not accounting for the user's manual selections.
|
||||
*/
|
||||
const naturalGridMode$ = scope.behavior<GridMode>(
|
||||
const naturalLayout$ = scope.behavior<LayoutMode>(
|
||||
combineLatest(
|
||||
[hasRemoteScreenShares$, windowMode$],
|
||||
(hasRemoteScreenShares, windowMode) => {
|
||||
// When the window is flat (as with a phone in landscape orientation),
|
||||
// grid mode is preferable as there's usually more than enough
|
||||
// horizontal space to fit in some grid tiles on the side.
|
||||
// grid is preferable as there's usually more than enough horizontal
|
||||
// space to fit in some grid tiles on the side.
|
||||
if (windowMode === "flat") return "grid";
|
||||
// When there are screen shares, spotlight is a better experience. We
|
||||
// want them to be big and readable.
|
||||
@@ -61,35 +68,32 @@ export function createLayoutModeSwitch(
|
||||
);
|
||||
|
||||
/**
|
||||
* The layout mode of the media tile grid.
|
||||
* The layout mode of the call's media tiles.
|
||||
*/
|
||||
const gridMode$ = scope.behavior<GridMode>(
|
||||
const layout$ = scope.behavior<LayoutMode>(
|
||||
// Whenever the user makes a selection, we enter a new mode of behavior:
|
||||
userSelection$.pipe(
|
||||
map((selection) => {
|
||||
if (selection === "grid")
|
||||
// The user has selected grid mode. Start by respecting their choice,
|
||||
// but then follow the natural mode again as soon as it matches.
|
||||
return naturalGridMode$.pipe(
|
||||
// The user has selected grid. Start by respecting their choice, but
|
||||
// then follow the natural mode again as soon as it matches.
|
||||
return naturalLayout$.pipe(
|
||||
skipWhile((naturalMode) => naturalMode !== selection),
|
||||
startWith(selection),
|
||||
);
|
||||
|
||||
// The user has selected spotlight mode. If this matches the natural
|
||||
// mode, then follow the natural mode going forward.
|
||||
return selection === naturalGridMode$.value
|
||||
? naturalGridMode$
|
||||
// The user has selected spotlight. If this matches the natural layout,
|
||||
// then follow the natural layout going forward.
|
||||
return selection === naturalLayout$.value
|
||||
? naturalLayout$
|
||||
: constant(selection);
|
||||
}),
|
||||
// Initially the mode of behavior is to just follow the natural grid mode.
|
||||
startWith(naturalGridMode$),
|
||||
// Initially the mode of behavior is to just follow the natural layout.
|
||||
startWith(naturalLayout$),
|
||||
// Switch between each mode of behavior.
|
||||
switchMap((mode$) => mode$),
|
||||
switchAll(),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
gridMode$,
|
||||
setGridMode,
|
||||
};
|
||||
return { layout$, setLayout };
|
||||
}
|
||||
Reference in New Issue
Block a user