Skip to content

Commit

Permalink
Implement the new spotlight layout
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed May 17, 2024
1 parent ac748cd commit 25bcbfa
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 49 deletions.
55 changes: 55 additions & 0 deletions src/grid/CallLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { BehaviorSubject, Observable } from "rxjs";

import { MediaViewModel } from "../state/MediaViewModel";
import { LayoutSystem } from "./Grid";
import { Alignment } from "../room/InCallView";

export interface Bounds {
width: number;
height: number;
}

export interface CallLayoutInputs {
/**
* The minimum bounds of the layout area.
*/
minBounds: Observable<Bounds>;
/**
* The alignment of the floating tile, if any.
*/
floatingAlignment: BehaviorSubject<Alignment>;
}

export interface CallLayoutOutputs<Model> {
/**
* The visually fixed (non-scrolling) layer of the layout.
*/
fixed: LayoutSystem<Model, MediaViewModel[], HTMLDivElement>;
/**
* The layer of the layout that can overflow and be scrolled.
*/
scrolling: LayoutSystem<Model, MediaViewModel, HTMLDivElement>;
}

/**
* A layout system for media tiles.
*/
export type CallLayout<Model> = (
inputs: CallLayoutInputs,
) => CallLayoutOutputs<Model>;
25 changes: 7 additions & 18 deletions src/grid/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,30 @@ limitations under the License.
*/

import { CSSProperties, forwardRef, useMemo } from "react";
import { BehaviorSubject, Observable, distinctUntilChanged } from "rxjs";
import { distinctUntilChanged } from "rxjs";
import { useObservableEagerState } from "observable-hooks";

import { GridLayout as GridLayoutModel } from "../state/CallViewModel";
import { MediaViewModel } from "../state/MediaViewModel";
import { LayoutSystem, Slot } from "./Grid";
import { Slot } from "./Grid";
import styles from "./GridLayout.module.css";
import { useReactiveState } from "../useReactiveState";
import { Alignment } from "../room/InCallView";
import { useInitial } from "../useInitial";

export interface Bounds {
width: number;
height: number;
}
import { CallLayout } from "./CallLayout";

interface GridCSSProperties extends CSSProperties {
"--gap": string;
"--width": string;
"--height": string;
}

interface GridLayoutSystems {
scrolling: LayoutSystem<GridLayoutModel, MediaViewModel, HTMLDivElement>;
fixed: LayoutSystem<GridLayoutModel, MediaViewModel[], HTMLDivElement>;
}

const slotMinHeight = 130;
const slotMaxAspectRatio = 17 / 9;
const slotMinAspectRatio = 4 / 3;

export const gridLayoutSystems = (
minBounds: Observable<Bounds>,
floatingAlignment: BehaviorSubject<Alignment>,
): GridLayoutSystems => ({
export const makeGridLayout: CallLayout<GridLayoutModel> = ({
minBounds,
floatingAlignment,
}) => ({
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
// lives
fixed: {
Expand Down
63 changes: 63 additions & 0 deletions src/grid/SpotlightLayout.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

.fixed,
.scrolling {
display: grid;
--grid-slot-width: 180px;
--grid-gap: 20px;
grid-template-columns: 1fr calc(
var(--grid-columns) * var(--grid-slot-width) + (var(--grid-columns) - 1) *
var(--grid-gap)
);
grid-template-rows: minmax(1fr, auto);
grid-template-areas: "spotlight grid";
gap: 30px;
}

.scrolling {
block-size: 100%;
}

.spotlight {
grid-area: spotlight;
container: spotlight / size;
display: grid;
place-items: center;
}

/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cq units */
@container spotlight (width > 0) {
.spotlight > .slot {
inline-size: min(100cqi, 100cqb * (17 / 9));
block-size: min(100cqb, 100cqi / (4 / 3));
}
}

.grid {
grid-area: grid;
display: flex;
flex-wrap: wrap;
gap: var(--grid-gap);
justify-content: center;
align-content: center;
}

.grid > .slot {
inline-size: var(--grid-slot-width);
block-size: 135px;
}
87 changes: 87 additions & 0 deletions src/grid/SpotlightLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

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

import { CallLayout } from "./CallLayout";
import { SpotlightLayout as SpotlightLayoutModel } from "../state/CallViewModel";
import { useReactiveState } from "../useReactiveState";
import styles from "./SpotlightLayout.module.css";
import { Slot } from "./Grid";

interface GridCSSProperties extends CSSProperties {
"--grid-columns": number;
}

const getGridColumns = (gridLength: number): number =>
gridLength > 20 ? 2 : 1;

export const makeSpotlightLayout: CallLayout<SpotlightLayoutModel> = ({
minBounds,
}) => ({
fixed: {
tiles: (model) => new Map([["spotlight", model.spotlight]]),
Layout: forwardRef(function SpotlightLayoutFixed({ model }, ref) {
const { width, height } = useObservableEagerState(minBounds);
const gridColumns = getGridColumns(model.grid.length);
const [generation] = useReactiveState<number>(
(prev) => (prev === undefined ? 0 : prev + 1),
[model.grid.length, width, height],
);

return (
<div
ref={ref}
className={styles.fixed}
data-generation={generation}
style={{ "--grid-columns": gridColumns, height } as GridCSSProperties}
>
<div className={styles.spotlight}>
<Slot className={styles.slot} tile="spotlight" />
</div>
</div>
);
}),
},

scrolling: {
tiles: (model) => new Map(model.grid.map((tile) => [tile.id, tile])),
Layout: forwardRef(function SpotlightLayoutScrolling({ model }, ref) {
const { width, height } = useObservableEagerState(minBounds);
const gridColumns = getGridColumns(model.grid.length);
const [generation] = useReactiveState<number>(
(prev) => (prev === undefined ? 0 : prev + 1),
[model.grid, width, height],
);

return (
<div
ref={ref}
data-generation={generation}
className={styles.scrolling}
style={{ "--grid-columns": gridColumns } as GridCSSProperties}
>
<div className={styles.grid}>
{model.grid.map((tile) => (
<Slot className={styles.slot} tile={tile.id} />
))}
</div>
</div>
);
}),
},
});
62 changes: 40 additions & 22 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
import useMeasure from "react-use-measure";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import classNames from "classnames";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, map } from "rxjs";
import { useObservableEagerState } from "observable-hooks";
import { useTranslation } from "react-i18next";

Expand Down Expand Up @@ -73,17 +73,20 @@ import { ECConnectionState } from "../livekit/useECConnectionState";
import { useOpenIDSFU } from "../livekit/openIDSFU";
import {
GridMode,
Layout,
TileDescriptor,
useCallViewModel,
} from "../state/CallViewModel";
import { Grid, TileProps } from "../grid/Grid";
import { MediaViewModel } from "../state/MediaViewModel";
import { gridLayoutSystems } from "../grid/GridLayout";
import { useObservable } from "../state/useObservable";
import { useInitial } from "../useInitial";
import { SpotlightTile } from "../tile/SpotlightTile";
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
import { E2eeType } from "../e2ee/e2eeType";
import { makeGridLayout } from "../grid/GridLayout";
import { makeSpotlightLayout } from "../grid/SpotlightLayout";
import { CallLayout } from "../grid/CallLayout";

const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});

Expand Down Expand Up @@ -302,6 +305,7 @@ export const InCallView: FC<InCallViewProps> = ({

const [headerRef, headerBounds] = useMeasure();
const [footerRef, footerBounds] = useMeasure();

const gridBounds = useMemo(
() => ({
width: footerBounds.width,
Expand All @@ -315,11 +319,29 @@ export const InCallView: FC<InCallViewProps> = ({
],
);
const gridBoundsObservable = useObservable(gridBounds);

const floatingAlignment = useInitial(
() => new BehaviorSubject(defaultAlignment),
);
const { fixed, scrolling } = useInitial(() =>
gridLayoutSystems(gridBoundsObservable, floatingAlignment),

const layoutSystem = useObservableEagerState(
useInitial(() =>
vm.layout.pipe(
map((l) => {
let makeLayout: CallLayout<Layout>;
if (l.type === "grid" && l.grid.length !== 2)
makeLayout = makeGridLayout as CallLayout<Layout>;
else if (l.type === "spotlight")
makeLayout = makeSpotlightLayout as CallLayout<Layout>;
else return null; // Not yet implemented

return makeLayout({
minBounds: gridBoundsObservable,
floatingAlignment,
});
}),
),
),
);

const setGridMode = useCallback(
Expand Down Expand Up @@ -423,39 +445,35 @@ export const InCallView: FC<InCallViewProps> = ({
);
}

// The only new layout we've implemented so far is grid layout for non-1:1
// calls. All other layouts use the legacy grid system for now.
if (
legacyLayout === "grid" &&
layout.type === "grid" &&
!(layout.grid.length === 2 && layout.spotlight === undefined)
) {
if (layoutSystem === null) {
// This new layout doesn't yet have an implemented layout system, so fall
// back to the legacy grid system
return (
<LegacyGrid
items={items}
layout={legacyLayout}
disableAnimations={prefersReducedMotion}
Tile={GridTileView}
/>
);
} else {
return (
<>
<Grid
className={styles.scrollingGrid}
model={layout}
system={scrolling}
system={layoutSystem.scrolling}
Tile={GridTileView}
/>
<Grid
className={styles.fixedGrid}
style={{ insetBlockStart: headerBounds.bottom }}
model={layout}
system={fixed}
system={layoutSystem.fixed}
Tile={SpotlightTileView}
/>
</>
);
} else {
return (
<LegacyGrid
items={items}
layout={legacyLayout}
disableAnimations={prefersReducedMotion}
Tile={GridTileView}
/>
);
}
};

Expand Down
Loading

0 comments on commit 25bcbfa

Please sign in to comment.