mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Make layout reactivity less brittle
Follow-up to ea2d98179c
This took a couple of iterations to find something that works without creating update loops, but I think that by automatically informing Grid whenever a layout component is re-rendered, we'll have a much easier time ensuring that our layouts are fully reactive.
This commit is contained in:
parent
c74cebcc4b
commit
447bac3280
@ -25,10 +25,15 @@ import {
|
|||||||
CSSProperties,
|
CSSProperties,
|
||||||
ComponentProps,
|
ComponentProps,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
|
Dispatch,
|
||||||
FC,
|
FC,
|
||||||
LegacyRef,
|
LegacyRef,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
SetStateAction,
|
||||||
|
createContext,
|
||||||
|
forwardRef,
|
||||||
|
memo,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@ -113,6 +118,27 @@ function offset(element: HTMLElement, relativeTo: Element): Offset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LayoutContext {
|
||||||
|
setGeneration: Dispatch<SetStateAction<number | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayoutContext = createContext<LayoutContext | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables Grid to react to layout changes. You must call this in your Layout
|
||||||
|
* component or else Grid will not be reactive.
|
||||||
|
*/
|
||||||
|
export function useLayout(): void {
|
||||||
|
const context = useContext(LayoutContext);
|
||||||
|
if (context === null)
|
||||||
|
throw new Error("useLayout called outside of a Grid layout component");
|
||||||
|
|
||||||
|
// On every render, tell Grid that the layout may have changed
|
||||||
|
useEffect(() =>
|
||||||
|
context.setGeneration((prev) => (prev === null ? 0 : prev + 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
||||||
ref: LegacyRef<R>;
|
ref: LegacyRef<R>;
|
||||||
model: LayoutModel;
|
model: LayoutModel;
|
||||||
@ -158,6 +184,11 @@ interface Drag {
|
|||||||
|
|
||||||
export type DragCallback = (drag: Drag) => void;
|
export type DragCallback = (drag: Drag) => void;
|
||||||
|
|
||||||
|
interface LayoutMemoProps<LayoutModel, TileModel, R extends HTMLElement>
|
||||||
|
extends LayoutProps<LayoutModel, TileModel, R> {
|
||||||
|
Layout: ComponentType<LayoutProps<LayoutModel, TileModel, R>>;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props<
|
interface Props<
|
||||||
LayoutModel,
|
LayoutModel,
|
||||||
TileModel,
|
TileModel,
|
||||||
@ -209,7 +240,7 @@ export function Grid<
|
|||||||
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
||||||
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
||||||
|
|
||||||
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
const [layoutRoot, layoutRef] = useState<HTMLElement | null>(null);
|
||||||
const [generation, setGeneration] = useState<number | null>(null);
|
const [generation, setGeneration] = useState<number | null>(null);
|
||||||
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
||||||
const prefersReducedMotion = usePrefersReducedMotion();
|
const prefersReducedMotion = usePrefersReducedMotion();
|
||||||
@ -236,27 +267,22 @@ export function Grid<
|
|||||||
[tiles],
|
[tiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
const layoutRef = useCallback(
|
// We must memoize the Layout component to break the update loop where a
|
||||||
(e: HTMLElement | null) => {
|
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
||||||
setLayoutRoot(e);
|
const LayoutMemo = useMemo(
|
||||||
if (e !== null)
|
() =>
|
||||||
setGeneration(parseInt(e.getAttribute("data-generation")!));
|
memo(
|
||||||
},
|
forwardRef<
|
||||||
[setLayoutRoot, setGeneration],
|
LayoutRef,
|
||||||
|
LayoutMemoProps<LayoutModel, TileModel, LayoutRef>
|
||||||
|
>(function LayoutMemo({ Layout, ...props }, ref): ReactNode {
|
||||||
|
return <Layout {...props} ref={ref} />;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const context: LayoutContext = useMemo(() => ({ setGeneration }), []);
|
||||||
if (layoutRoot !== null) {
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
if (mutations.some((m) => m.type === "attributes")) {
|
|
||||||
setGeneration(parseInt(layoutRoot.getAttribute("data-generation")!));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(layoutRoot, { attributes: true });
|
|
||||||
return (): void => observer.disconnect();
|
|
||||||
}
|
|
||||||
}, [layoutRoot, setGeneration]);
|
|
||||||
|
|
||||||
// Combine the tile definitions and slots together to create placed tiles
|
// Combine the tile definitions and slots together to create placed tiles
|
||||||
const placedTiles = useMemo(() => {
|
const placedTiles = useMemo(() => {
|
||||||
@ -279,10 +305,10 @@ export function Grid<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
// The rects may change due to the grid updating to a new generation, but
|
// The rects may change due to the grid resizing or updating to a new
|
||||||
// eslint can't statically verify this
|
// generation, but eslint can't statically verify this
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [gridRoot, layoutRoot, tiles, generation]);
|
}, [gridRoot, layoutRoot, tiles, gridBounds, generation]);
|
||||||
|
|
||||||
// Drag state is stored in a ref rather than component state, because we use
|
// Drag state is stored in a ref rather than component state, because we use
|
||||||
// react-spring's imperative API during gestures to improve responsiveness
|
// react-spring's imperative API during gestures to improve responsiveness
|
||||||
@ -463,7 +489,9 @@ export function Grid<
|
|||||||
className={classNames(className, styles.grid)}
|
className={classNames(className, styles.grid)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<Layout ref={layoutRef} model={model} Slot={Slot} />
|
<LayoutContext.Provider value={context}>
|
||||||
|
<LayoutMemo ref={layoutRef} Layout={Layout} model={model} Slot={Slot} />
|
||||||
|
</LayoutContext.Provider>
|
||||||
{tileTransitions((spring, { id, model, onDrag, width, height }) => (
|
{tileTransitions((spring, { id, model, onDrag, width, height }) => (
|
||||||
<TileWrapper
|
<TileWrapper
|
||||||
key={id}
|
key={id}
|
||||||
|
@ -20,7 +20,6 @@ import { useObservableEagerState } from "observable-hooks";
|
|||||||
|
|
||||||
import { GridLayout as GridLayoutModel } from "../state/CallViewModel";
|
import { GridLayout as GridLayoutModel } from "../state/CallViewModel";
|
||||||
import styles from "./GridLayout.module.css";
|
import styles from "./GridLayout.module.css";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
|
||||||
import { useInitial } from "../useInitial";
|
import { useInitial } from "../useInitial";
|
||||||
import {
|
import {
|
||||||
CallLayout,
|
CallLayout,
|
||||||
@ -28,7 +27,7 @@ import {
|
|||||||
TileModel,
|
TileModel,
|
||||||
arrangeTiles,
|
arrangeTiles,
|
||||||
} from "./CallLayout";
|
} from "./CallLayout";
|
||||||
import { DragCallback } from "./Grid";
|
import { DragCallback, useLayout } from "./Grid";
|
||||||
|
|
||||||
interface GridCSSProperties extends CSSProperties {
|
interface GridCSSProperties extends CSSProperties {
|
||||||
"--gap": string;
|
"--gap": string;
|
||||||
@ -49,7 +48,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
||||||
// lives
|
// lives
|
||||||
fixed: forwardRef(function GridLayoutFixed({ model, Slot }, ref) {
|
fixed: forwardRef(function GridLayoutFixed({ model, Slot }, ref) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
const alignment = useObservableEagerState(
|
const alignment = useObservableEagerState(
|
||||||
useInitial(() =>
|
useInitial(() =>
|
||||||
spotlightAlignment.pipe(
|
spotlightAlignment.pipe(
|
||||||
@ -68,10 +67,6 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
},
|
},
|
||||||
[model.spotlight],
|
[model.spotlight],
|
||||||
);
|
);
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.spotlight === undefined, width, height, alignment],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDragSpotlight: DragCallback = useCallback(
|
const onDragSpotlight: DragCallback = useCallback(
|
||||||
({ xRatio, yRatio }) =>
|
({ xRatio, yRatio }) =>
|
||||||
@ -83,7 +78,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={styles.fixed} data-generation={generation}>
|
<div ref={ref} className={styles.fixed}>
|
||||||
{tileModel && (
|
{tileModel && (
|
||||||
<Slot
|
<Slot
|
||||||
className={styles.slot}
|
className={styles.slot}
|
||||||
@ -100,17 +95,13 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
|
|
||||||
// The scrolling part of the layout is where all the grid tiles live
|
// The scrolling part of the layout is where all the grid tiles live
|
||||||
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
||||||
|
useLayout();
|
||||||
const { width, height: minHeight } = useObservableEagerState(minBounds);
|
const { width, height: minHeight } = useObservableEagerState(minBounds);
|
||||||
const { gap, tileWidth, tileHeight } = useMemo(
|
const { gap, tileWidth, tileHeight } = useMemo(
|
||||||
() => arrangeTiles(width, minHeight, model.grid.length),
|
() => arrangeTiles(width, minHeight, model.grid.length),
|
||||||
[width, minHeight, model.grid.length],
|
[width, minHeight, model.grid.length],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.grid, width, minHeight],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tileModels: GridTileModel[] = useMemo(
|
const tileModels: GridTileModel[] = useMemo(
|
||||||
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
||||||
[model.grid],
|
[model.grid],
|
||||||
@ -119,7 +110,6 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-generation={generation}
|
|
||||||
className={styles.scrolling}
|
className={styles.scrolling}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
@ -20,9 +20,8 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
import { OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel";
|
import { OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel";
|
||||||
import { CallLayout, GridTileModel, arrangeTiles } from "./CallLayout";
|
import { CallLayout, GridTileModel, arrangeTiles } from "./CallLayout";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
|
||||||
import styles from "./OneOnOneLayout.module.css";
|
import styles from "./OneOnOneLayout.module.css";
|
||||||
import { DragCallback } from "./Grid";
|
import { DragCallback, useLayout } from "./Grid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "one-on-one" layout, in which the remote participant
|
* An implementation of the "one-on-one" layout, in which the remote participant
|
||||||
@ -35,10 +34,12 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function OneOnOneLayoutFixed(_props, ref) {
|
fixed: forwardRef(function OneOnOneLayoutFixed(_props, ref) {
|
||||||
return <div ref={ref} data-generation={0} />;
|
useLayout();
|
||||||
|
return <div ref={ref} />;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
||||||
|
useLayout();
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
const { width, height } = useObservableEagerState(minBounds);
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
||||||
const { tileWidth, tileHeight } = useMemo(
|
const { tileWidth, tileHeight } = useMemo(
|
||||||
@ -46,11 +47,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
[width, height],
|
[width, height],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[width, height, pipAlignmentValue],
|
|
||||||
);
|
|
||||||
|
|
||||||
const remoteTileModel: GridTileModel = useMemo(
|
const remoteTileModel: GridTileModel = useMemo(
|
||||||
() => ({ type: "grid", vm: model.remote }),
|
() => ({ type: "grid", vm: model.remote }),
|
||||||
[model.remote],
|
[model.remote],
|
||||||
@ -70,7 +66,7 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
<Slot
|
<Slot
|
||||||
id={remoteTileModel.vm.id}
|
id={remoteTileModel.vm.id}
|
||||||
model={remoteTileModel}
|
model={remoteTileModel}
|
||||||
|
@ -19,9 +19,8 @@ import { useObservableEagerState } from "observable-hooks";
|
|||||||
|
|
||||||
import { SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
|
import { SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
|
||||||
import { CallLayout, GridTileModel, SpotlightTileModel } from "./CallLayout";
|
import { CallLayout, GridTileModel, SpotlightTileModel } from "./CallLayout";
|
||||||
import { DragCallback } from "./Grid";
|
import { DragCallback, useLayout } from "./Grid";
|
||||||
import styles from "./SpotlightExpandedLayout.module.css";
|
import styles from "./SpotlightExpandedLayout.module.css";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "expanded spotlight" layout, in which the spotlight
|
* An implementation of the "expanded spotlight" layout, in which the spotlight
|
||||||
@ -29,27 +28,21 @@ import { useReactiveState } from "../useReactiveState";
|
|||||||
*/
|
*/
|
||||||
export const makeSpotlightExpandedLayout: CallLayout<
|
export const makeSpotlightExpandedLayout: CallLayout<
|
||||||
SpotlightExpandedLayoutModel
|
SpotlightExpandedLayoutModel
|
||||||
> = ({ minBounds, pipAlignment }) => ({
|
> = ({ pipAlignment }) => ({
|
||||||
scrollingOnTop: true,
|
scrollingOnTop: true,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
|
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[width, height, model.spotlight],
|
|
||||||
);
|
|
||||||
|
|
||||||
const spotlightTileModel: SpotlightTileModel = useMemo(
|
const spotlightTileModel: SpotlightTileModel = useMemo(
|
||||||
() => ({ type: "spotlight", vms: model.spotlight, maximised: true }),
|
() => ({ type: "spotlight", vms: model.spotlight, maximised: true }),
|
||||||
[model.spotlight],
|
[model.spotlight],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
<Slot
|
<Slot
|
||||||
className={styles.spotlight}
|
className={styles.spotlight}
|
||||||
id="spotlight"
|
id="spotlight"
|
||||||
@ -63,14 +56,9 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
||||||
|
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[width, height, model.pip === undefined, pipAlignmentValue],
|
|
||||||
);
|
|
||||||
|
|
||||||
const pipTileModel: GridTileModel | undefined = useMemo(
|
const pipTileModel: GridTileModel | undefined = useMemo(
|
||||||
() => model.pip && { type: "grid", vm: model.pip },
|
() => model.pip && { type: "grid", vm: model.pip },
|
||||||
[model.pip],
|
[model.pip],
|
||||||
@ -86,7 +74,7 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
{pipTileModel && (
|
{pipTileModel && (
|
||||||
<Slot
|
<Slot
|
||||||
className={styles.pip}
|
className={styles.pip}
|
||||||
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
|||||||
import { CallLayout, GridTileModel, TileModel } from "./CallLayout";
|
import { CallLayout, GridTileModel, TileModel } from "./CallLayout";
|
||||||
import { SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
|
import { SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
|
||||||
import styles from "./SpotlightLandscapeLayout.module.css";
|
import styles from "./SpotlightLandscapeLayout.module.css";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
import { useLayout } from "./Grid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "spotlight landscape" layout, in which the spotlight
|
* An implementation of the "spotlight landscape" layout, in which the spotlight
|
||||||
@ -37,7 +37,8 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
|
useObservableEagerState(minBounds);
|
||||||
const tileModel: TileModel = useMemo(
|
const tileModel: TileModel = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
type: "spotlight",
|
type: "spotlight",
|
||||||
@ -46,13 +47,9 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
}),
|
}),
|
||||||
[model.spotlight],
|
[model.spotlight],
|
||||||
);
|
);
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.grid.length, width, height, model.spotlight],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
<div className={styles.spotlight}>
|
<div className={styles.spotlight}>
|
||||||
<Slot className={styles.slot} id="spotlight" model={tileModel} />
|
<Slot className={styles.slot} id="spotlight" model={tileModel} />
|
||||||
</div>
|
</div>
|
||||||
@ -65,18 +62,15 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
|
useObservableEagerState(minBounds);
|
||||||
const tileModels: GridTileModel[] = useMemo(
|
const tileModels: GridTileModel[] = useMemo(
|
||||||
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
||||||
[model.grid],
|
[model.grid],
|
||||||
);
|
);
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.spotlight.length, model.grid, width, height],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.spotlight, {
|
className={classNames(styles.spotlight, {
|
||||||
[styles.withIndicators]: model.spotlight.length > 1,
|
[styles.withIndicators]: model.spotlight.length > 1,
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
} from "./CallLayout";
|
} from "./CallLayout";
|
||||||
import { SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
|
import { SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
|
||||||
import styles from "./SpotlightPortraitLayout.module.css";
|
import styles from "./SpotlightPortraitLayout.module.css";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
import { useLayout } from "./Grid";
|
||||||
|
|
||||||
interface GridCSSProperties extends CSSProperties {
|
interface GridCSSProperties extends CSSProperties {
|
||||||
"--grid-gap": string;
|
"--grid-gap": string;
|
||||||
@ -48,7 +48,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
const tileModel: TileModel = useMemo(
|
const tileModel: TileModel = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
type: "spotlight",
|
type: "spotlight",
|
||||||
@ -57,13 +57,9 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
}),
|
}),
|
||||||
[model.spotlight],
|
[model.spotlight],
|
||||||
);
|
);
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.grid.length, width, height, model.spotlight],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-generation={generation} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
<div className={styles.spotlight}>
|
<div className={styles.spotlight}>
|
||||||
<Slot className={styles.slot} id="spotlight" model={tileModel} />
|
<Slot className={styles.slot} id="spotlight" model={tileModel} />
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +71,8 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
{ model, Slot },
|
{ model, Slot },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
useLayout();
|
||||||
|
const { width } = useObservableEagerState(minBounds);
|
||||||
const { gap, tileWidth, tileHeight } = arrangeTiles(
|
const { gap, tileWidth, tileHeight } = arrangeTiles(
|
||||||
width,
|
width,
|
||||||
0,
|
0,
|
||||||
@ -85,15 +82,10 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
() => model.grid.map((vm) => ({ type: "grid", vm })),
|
||||||
[model.grid],
|
[model.grid],
|
||||||
);
|
);
|
||||||
const [generation] = useReactiveState<number>(
|
|
||||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
|
||||||
[model.spotlight.length, model.grid, width, height],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-generation={generation}
|
|
||||||
className={styles.layer}
|
className={styles.layer}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user