Skip to content

Commit

Permalink
Tell tiles whether they're actually visible in a more timely manner
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Oct 28, 2024
1 parent 7650762 commit d3a5546
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 20 deletions.
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
8 changes: 7 additions & 1 deletion src/grid/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
}
>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
<Slot
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/grid/OneOnOneLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
<Slot
id={model.remote.id}
model={model.remote}
onVisibilityChange={model.remote.setVisible}
className={styles.container}
style={{ width: tileWidth, height: tileHeight }}
>
Expand All @@ -60,6 +61,7 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
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
1 change: 1 addition & 0 deletions src/grid/SpotlightExpandedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
8 changes: 7 additions & 1 deletion src/grid/SpotlightLandscapeLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ export const makeSpotlightLandscapeLayout: CallLayout<
/>
<div className={styles.grid}>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
<Slot
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))}
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/grid/SpotlightPortraitLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ export const makeSpotlightPortraitLayout: CallLayout<
/>
<div className={styles.grid}>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
<Slot
key={m.id}
className={styles.slot}
id={m.id}
model={m}
onVisibilityChange={m.setVisible}
/>
))}
</div>
</div>
Expand Down
4 changes: 1 addition & 3 deletions src/state/TileViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export class GridTileViewModel extends ViewModel {
*/
public readonly visible: Observable<boolean> = 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<UserMediaViewModel>) {
super();
Expand Down
11 changes: 0 additions & 11 deletions src/tile/GridTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ReactNode,
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -284,16 +283,6 @@ export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
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 (
Expand Down

0 comments on commit d3a5546

Please sign in to comment.