diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 51d258e3b..2983357b1 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -24,6 +24,7 @@ import { createContext, forwardRef, memo, + useCallback, useContext, useEffect, useMemo, @@ -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"; @@ -51,6 +54,7 @@ interface Tile { id: string; model: Model; onDrag: DragCallback | undefined; + setVisible: (visible: boolean) => void; } type PlacedTile = Tile & Rect; @@ -84,6 +88,7 @@ interface SlotProps extends Omit, "onDrag"> { id: string; model: Model; onDrag?: DragCallback; + onVisibilityChange?: (visible: boolean) => void; style?: CSSProperties; className?: string; } @@ -131,6 +136,11 @@ export function useUpdateLayout(): void { ); } +const windowHeightObservable = fromEvent(window, "resize").pipe( + startWith(null), + map(() => window.innerHeight), +); + export interface LayoutProps { ref: LegacyRef; model: LayoutModel; @@ -232,6 +242,7 @@ export function Grid< const [gridRoot, gridRef2] = useState(null); const gridRef = useMergedRefs(gridRef1, gridRef2); + const windowHeight = useObservableEagerState(windowHeightObservable); const [layoutRoot, setLayoutRoot] = useState(null); const [generation, setGeneration] = useState(null); const tiles = useInitial(() => new Map>()); @@ -239,12 +250,34 @@ export function Grid< const Slot: FC> = useMemo( () => - function Slot({ id, model, onDrag, style, className, ...props }) { + function Slot({ + id, + model, + onDrag, + onVisibilityChange, + style, + className, + ...props + }) { const ref = useRef(null); + const prevVisible = useRef(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 (
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(null); diff --git a/src/grid/GridLayout.tsx b/src/grid/GridLayout.tsx index 9591ae4eb..3dc3bef1d 100644 --- a/src/grid/GridLayout.tsx +++ b/src/grid/GridLayout.tsx @@ -93,7 +93,13 @@ export const makeGridLayout: CallLayout = ({ } > {model.grid.map((m) => ( - + ))}
); diff --git a/src/grid/OneOnOneLayout.tsx b/src/grid/OneOnOneLayout.tsx index 417f01519..03ff5b32e 100644 --- a/src/grid/OneOnOneLayout.tsx +++ b/src/grid/OneOnOneLayout.tsx @@ -52,6 +52,7 @@ export const makeOneOnOneLayout: CallLayout = ({ @@ -60,6 +61,7 @@ export const makeOneOnOneLayout: CallLayout = ({ id={model.local.id} model={model.local} onDrag={onDragLocalTile} + onVisibilityChange={model.local.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightExpandedLayout.tsx b/src/grid/SpotlightExpandedLayout.tsx index 0430082da..084950360 100644 --- a/src/grid/SpotlightExpandedLayout.tsx +++ b/src/grid/SpotlightExpandedLayout.tsx @@ -63,6 +63,7 @@ export const makeSpotlightExpandedLayout: CallLayout< id={model.pip.id} model={model.pip} onDrag={onDragPip} + onVisibilityChange={model.pip.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightLandscapeLayout.tsx b/src/grid/SpotlightLandscapeLayout.tsx index e7465aa48..b9e6b2891 100644 --- a/src/grid/SpotlightLandscapeLayout.tsx +++ b/src/grid/SpotlightLandscapeLayout.tsx @@ -63,7 +63,13 @@ export const makeSpotlightLandscapeLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/grid/SpotlightPortraitLayout.tsx b/src/grid/SpotlightPortraitLayout.tsx index 2cc0ce0a8..e617160e9 100644 --- a/src/grid/SpotlightPortraitLayout.tsx +++ b/src/grid/SpotlightPortraitLayout.tsx @@ -84,7 +84,13 @@ export const makeSpotlightPortraitLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 3daf739bd..3c25907ea 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -24,9 +24,7 @@ export class GridTileViewModel extends ViewModel { */ public readonly visible: Observable = this.visible_; - public setVisible(value: boolean): void { - this.visible_.next(value); - } + public setVisible = (value: boolean): void => this.visible_.next(value); public constructor(public readonly media: Observable) { super(); diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index a80197c64..181c309d3 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -10,7 +10,6 @@ import { ReactNode, forwardRef, useCallback, - useEffect, useRef, useState, } from "react"; @@ -284,16 +283,6 @@ export const GridTile = forwardRef( const ref = useMergedRefs(ourRef, theirRef); const media = useObservableEagerState(vm.media); const displayName = useDisplayName(media); - useEffect(() => { - const io = new IntersectionObserver( - (entries) => { - vm.setVisible(entries.some((e) => e.isIntersecting)); - }, - { threshold: 1 }, - ); - io.observe(ourRef.current!); - return (): void => io.disconnect(); - }, [vm]); if (media instanceof LocalUserMediaViewModel) { return (