Skip to content

Commit

Permalink
Keep tiles in a stable order
Browse files Browse the repository at this point in the history
This introduces a new layer of abstraction on top of MediaViewModel: TileViewModel, which gives us a place to store data relating to tiles rather than their media, and also generally makes it easier to reason about tiles as they move about the call layout. I have created a class called TileStore to keep track of these tiles.

This allows us to swap out the media shown on a tile as the spotlight speaker changes, and avoid moving tiles around unless they really need to jump between the visible/invisible regions of the layout.
  • Loading branch information
robintown committed Oct 18, 2024
1 parent 75c7516 commit 217c57e
Show file tree
Hide file tree
Showing 21 changed files with 1,038 additions and 327 deletions.
19 changes: 3 additions & 16 deletions src/grid/CallLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Please see LICENSE in the repository root for full details.
import { BehaviorSubject, Observable } from "rxjs";
import { ComponentType } from "react";

import { MediaViewModel, UserMediaViewModel } from "../state/MediaViewModel";
import { LayoutProps } from "./Grid";
import { TileViewModel } from "../state/TileViewModel";

export interface Bounds {
width: number;
Expand Down Expand Up @@ -42,19 +42,6 @@ export interface CallLayoutInputs {
pipAlignment: BehaviorSubject<Alignment>;
}

export interface GridTileModel {
type: "grid";
vm: UserMediaViewModel;
}

export interface SpotlightTileModel {
type: "spotlight";
vms: MediaViewModel[];
maximised: boolean;
}

export type TileModel = GridTileModel | SpotlightTileModel;

export interface CallLayoutOutputs<Model> {
/**
* Whether the scrolling layer of the layout should appear on top.
Expand All @@ -63,11 +50,11 @@ export interface CallLayoutOutputs<Model> {
/**
* The visually fixed (non-scrolling) layer of the layout.
*/
fixed: ComponentType<LayoutProps<Model, TileModel, HTMLDivElement>>;
fixed: ComponentType<LayoutProps<Model, TileViewModel, HTMLDivElement>>;
/**
* The layer of the layout that can overflow and be scrolled.
*/
scrolling: ComponentType<LayoutProps<Model, TileModel, HTMLDivElement>>;
scrolling: ComponentType<LayoutProps<Model, TileViewModel, HTMLDivElement>>;
}

/**
Expand Down
29 changes: 5 additions & 24 deletions src/grid/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ import { useObservableEagerState } from "observable-hooks";
import { GridLayout as GridLayoutModel } from "../state/CallViewModel";
import styles from "./GridLayout.module.css";
import { useInitial } from "../useInitial";
import {
CallLayout,
GridTileModel,
TileModel,
arrangeTiles,
} from "./CallLayout";
import { CallLayout, arrangeTiles } from "./CallLayout";
import { DragCallback, useUpdateLayout } from "./Grid";

interface GridCSSProperties extends CSSProperties {
Expand Down Expand Up @@ -49,15 +44,6 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
),
),
);
const tileModel: TileModel | undefined = useMemo(
() =>
model.spotlight && {
type: "spotlight",
vms: model.spotlight,
maximised: false,
},
[model.spotlight],
);

const onDragSpotlight: DragCallback = useCallback(
({ xRatio, yRatio }) =>
Expand All @@ -70,11 +56,11 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({

return (
<div ref={ref} className={styles.fixed}>
{tileModel && (
{model.spotlight && (

Check warning on line 59 in src/grid/GridLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/GridLayout.tsx#L59

Added line #L59 was not covered by tests
<Slot
className={styles.slot}
id="spotlight"
model={tileModel}
model={model.spotlight}

Check warning on line 63 in src/grid/GridLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/GridLayout.tsx#L63

Added line #L63 was not covered by tests
onDrag={onDragSpotlight}
data-block-alignment={alignment.block}
data-inline-alignment={alignment.inline}
Expand All @@ -93,11 +79,6 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
[width, minHeight, model.grid.length],
);

const tileModels: GridTileModel[] = useMemo(
() => model.grid.map((vm) => ({ type: "grid", vm })),
[model.grid],
);

return (
<div
ref={ref}
Expand All @@ -111,8 +92,8 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
} as GridCSSProperties
}
>
{tileModels.map((m) => (
<Slot key={m.vm.id} className={styles.slot} id={m.vm.id} model={m} />
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />

Check warning on line 96 in src/grid/GridLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/GridLayout.tsx#L95-L96

Added lines #L95 - L96 were not covered by tests
))}
</div>
);
Expand Down
19 changes: 5 additions & 14 deletions src/grid/OneOnOneLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";

import { OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel";
import { CallLayout, GridTileModel, arrangeTiles } from "./CallLayout";
import { CallLayout, arrangeTiles } from "./CallLayout";
import styles from "./OneOnOneLayout.module.css";
import { DragCallback, useUpdateLayout } from "./Grid";

Expand Down Expand Up @@ -38,15 +38,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
[width, height],
);

const remoteTileModel: GridTileModel = useMemo(
() => ({ type: "grid", vm: model.remote }),
[model.remote],
);
const localTileModel: GridTileModel = useMemo(
() => ({ type: "grid", vm: model.local }),
[model.local],
);

const onDragLocalTile: DragCallback = useCallback(
({ xRatio, yRatio }) =>
pipAlignment.next({
Expand All @@ -59,15 +50,15 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
return (
<div ref={ref} className={styles.layer}>
<Slot
id={remoteTileModel.vm.id}
model={remoteTileModel}
id={model.remote.id}
model={model.remote}

Check warning on line 54 in src/grid/OneOnOneLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/OneOnOneLayout.tsx#L53-L54

Added lines #L53 - L54 were not covered by tests
className={styles.container}
style={{ width: tileWidth, height: tileHeight }}
>
<Slot
className={classNames(styles.slot, styles.local)}
id={localTileModel.vm.id}
model={localTileModel}
id={model.local.id}
model={model.local}

Check warning on line 61 in src/grid/OneOnOneLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/OneOnOneLayout.tsx#L60-L61

Added lines #L60 - L61 were not covered by tests
onDrag={onDragLocalTile}
data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline}
Expand Down
21 changes: 6 additions & 15 deletions src/grid/SpotlightExpandedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { forwardRef, useCallback, useMemo } from "react";
import { forwardRef, useCallback } from "react";
import { useObservableEagerState } from "observable-hooks";

import { SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
import { CallLayout, GridTileModel, SpotlightTileModel } from "./CallLayout";
import { CallLayout } from "./CallLayout";
import { DragCallback, useUpdateLayout } from "./Grid";
import styles from "./SpotlightExpandedLayout.module.css";

Expand All @@ -27,17 +27,13 @@ export const makeSpotlightExpandedLayout: CallLayout<
ref,
) {
useUpdateLayout();
const spotlightTileModel: SpotlightTileModel = useMemo(
() => ({ type: "spotlight", vms: model.spotlight, maximised: true }),
[model.spotlight],
);

return (
<div ref={ref} className={styles.layer}>
<Slot
className={styles.spotlight}
id="spotlight"
model={spotlightTileModel}
model={model.spotlight}

Check warning on line 36 in src/grid/SpotlightExpandedLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightExpandedLayout.tsx#L36

Added line #L36 was not covered by tests
/>
</div>
);
Expand All @@ -50,11 +46,6 @@ export const makeSpotlightExpandedLayout: CallLayout<
useUpdateLayout();
const pipAlignmentValue = useObservableEagerState(pipAlignment);

const pipTileModel: GridTileModel | undefined = useMemo(
() => model.pip && { type: "grid", vm: model.pip },
[model.pip],
);

const onDragPip: DragCallback = useCallback(
({ xRatio, yRatio }) =>
pipAlignment.next({
Expand All @@ -66,11 +57,11 @@ export const makeSpotlightExpandedLayout: CallLayout<

return (
<div ref={ref} className={styles.layer}>
{pipTileModel && (
{model.pip && (

Check warning on line 60 in src/grid/SpotlightExpandedLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightExpandedLayout.tsx#L60

Added line #L60 was not covered by tests
<Slot
className={styles.pip}
id="pip"
model={pipTileModel}
id={model.pip.id}
model={model.pip}

Check warning on line 64 in src/grid/SpotlightExpandedLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightExpandedLayout.tsx#L63-L64

Added lines #L63 - L64 were not covered by tests
onDrag={onDragPip}
data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline}
Expand Down
35 changes: 12 additions & 23 deletions src/grid/SpotlightLandscapeLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { forwardRef, useMemo } from "react";
import { forwardRef } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";

import { CallLayout, GridTileModel, TileModel } from "./CallLayout";
import { CallLayout } from "./CallLayout";
import { SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
import styles from "./SpotlightLandscapeLayout.module.css";
import { useUpdateLayout } from "./Grid";
Expand All @@ -30,19 +30,15 @@ export const makeSpotlightLandscapeLayout: CallLayout<
) {
useUpdateLayout();
useObservableEagerState(minBounds);
const tileModel: TileModel = useMemo(
() => ({
type: "spotlight",
vms: model.spotlight,
maximised: false,
}),
[model.spotlight],
);

return (
<div ref={ref} className={styles.layer}>
<div className={styles.spotlight}>
<Slot className={styles.slot} id="spotlight" model={tileModel} />
<Slot
className={styles.slot}
id="spotlight"
model={model.spotlight}
/>

Check warning on line 41 in src/grid/SpotlightLandscapeLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightLandscapeLayout.tsx#L37-L41

Added lines #L37 - L41 were not covered by tests
</div>
<div className={styles.grid} />
</div>
Expand All @@ -55,26 +51,19 @@ export const makeSpotlightLandscapeLayout: CallLayout<
) {
useUpdateLayout();
useObservableEagerState(minBounds);
const tileModels: GridTileModel[] = useMemo(
() => model.grid.map((vm) => ({ type: "grid", vm })),
[model.grid],
);
const withIndicators =
useObservableEagerState(model.spotlight.media).length > 1;

Check warning on line 55 in src/grid/SpotlightLandscapeLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightLandscapeLayout.tsx#L54-L55

Added lines #L54 - L55 were not covered by tests

return (
<div ref={ref} className={styles.layer}>
<div
className={classNames(styles.spotlight, {
[styles.withIndicators]: model.spotlight.length > 1,
[styles.withIndicators]: withIndicators,

Check warning on line 61 in src/grid/SpotlightLandscapeLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightLandscapeLayout.tsx#L61

Added line #L61 was not covered by tests
})}
/>
<div className={styles.grid}>
{tileModels.map((m) => (
<Slot
key={m.vm.id}
className={styles.slot}
id={m.vm.id}
model={m}
/>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />

Check warning on line 66 in src/grid/SpotlightLandscapeLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightLandscapeLayout.tsx#L65-L66

Added lines #L65 - L66 were not covered by tests
))}
</div>
</div>
Expand Down
40 changes: 12 additions & 28 deletions src/grid/SpotlightPortraitLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { CSSProperties, forwardRef, useMemo } from "react";
import { CSSProperties, forwardRef } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";

import {
CallLayout,
GridTileModel,
TileModel,
arrangeTiles,
} from "./CallLayout";
import { CallLayout, arrangeTiles } from "./CallLayout";
import { SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
import styles from "./SpotlightPortraitLayout.module.css";
import { useUpdateLayout } from "./Grid";
Expand All @@ -40,19 +35,15 @@ export const makeSpotlightPortraitLayout: CallLayout<
ref,
) {
useUpdateLayout();
const tileModel: TileModel = useMemo(
() => ({
type: "spotlight",
vms: model.spotlight,
maximised: true,
}),
[model.spotlight],
);

return (
<div ref={ref} className={styles.layer}>
<div className={styles.spotlight}>
<Slot className={styles.slot} id="spotlight" model={tileModel} />
<Slot
className={styles.slot}
id="spotlight"
model={model.spotlight}
/>

Check warning on line 46 in src/grid/SpotlightPortraitLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightPortraitLayout.tsx#L42-L46

Added lines #L42 - L46 were not covered by tests
</div>
</div>
);
Expand All @@ -71,10 +62,8 @@ export const makeSpotlightPortraitLayout: CallLayout<
width,
model.grid.length,
);
const tileModels: GridTileModel[] = useMemo(
() => model.grid.map((vm) => ({ type: "grid", vm })),
[model.grid],
);
const withIndicators =
useObservableEagerState(model.spotlight.media).length > 1;

Check warning on line 66 in src/grid/SpotlightPortraitLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightPortraitLayout.tsx#L65-L66

Added lines #L65 - L66 were not covered by tests

return (
<div
Expand All @@ -90,17 +79,12 @@ export const makeSpotlightPortraitLayout: CallLayout<
>
<div
className={classNames(styles.spotlight, {
[styles.withIndicators]: model.spotlight.length > 1,
[styles.withIndicators]: withIndicators,

Check warning on line 82 in src/grid/SpotlightPortraitLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightPortraitLayout.tsx#L82

Added line #L82 was not covered by tests
})}
/>
<div className={styles.grid}>
{tileModels.map((m) => (
<Slot
key={m.vm.id}
className={styles.slot}
id={m.vm.id}
model={m}
/>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />

Check warning on line 87 in src/grid/SpotlightPortraitLayout.tsx

View check run for this annotation

Codecov / codecov/patch

src/grid/SpotlightPortraitLayout.tsx#L86-L87

Added lines #L86 - L87 were not covered by tests
))}
</div>
</div>
Expand Down
Loading

0 comments on commit 217c57e

Please sign in to comment.