Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep tiles in a stable order #2670

Merged
merged 11 commits into from
Nov 6, 2024
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
50 changes: 47 additions & 3 deletions src/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
createContext,
forwardRef,
memo,
useCallback,
useContext,
useEffect,
useMemo,
Expand All @@ -33,6 +34,8 @@ import {
import useMeasure from "react-use-measure";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { useObservableEagerState } from "observable-hooks";
import { fromEvent, map, startWith } from "rxjs";

import styles from "./Grid.module.css";
import { useMergedRefs } from "../useMergedRefs";
Expand All @@ -51,6 +54,7 @@ interface Tile<Model> {
id: string;
model: Model;
onDrag: DragCallback | undefined;
setVisible: (visible: boolean) => void;
}

type PlacedTile<Model> = Tile<Model> & Rect;
Expand Down Expand Up @@ -84,6 +88,7 @@ interface SlotProps<Model> extends Omit<ComponentProps<"div">, "onDrag"> {
id: string;
model: Model;
onDrag?: DragCallback;
onVisibilityChange?: (visible: boolean) => void;
style?: CSSProperties;
className?: string;
}
Expand Down Expand Up @@ -131,6 +136,11 @@ export function useUpdateLayout(): void {
);
}

const windowHeightObservable = fromEvent(window, "resize").pipe(
startWith(null),
map(() => window.innerHeight),
);

export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
ref: LegacyRef<R>;
model: LayoutModel;
Expand Down Expand Up @@ -232,19 +242,42 @@ export function Grid<
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);

const windowHeight = useObservableEagerState(windowHeightObservable);
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
const [generation, setGeneration] = useState<number | null>(null);
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
const prefersReducedMotion = usePrefersReducedMotion();

const Slot: FC<SlotProps<TileModel>> = useMemo(
() =>
function Slot({ id, model, onDrag, style, className, ...props }) {
function Slot({
id,
model,
onDrag,
onVisibilityChange,
style,
className,
...props
}) {
const ref = useRef<HTMLDivElement | null>(null);
const prevVisible = useRef<boolean | null>(null);
const setVisible = useCallback(
(visible: boolean) => {
if (
onVisibilityChange !== undefined &&
visible !== prevVisible.current
) {
onVisibilityChange(visible);
prevVisible.current = visible;
}
},
[onVisibilityChange],
);

useEffect(() => {
tiles.set(id, { id, model, onDrag });
tiles.set(id, { id, model, onDrag, setVisible });
return (): void => void tiles.delete(id);
}, [id, model, onDrag]);
}, [id, model, onDrag, setVisible]);

return (
<div
Expand Down Expand Up @@ -302,6 +335,17 @@ export function Grid<
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gridRoot, layoutRoot, tiles, gridBounds, generation]);

// The height of the portion of the grid visible at any given time
const visibleHeight = useMemo(
() => Math.min(gridBounds.bottom, windowHeight) - gridBounds.top,
[gridBounds, windowHeight],
);

useEffect(() => {
for (const tile of placedTiles)
tile.setVisible(tile.y + tile.height <= visibleHeight);
}, [placedTiles, visibleHeight]);

// Drag state is stored in a ref rather than component state, because we use
// react-spring's imperative API during gestures to improve responsiveness
const dragState = useRef<DragState | null>(null);
Expand Down
35 changes: 11 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 && (
<Slot
className={styles.slot}
id="spotlight"
model={tileModel}
model={model.spotlight}
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,14 @@ 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}
onVisibilityChange={m.setVisible}
/>
))}
</div>
);
Expand Down
21 changes: 7 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,16 +50,18 @@ 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}
onVisibilityChange={model.remote.setVisible}
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}
onDrag={onDragLocalTile}
onVisibilityChange={model.local.setVisible}
data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline}
/>
Expand Down
22 changes: 7 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}
/>
</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,12 +57,13 @@ export const makeSpotlightExpandedLayout: CallLayout<

return (
<div ref={ref} className={styles.layer}>
{pipTileModel && (
{model.pip && (
<Slot
className={styles.pip}
id="pip"
model={pipTileModel}
id={model.pip.id}
model={model.pip}
onDrag={onDragPip}
onVisibilityChange={model.pip.setVisible}
data-block-alignment={pipAlignmentValue.block}
data-inline-alignment={pipAlignmentValue.inline}
/>
Expand Down
Loading