mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-05 19:59: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:
@@ -16,10 +16,12 @@ import inCallViewStyles from "../room/InCallView.module.css";
|
|||||||
import { useStaticViewModel } from "../state/ViewModel";
|
import { useStaticViewModel } from "../state/ViewModel";
|
||||||
import { ReactionsSenderContext } from "../reactions/useReactionsSender";
|
import { ReactionsSenderContext } from "../reactions/useReactionsSender";
|
||||||
import { type ReactionOption } from "../reactions";
|
import { type ReactionOption } from "../reactions";
|
||||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
|
||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
import { MediaDevices } from "../state/MediaDevices";
|
import { MediaDevices } from "../state/MediaDevices";
|
||||||
import { globalScope } from "../state/ObservableScope";
|
import { globalScope } from "../state/ObservableScope";
|
||||||
|
import { constant } from "../state/Behavior";
|
||||||
|
import { type LayoutMode } from "../state/LayoutSwitchViewModel";
|
||||||
|
|
||||||
// consts for tests
|
// consts for tests
|
||||||
const reactionIdentifier = "@user:example.com:DEVICE";
|
const reactionIdentifier = "@user:example.com:DEVICE";
|
||||||
const reactionData = {
|
const reactionData = {
|
||||||
@@ -32,6 +34,7 @@ const mediaDevices = new MediaDevices(globalScope);
|
|||||||
/**
|
/**
|
||||||
* A wrapper component that is used for:
|
* A wrapper component that is used for:
|
||||||
* - exposing the snapshot via props so the storybook documents the snapshot properties (basically unpack them form the vm)
|
* - exposing the snapshot via props so the storybook documents the snapshot properties (basically unpack them form the vm)
|
||||||
|
* - constructing the layout switch view model
|
||||||
* - Add additional react context
|
* - Add additional react context
|
||||||
* The paraeters are all params from the FooterSnapshot,
|
* The paraeters are all params from the FooterSnapshot,
|
||||||
* the Snapshot of the vm, the wrapper will create a mocked vm from it and pass it to the CallFooter.
|
* the Snapshot of the vm, the wrapper will create a mocked vm from it and pass it to the CallFooter.
|
||||||
@@ -40,11 +43,18 @@ const mediaDevices = new MediaDevices(globalScope);
|
|||||||
*/
|
*/
|
||||||
function CallFooterStoryWrapper({
|
function CallFooterStoryWrapper({
|
||||||
children,
|
children,
|
||||||
|
layout,
|
||||||
|
setLayout,
|
||||||
...vmSnapshot
|
...vmSnapshot
|
||||||
}: FooterSnapshot & {
|
}: Omit<FooterSnapshot, "layoutSwitchVm"> & {
|
||||||
children?: false | JSX.Element | JSX.Element[] | undefined;
|
children?: false | JSX.Element | JSX.Element[] | undefined;
|
||||||
|
layout: LayoutMode | null;
|
||||||
|
setLayout: (value: LayoutMode) => void;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const vm = useStaticViewModel(vmSnapshot);
|
const vm = useStaticViewModel({
|
||||||
|
...vmSnapshot,
|
||||||
|
layoutSwitchVm: layout && { layout$: constant(layout), setLayout },
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<MediaDevicesContext value={mediaDevices}>
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
<div className={inCallViewStyles.inRoom}>
|
<div className={inCallViewStyles.inRoom}>
|
||||||
@@ -62,28 +72,50 @@ function CallFooterStoryWrapper({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = {
|
|
||||||
component: CallFooterStoryWrapper,
|
|
||||||
} satisfies Meta<typeof CallFooterStoryWrapper>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof meta>;
|
|
||||||
|
|
||||||
const fnArgType = {
|
const fnArgType = {
|
||||||
control: { type: "select" as const },
|
control: { type: "select" as const },
|
||||||
options: ["MockedCallback", "undefined"],
|
options: ["MockedCallback", "undefined"],
|
||||||
mapping: { MockedCallback: fn(), undefined: undefined },
|
mapping: { MockedCallback: fn(), undefined: undefined },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: CallFooterStoryWrapper,
|
||||||
|
argTypes: {
|
||||||
|
layout: {
|
||||||
|
control: "radio",
|
||||||
|
options: ["grid", "spotlight"] satisfies LayoutMode[],
|
||||||
|
},
|
||||||
|
audioOutputSwitcher: {
|
||||||
|
control: "select",
|
||||||
|
options: ["NoOutputCallback", "speaker", "earpiece"],
|
||||||
|
table: { defaultValue: { summary: "NoOutputCallback" } },
|
||||||
|
mapping: {
|
||||||
|
NoOutputCallback: undefined,
|
||||||
|
// This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one.
|
||||||
|
speaker: { targetOutput: "earpiece", switch: fn() },
|
||||||
|
earpiece: { targetOutput: "speaker", switch: fn() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toggleScreenSharing: fnArgType,
|
||||||
|
openSettings: fnArgType,
|
||||||
|
toggleAudio: fnArgType,
|
||||||
|
toggleVideo: fnArgType,
|
||||||
|
hangup: fnArgType,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CallFooterStoryWrapper>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
layoutMode: "grid",
|
layout: "grid",
|
||||||
|
setLayout: fn(),
|
||||||
audioEnabled: true,
|
audioEnabled: true,
|
||||||
audioBusy: false,
|
audioBusy: false,
|
||||||
videoEnabled: true,
|
videoEnabled: true,
|
||||||
videoBusy: false,
|
videoBusy: false,
|
||||||
setLayoutMode: fn(),
|
|
||||||
openSettings: fn(),
|
openSettings: fn(),
|
||||||
toggleAudio: fn(),
|
toggleAudio: fn(),
|
||||||
toggleVideo: fn(),
|
toggleVideo: fn(),
|
||||||
@@ -111,29 +143,6 @@ export const Default: Story = {
|
|||||||
parameters: {
|
parameters: {
|
||||||
layout: "fullscreen",
|
layout: "fullscreen",
|
||||||
},
|
},
|
||||||
argTypes: {
|
|
||||||
layoutMode: {
|
|
||||||
control: "radio",
|
|
||||||
options: ["grid", "spotlight"] satisfies GridMode[],
|
|
||||||
},
|
|
||||||
audioOutputSwitcher: {
|
|
||||||
control: "select",
|
|
||||||
options: ["NoOutputCallback", "speaker", "earpiece"],
|
|
||||||
table: { defaultValue: { summary: "NoOutputCallback" } },
|
|
||||||
mapping: {
|
|
||||||
NoOutputCallback: undefined,
|
|
||||||
// This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one.
|
|
||||||
speaker: { targetOutput: "earpiece", switch: fn() },
|
|
||||||
earpiece: { targetOutput: "speaker", switch: fn() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
toggleScreenSharing: fnArgType,
|
|
||||||
setLayoutMode: fnArgType,
|
|
||||||
openSettings: fnArgType,
|
|
||||||
toggleAudio: fnArgType,
|
|
||||||
toggleVideo: fnArgType,
|
|
||||||
hangup: fnArgType,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithAudioAndVideoOptions: Story = {
|
export const WithAudioAndVideoOptions: Story = {
|
||||||
@@ -194,7 +203,7 @@ export const AudioVideoEnabled: Story = {
|
|||||||
|
|
||||||
const spotlightRadio = canvas.getByRole("radio", { name: "Spotlight" });
|
const spotlightRadio = canvas.getByRole("radio", { name: "Spotlight" });
|
||||||
await userEvent.click(spotlightRadio);
|
await userEvent.click(spotlightRadio);
|
||||||
await expect(args.setLayoutMode).toHaveBeenCalledWith("spotlight");
|
await expect(args.setLayout).toHaveBeenCalledWith("spotlight");
|
||||||
|
|
||||||
const micButtonMute = canvas.getByRole("switch", {
|
const micButtonMute = canvas.getByRole("switch", {
|
||||||
name: "Mute microphone",
|
name: "Mute microphone",
|
||||||
@@ -225,14 +234,14 @@ export const SpotlightMode: Story = {
|
|||||||
...Default,
|
...Default,
|
||||||
args: {
|
args: {
|
||||||
...Default.args,
|
...Default.args,
|
||||||
layoutMode: "spotlight",
|
layout: "spotlight",
|
||||||
},
|
},
|
||||||
play: async ({ args, canvasElement }) => {
|
play: async ({ args, canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const spotlightRadio = canvas.getByRole("radio", { name: "Grid" });
|
const spotlightRadio = canvas.getByRole("radio", { name: "Grid" });
|
||||||
await userEvent.click(spotlightRadio);
|
await userEvent.click(spotlightRadio);
|
||||||
await expect(args.setLayoutMode).toHaveBeenCalledWith("grid");
|
await expect(args.setLayout).toHaveBeenCalledWith("grid");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,7 +273,7 @@ export const Pip: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Default.args,
|
...Default.args,
|
||||||
buttonSize: "md",
|
buttonSize: "md",
|
||||||
layoutMode: undefined,
|
layout: null,
|
||||||
},
|
},
|
||||||
play: async ({ args, canvasElement }) => {
|
play: async ({ args, canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
@@ -348,7 +357,7 @@ export const Lobby: Story = {
|
|||||||
...Default.args,
|
...Default.args,
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
openSettings: undefined,
|
openSettings: undefined,
|
||||||
setLayoutMode: undefined,
|
layout: null,
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -362,7 +371,7 @@ export const LobbyMobile: Story = {
|
|||||||
...Default.args,
|
...Default.args,
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
|
|
||||||
setLayoutMode: undefined,
|
layout: null,
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
@@ -379,7 +388,7 @@ export const LobbyRecentButton: Story = {
|
|||||||
...Default.args,
|
...Default.args,
|
||||||
children: <Link>Back To Recents</Link>,
|
children: <Link>Back To Recents</Link>,
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
setLayoutMode: undefined,
|
layout: null,
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -393,7 +402,7 @@ export const LobbyRecentButtonMobile: Story = {
|
|||||||
...Default.args,
|
...Default.args,
|
||||||
children: <Link>Back To Recents</Link>,
|
children: <Link>Back To Recents</Link>,
|
||||||
showLogo: false,
|
showLogo: false,
|
||||||
setLayoutMode: undefined,
|
layout: null,
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
|
||||||
SpotlightViewIcon,
|
|
||||||
GridIcon,
|
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
import { Switch } from "@vector-im/compound-web";
|
|
||||||
import { t } from "i18next";
|
|
||||||
|
|
||||||
import LogoMark from "../icons/LogoMark.svg?react";
|
import LogoMark from "../icons/LogoMark.svg?react";
|
||||||
import LogoType from "../icons/LogoType.svg?react";
|
import LogoType from "../icons/LogoType.svg?react";
|
||||||
@@ -28,13 +22,14 @@ import {
|
|||||||
type ReactionData,
|
type ReactionData,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import styles from "./CallFooter.module.css";
|
import styles from "./CallFooter.module.css";
|
||||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
|
||||||
import {
|
import {
|
||||||
MediaMuteAndSwitchButton,
|
MediaMuteAndSwitchButton,
|
||||||
type MenuOptions,
|
type MenuOptions,
|
||||||
} from "./MediaMuteAndSwitchButton";
|
} from "./MediaMuteAndSwitchButton";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
import { type ViewModel } from "../state/ViewModel";
|
||||||
import { useBehavior } from "../useBehavior";
|
import { useBehavior } from "../useBehavior";
|
||||||
|
import { type LayoutSwitchViewModel } from "../state/LayoutSwitchViewModel";
|
||||||
|
import { LayoutSwitch } from "../room/LayoutSwitch";
|
||||||
|
|
||||||
export interface AudioOutputSwitcher {
|
export interface AudioOutputSwitcher {
|
||||||
targetOutput: string;
|
targetOutput: string;
|
||||||
@@ -61,8 +56,6 @@ export interface FooterActions {
|
|||||||
/** Also controls if the videoMute button is disabled */
|
/** Also controls if the videoMute button is disabled */
|
||||||
toggleVideo: (() => void) | undefined;
|
toggleVideo: (() => void) | undefined;
|
||||||
toggleBlur: (() => void) | undefined;
|
toggleBlur: (() => void) | undefined;
|
||||||
/** Also controls if the layout button is visible */
|
|
||||||
setLayoutMode: ((mode: GridMode) => void) | undefined;
|
|
||||||
toggleScreenSharing: (() => void) | undefined;
|
toggleScreenSharing: (() => void) | undefined;
|
||||||
/** Also controls if the settings button is visible */
|
/** Also controls if the settings button is visible */
|
||||||
openSettings: (() => void) | undefined;
|
openSettings: (() => void) | undefined;
|
||||||
@@ -87,7 +80,8 @@ export interface FooterState {
|
|||||||
buttonSize: "md" | "lg";
|
buttonSize: "md" | "lg";
|
||||||
showLogo: boolean;
|
showLogo: boolean;
|
||||||
|
|
||||||
layoutMode: GridMode | undefined;
|
/** Also controls if the layout switch is visible */
|
||||||
|
layoutSwitchVm: LayoutSwitchViewModel | null;
|
||||||
|
|
||||||
sharingScreen: boolean;
|
sharingScreen: boolean;
|
||||||
|
|
||||||
@@ -126,8 +120,7 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
const asOverlay = useBehavior(vm.asOverlay$);
|
const asOverlay = useBehavior(vm.asOverlay$);
|
||||||
const showFooter = useBehavior(vm.showFooter$);
|
const showFooter = useBehavior(vm.showFooter$);
|
||||||
const hideControls = useBehavior(vm.hideControls$);
|
const hideControls = useBehavior(vm.hideControls$);
|
||||||
const layoutMode = useBehavior(vm.layoutMode$);
|
const layoutSwitchVm = useBehavior(vm.layoutSwitchVm$);
|
||||||
const setLayoutMode = useBehavior(vm.setLayoutMode$);
|
|
||||||
const openSettings = useBehavior(vm.openSettings$);
|
const openSettings = useBehavior(vm.openSettings$);
|
||||||
const audioEnabled = useBehavior(vm.audioEnabled$);
|
const audioEnabled = useBehavior(vm.audioEnabled$);
|
||||||
const audioBusy = useBehavior(vm.audioBusy$);
|
const audioBusy = useBehavior(vm.audioBusy$);
|
||||||
@@ -317,20 +310,8 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
{(showLogo || debugTileLayout) && logoDebugContainer}
|
{(showLogo || debugTileLayout) && logoDebugContainer}
|
||||||
</div>
|
</div>
|
||||||
{!hideControls && <div className={styles.buttons}>{buttons}</div>}
|
{!hideControls && <div className={styles.buttons}>{buttons}</div>}
|
||||||
{!hideControls && setLayoutMode && layoutMode && (
|
{!hideControls && layoutSwitchVm && (
|
||||||
<Switch<"spotlight", "grid">
|
<LayoutSwitch vm={layoutSwitchVm} className={styles.layout} />
|
||||||
name="layoutMode"
|
|
||||||
aria-label={t("layout_switch_label")}
|
|
||||||
leftLabel={t("layout_spotlight_label")}
|
|
||||||
leftValue="spotlight"
|
|
||||||
leftIcon={SpotlightViewIcon}
|
|
||||||
rightLabel={t("layout_grid_label")}
|
|
||||||
rightValue="grid"
|
|
||||||
rightIcon={GridIcon}
|
|
||||||
className={styles.layout}
|
|
||||||
value={layoutMode}
|
|
||||||
onChange={setLayoutMode}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -182,14 +182,7 @@ export function createCallFooterViewModel(
|
|||||||
|
|
||||||
showLogo$: scope.behavior(isPip$.pipe(map((isPip) => showLogo && !isPip))),
|
showLogo$: scope.behavior(isPip$.pipe(map((isPip) => showLogo && !isPip))),
|
||||||
|
|
||||||
layoutMode$: callModel.gridMode$,
|
layoutSwitchVm$: callModel.layoutSwitchVm$,
|
||||||
setLayoutMode$: scope.behavior(
|
|
||||||
isPip$.pipe(
|
|
||||||
map((isPip) =>
|
|
||||||
!isPip && showControls ? callModel.setGridMode : undefined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
sharingScreen$: callModel.sharingScreen$,
|
sharingScreen$: callModel.sharingScreen$,
|
||||||
toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined),
|
toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined),
|
||||||
@@ -247,20 +240,18 @@ export function createLobbyFooterViewModel(
|
|||||||
hideControls: false,
|
hideControls: false,
|
||||||
asOverlay: false,
|
asOverlay: false,
|
||||||
buttonSize: "lg",
|
buttonSize: "lg",
|
||||||
showLayoutSwitcher: false,
|
|
||||||
openSettings,
|
openSettings,
|
||||||
hangup,
|
hangup,
|
||||||
debugTileLayout: false,
|
debugTileLayout: false,
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
toggleAudio: undefined,
|
toggleAudio: undefined,
|
||||||
toggleVideo: undefined,
|
toggleVideo: undefined,
|
||||||
setLayoutMode: undefined,
|
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
audioEnabled: undefined,
|
audioEnabled: undefined,
|
||||||
audioBusy: false,
|
audioBusy: false,
|
||||||
videoEnabled: undefined,
|
videoEnabled: undefined,
|
||||||
videoBusy: false,
|
videoBusy: false,
|
||||||
layoutMode: undefined,
|
layoutSwitchVm: null,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
audioOutputSwitcher: undefined,
|
audioOutputSwitcher: undefined,
|
||||||
reactionIdentifier: undefined,
|
reactionIdentifier: undefined,
|
||||||
|
|||||||
43
src/room/LayoutSwitch.tsx
Normal file
43
src/room/LayoutSwitch.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type FC } from "react";
|
||||||
|
import {
|
||||||
|
SpotlightViewIcon,
|
||||||
|
GridIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
|
import { type LayoutSwitchViewModel } from "../state/LayoutSwitchViewModel";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Switch } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
vm: LayoutSwitchViewModel;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LayoutSwitch: FC<Props> = ({ vm, className }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const layout = useBehavior(vm.layout$);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch<"spotlight", "grid">
|
||||||
|
name="layout"
|
||||||
|
aria-label={t("layout_switch_label")}
|
||||||
|
leftLabel={t("layout_spotlight_label")}
|
||||||
|
leftValue="spotlight"
|
||||||
|
leftIcon={SpotlightViewIcon}
|
||||||
|
rightLabel={t("layout_grid_label")}
|
||||||
|
rightValue="grid"
|
||||||
|
rightIcon={GridIcon}
|
||||||
|
className={className}
|
||||||
|
value={layout}
|
||||||
|
onChange={vm.setLayout}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -433,7 +433,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-labelledby="_r_19_"
|
aria-labelledby="_r_19_"
|
||||||
name="layoutMode"
|
name="layout"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="spotlight"
|
value="spotlight"
|
||||||
/>
|
/>
|
||||||
@@ -454,7 +454,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-labelledby="_r_1e_"
|
aria-labelledby="_r_1e_"
|
||||||
checked=""
|
checked=""
|
||||||
name="layoutMode"
|
name="layout"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="grid"
|
value="grid"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -327,8 +327,8 @@ describe.each([
|
|||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
g: () => vm.setGridMode("grid"),
|
g: () => vm.layoutSwitchVm$.value!.setLayout("grid"),
|
||||||
});
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
@@ -815,7 +815,9 @@ describe.each([
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
schedule(modeInputMarbles, {
|
||||||
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
@@ -1021,7 +1023,7 @@ describe.each([
|
|||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
});
|
});
|
||||||
schedule(expandInputMarbles, {
|
schedule(expandInputMarbles, {
|
||||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||||
@@ -1091,7 +1093,7 @@ describe.each([
|
|||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
});
|
});
|
||||||
schedule(expandInputMarbles, {
|
schedule(expandInputMarbles, {
|
||||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||||
@@ -1131,7 +1133,7 @@ describe.each([
|
|||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule("s", {
|
schedule("s", {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
});
|
});
|
||||||
schedule("a", {
|
schedule("a", {
|
||||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||||
@@ -1168,8 +1170,8 @@ describe.each([
|
|||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.layoutSwitchVm$.value!.setLayout("spotlight"),
|
||||||
g: () => vm.setGridMode("grid"),
|
g: () => vm.layoutSwitchVm$.value!.setLayout("grid"),
|
||||||
});
|
});
|
||||||
schedule(expandInputMarbles, {
|
schedule(expandInputMarbles, {
|
||||||
a: () => vm.toggleSpotlightExpanded$.value!(),
|
a: () => vm.toggleSpotlightExpanded$.value!(),
|
||||||
@@ -1235,7 +1237,7 @@ describe.each([
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.layoutSwitchVm$.value!.setLayout("grid");
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
@@ -1278,7 +1280,7 @@ describe.each([
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.layoutSwitchVm$.value!.setLayout("grid");
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ import {
|
|||||||
} from "./remoteMembers/MatrixMemberMetadata.ts";
|
} from "./remoteMembers/MatrixMemberMetadata.ts";
|
||||||
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 { createLayoutModeSwitch } from "./LayoutSwitch.ts";
|
import {
|
||||||
|
type LayoutSwitchViewModel,
|
||||||
|
createLayoutSwitchViewModel,
|
||||||
|
} from "../LayoutSwitchViewModel.ts";
|
||||||
import {
|
import {
|
||||||
createWrappedUserMedia,
|
createWrappedUserMedia,
|
||||||
type WrappedUserMediaViewModel,
|
type WrappedUserMediaViewModel,
|
||||||
@@ -201,8 +204,6 @@ const smallMobileCallThreshold = 3;
|
|||||||
// with the interface
|
// with the interface
|
||||||
const showFooterMs = 4000;
|
const showFooterMs = 4000;
|
||||||
|
|
||||||
export type GridMode = "grid" | "spotlight";
|
|
||||||
|
|
||||||
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
||||||
|
|
||||||
interface LayoutScanState {
|
interface LayoutScanState {
|
||||||
@@ -349,8 +350,7 @@ export interface CallViewModel {
|
|||||||
showNameTags$: Behavior<boolean>;
|
showNameTags$: Behavior<boolean>;
|
||||||
spotlightExpanded$: Behavior<boolean>;
|
spotlightExpanded$: Behavior<boolean>;
|
||||||
toggleSpotlightExpanded$: Behavior<(() => void) | null>;
|
toggleSpotlightExpanded$: Behavior<(() => void) | null>;
|
||||||
gridMode$: Behavior<GridMode>;
|
layoutSwitchVm$: Behavior<LayoutSwitchViewModel | null>;
|
||||||
setGridMode: (value: GridMode) => void;
|
|
||||||
|
|
||||||
// header/footer visibility
|
// header/footer visibility
|
||||||
showHeader$: Behavior<boolean>;
|
showHeader$: Behavior<boolean>;
|
||||||
@@ -1056,7 +1056,7 @@ export function createCallViewModel$(
|
|||||||
spotlightExpandedToggle$,
|
spotlightExpandedToggle$,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setGridMode, gridMode$ } = createLayoutModeSwitch(
|
const layoutSwitchVm = createLayoutSwitchViewModel(
|
||||||
scope,
|
scope,
|
||||||
windowMode$,
|
windowMode$,
|
||||||
hasRemoteScreenShares$,
|
hasRemoteScreenShares$,
|
||||||
@@ -1223,9 +1223,9 @@ export function createCallViewModel$(
|
|||||||
switchMap((windowMode) => {
|
switchMap((windowMode) => {
|
||||||
switch (windowMode) {
|
switch (windowMode) {
|
||||||
case "normal":
|
case "normal":
|
||||||
return gridMode$.pipe(
|
return layoutSwitchVm.layout$.pipe(
|
||||||
switchMap((gridMode) => {
|
switchMap((layout) => {
|
||||||
switch (gridMode) {
|
switch (layout) {
|
||||||
case "grid":
|
case "grid":
|
||||||
return oneOnOneDesktopLayoutMedia$.pipe(
|
return oneOnOneDesktopLayoutMedia$.pipe(
|
||||||
switchMap((oneOnOne) =>
|
switchMap((oneOnOne) =>
|
||||||
@@ -1260,9 +1260,9 @@ export function createCallViewModel$(
|
|||||||
return oneOnOneMobileLayoutMedia$.pipe(
|
return oneOnOneMobileLayoutMedia$.pipe(
|
||||||
switchMap((oneOnOne) =>
|
switchMap((oneOnOne) =>
|
||||||
oneOnOne === null
|
oneOnOne === null
|
||||||
? gridMode$.pipe(
|
? layoutSwitchVm.layout$.pipe(
|
||||||
switchMap((gridMode) => {
|
switchMap((layout) => {
|
||||||
switch (gridMode) {
|
switch (layout) {
|
||||||
case "grid":
|
case "grid":
|
||||||
// Yes, grid mode actually gets you a "spotlight" layout in
|
// Yes, grid mode actually gets you a "spotlight" layout in
|
||||||
// this window mode.
|
// this window mode.
|
||||||
@@ -1775,8 +1775,7 @@ export function createCallViewModel$(
|
|||||||
|
|
||||||
spotlightExpanded$: spotlightExpanded$,
|
spotlightExpanded$: spotlightExpanded$,
|
||||||
toggleSpotlightExpanded$: toggleSpotlightExpanded$,
|
toggleSpotlightExpanded$: toggleSpotlightExpanded$,
|
||||||
gridMode$: gridMode$,
|
layoutSwitchVm$: constant(layoutSwitchVm),
|
||||||
setGridMode: setGridMode,
|
|
||||||
layout$: layout$,
|
layout$: layout$,
|
||||||
localMatrixLivekitMember$,
|
localMatrixLivekitMember$,
|
||||||
remoteMatrixLivekitMembers$: scope.behavior(
|
remoteMatrixLivekitMembers$: scope.behavior(
|
||||||
|
|||||||
@@ -7,31 +7,31 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { describe, test } from "vitest";
|
import { describe, test } from "vitest";
|
||||||
|
|
||||||
import { createLayoutModeSwitch } from "./LayoutSwitch";
|
import { createLayoutSwitchViewModel } from "./LayoutSwitchViewModel";
|
||||||
import { testScope, withTestScheduler } from "../../utils/test";
|
import { testScope, withTestScheduler } from "../utils/test";
|
||||||
|
|
||||||
function testLayoutSwitch({
|
function testLayoutSwitch({
|
||||||
windowMode = "n",
|
windowMode = "n",
|
||||||
hasScreenShares = "n",
|
hasScreenShares = "n",
|
||||||
userSelection = "",
|
userSelection = "",
|
||||||
expectedGridMode,
|
expectedLayout,
|
||||||
}: {
|
}: {
|
||||||
windowMode?: string;
|
windowMode?: string;
|
||||||
hasScreenShares?: string;
|
hasScreenShares?: string;
|
||||||
userSelection?: string;
|
userSelection?: string;
|
||||||
expectedGridMode: string;
|
expectedLayout: string;
|
||||||
}): void {
|
}): void {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
const { gridMode$, setGridMode } = createLayoutModeSwitch(
|
const { layout$, setLayout } = createLayoutSwitchViewModel(
|
||||||
testScope(),
|
testScope(),
|
||||||
behavior(windowMode, { n: "normal", N: "narrow", f: "flat" }),
|
behavior(windowMode, { n: "normal", N: "narrow", f: "flat" }),
|
||||||
behavior(hasScreenShares, { y: true, n: false }),
|
behavior(hasScreenShares, { y: true, n: false }),
|
||||||
);
|
);
|
||||||
schedule(userSelection, {
|
schedule(userSelection, {
|
||||||
g: () => setGridMode("grid"),
|
g: () => setLayout("grid"),
|
||||||
s: () => setGridMode("spotlight"),
|
s: () => setLayout("spotlight"),
|
||||||
});
|
});
|
||||||
expectObservable(gridMode$).toBe(expectedGridMode, {
|
expectObservable(layout$).toBe(expectedLayout, {
|
||||||
g: "grid",
|
g: "grid",
|
||||||
s: "spotlight",
|
s: "spotlight",
|
||||||
});
|
});
|
||||||
@@ -41,86 +41,86 @@ function testLayoutSwitch({
|
|||||||
describe("default mode", () => {
|
describe("default mode", () => {
|
||||||
test("uses grid layout in normal window", () =>
|
test("uses grid layout in normal window", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
windowMode: " n",
|
windowMode: " n",
|
||||||
expectedGridMode: "g",
|
expectedLayout: "g",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("uses grid layout in flat window", () =>
|
test("uses grid layout in flat window", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
windowMode: " f",
|
windowMode: " f",
|
||||||
expectedGridMode: "g",
|
expectedLayout: "g",
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allows switching modes manually", () =>
|
test("allows switching modes manually", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
userSelection: " --sgs",
|
userSelection: " --sgs",
|
||||||
expectedGridMode: "g-sgs",
|
expectedLayout: "g-sgs",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("switches to spotlight mode when there is a remote screen share", () =>
|
test("switches to spotlight mode when there is a remote screen share", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
hasScreenShares: " n--y",
|
hasScreenShares: "n--y",
|
||||||
expectedGridMode: "g--s",
|
expectedLayout: " g--s",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("can manually switch to grid when there is a screenshare", () =>
|
test("can manually switch to grid when there is a screenshare", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
hasScreenShares: " n-y",
|
hasScreenShares: "n-y",
|
||||||
userSelection: " ---g",
|
userSelection: " ---g",
|
||||||
expectedGridMode: "g-sg",
|
expectedLayout: " g-sg",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("auto-switches after manually selecting grid", () =>
|
test("auto-switches after manually selecting grid", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
// Two screenshares will happen in sequence. There is a screen share that
|
// Two screenshares will happen in sequence. There is a screen share that
|
||||||
// forces spotlight, then the user manually switches back to grid.
|
// forces spotlight, then the user manually switches back to grid.
|
||||||
hasScreenShares: " n-y-ny",
|
hasScreenShares: "n-y-ny",
|
||||||
userSelection: " ---g",
|
userSelection: " ---g",
|
||||||
expectedGridMode: "g-sg-s",
|
expectedLayout: " g-sg-s",
|
||||||
// If we did want to respect manual selection, the expectation would be: g-sg
|
// 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", () =>
|
test("switches back to grid mode when the remote screen share ends", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
hasScreenShares: " n--y--n",
|
hasScreenShares: "n--y--n",
|
||||||
expectedGridMode: "g--s--g",
|
expectedLayout: " g--s--g",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("auto-switches to spotlight again after first screen share ends", () =>
|
test("auto-switches to spotlight again after first screen share ends", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
hasScreenShares: " nyny",
|
hasScreenShares: "nyny",
|
||||||
expectedGridMode: "gsgs",
|
expectedLayout: " gsgs",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("switches manually to grid after screen share while manually in spotlight", () =>
|
test("switches manually to grid after screen share while manually in spotlight", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
// Initially, no one is sharing. Then the user manually switches to spotlight.
|
// Initially, no one is sharing. Then the user manually switches to spotlight.
|
||||||
// After a screen share starts, the user manually switches to grid.
|
// After a screen share starts, the user manually switches to grid.
|
||||||
hasScreenShares: " n-y",
|
hasScreenShares: "n-y",
|
||||||
userSelection: " -s-g",
|
userSelection: " -s-g",
|
||||||
expectedGridMode: "gs-g",
|
expectedLayout: " gs-g",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("allows switching modes manually when in flat window mode", () =>
|
test("allows switching modes manually when in flat window mode", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
// Window becomes flat, then user switches to spotlight and back.
|
// Window becomes flat, then user switches to spotlight and back.
|
||||||
// Finally the window returns to a normal shape.
|
// Finally the window returns to a normal shape.
|
||||||
windowMode: " nf--n",
|
windowMode: " nf--n",
|
||||||
userSelection: " --sg",
|
userSelection: " --sg",
|
||||||
expectedGridMode: "g-sg",
|
expectedLayout: "g-sg",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("switches to grid when in flat window mode even when there are screen shares", () =>
|
test("switches to grid when in flat window mode even when there are screen shares", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
windowMode: " nf",
|
windowMode: " nf",
|
||||||
hasScreenShares: " y",
|
hasScreenShares: "y",
|
||||||
expectedGridMode: "sg",
|
expectedLayout: " sg",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("ignores screen share until window mode returns to normal", () =>
|
test("ignores screen share until window mode returns to normal", () =>
|
||||||
testLayoutSwitch({
|
testLayoutSwitch({
|
||||||
windowMode: " f-n",
|
windowMode: " f-n",
|
||||||
hasScreenShares: " ny-n",
|
hasScreenShares: "ny-n",
|
||||||
expectedGridMode: "g-sg",
|
expectedLayout: " g-sg",
|
||||||
}));
|
}));
|
||||||
@@ -11,15 +11,25 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
startWith,
|
startWith,
|
||||||
skipWhile,
|
skipWhile,
|
||||||
switchMap,
|
switchAll,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { type GridMode, type WindowMode } from "./CallViewModel.ts";
|
import { type WindowMode } from "./CallViewModel/CallViewModel.ts";
|
||||||
import { constant, type Behavior } from "../Behavior.ts";
|
import { constant, type Behavior } from "./Behavior.ts";
|
||||||
import { type ObservableScope } from "../ObservableScope.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
|
* The actual layout mode might switch automatically to spotlight if there is a
|
||||||
* remote screen share active or if the window mode is flat.
|
* 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 windowMode$ - The current window mode.
|
||||||
* @param hasRemoteScreenShares$ - A behavior indicating if there are remote screen shares active.
|
* @param hasRemoteScreenShares$ - A behavior indicating if there are remote screen shares active.
|
||||||
*/
|
*/
|
||||||
export function createLayoutModeSwitch(
|
export function createLayoutSwitchViewModel(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
windowMode$: Behavior<WindowMode>,
|
windowMode$: Behavior<WindowMode>,
|
||||||
hasRemoteScreenShares$: Behavior<boolean>,
|
hasRemoteScreenShares$: Behavior<boolean>,
|
||||||
): {
|
): LayoutSwitchViewModel {
|
||||||
gridMode$: Behavior<GridMode>;
|
const userSelection$ = new Subject<LayoutMode>();
|
||||||
setGridMode: (value: GridMode) => void;
|
// Callback to set the layout desired by the user.
|
||||||
} {
|
// Notice that this is only a preference, the actual layout can be overridden
|
||||||
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
|
|
||||||
// if there is a remote screen share active.
|
// 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.
|
* not accounting for the user's manual selections.
|
||||||
*/
|
*/
|
||||||
const naturalGridMode$ = scope.behavior<GridMode>(
|
const naturalLayout$ = scope.behavior<LayoutMode>(
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[hasRemoteScreenShares$, windowMode$],
|
[hasRemoteScreenShares$, windowMode$],
|
||||||
(hasRemoteScreenShares, windowMode) => {
|
(hasRemoteScreenShares, windowMode) => {
|
||||||
// When the window is flat (as with a phone in landscape orientation),
|
// When the window is flat (as with a phone in landscape orientation),
|
||||||
// grid mode is preferable as there's usually more than enough
|
// grid is preferable as there's usually more than enough horizontal
|
||||||
// horizontal space to fit in some grid tiles on the side.
|
// space to fit in some grid tiles on the side.
|
||||||
if (windowMode === "flat") return "grid";
|
if (windowMode === "flat") return "grid";
|
||||||
// When there are screen shares, spotlight is a better experience. We
|
// When there are screen shares, spotlight is a better experience. We
|
||||||
// want them to be big and readable.
|
// 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:
|
// Whenever the user makes a selection, we enter a new mode of behavior:
|
||||||
userSelection$.pipe(
|
userSelection$.pipe(
|
||||||
map((selection) => {
|
map((selection) => {
|
||||||
if (selection === "grid")
|
if (selection === "grid")
|
||||||
// The user has selected grid mode. Start by respecting their choice,
|
// The user has selected grid. Start by respecting their choice, but
|
||||||
// but then follow the natural mode again as soon as it matches.
|
// then follow the natural mode again as soon as it matches.
|
||||||
return naturalGridMode$.pipe(
|
return naturalLayout$.pipe(
|
||||||
skipWhile((naturalMode) => naturalMode !== selection),
|
skipWhile((naturalMode) => naturalMode !== selection),
|
||||||
startWith(selection),
|
startWith(selection),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The user has selected spotlight mode. If this matches the natural
|
// The user has selected spotlight. If this matches the natural layout,
|
||||||
// mode, then follow the natural mode going forward.
|
// then follow the natural layout going forward.
|
||||||
return selection === naturalGridMode$.value
|
return selection === naturalLayout$.value
|
||||||
? naturalGridMode$
|
? naturalLayout$
|
||||||
: constant(selection);
|
: constant(selection);
|
||||||
}),
|
}),
|
||||||
// Initially the mode of behavior is to just follow the natural grid mode.
|
// Initially the mode of behavior is to just follow the natural layout.
|
||||||
startWith(naturalGridMode$),
|
startWith(naturalLayout$),
|
||||||
// Switch between each mode of behavior.
|
// Switch between each mode of behavior.
|
||||||
switchMap((mode$) => mode$),
|
switchAll(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { layout$, setLayout };
|
||||||
gridMode$,
|
|
||||||
setGridMode,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user