Decouple video grid from video tile components
This is an attempt to address the feedback in https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404 that the video grid and video tile components have become too tightly coupled. After this change, the only requirements that the video grid makes of its child components are: - They accept ref, style, and item props - They attach the ref and styles to a react-spring animated element Note: I removed the video grid Storybook file, because I'm not aware of anyone using Storybook for development of Element Call beyond Robert, and it would take some effort to fix to work with these changes.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022-2023 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.
|
||||
@@ -14,21 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Key, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FullGestureState, useDrag, useGesture } from "@use-gesture/react";
|
||||
import React, {
|
||||
ComponentProps,
|
||||
Key,
|
||||
Ref,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
EventTypes,
|
||||
FullGestureState,
|
||||
Handler,
|
||||
useGesture,
|
||||
} from "@use-gesture/react";
|
||||
import {
|
||||
animated,
|
||||
SpringRef,
|
||||
SpringValue,
|
||||
SpringValues,
|
||||
useSprings,
|
||||
} from "@react-spring/web";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
|
||||
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
|
||||
|
||||
import styles from "./VideoGrid.module.css";
|
||||
import { Layout } from "../room/GridLayoutMenu";
|
||||
import { TileDescriptor } from "./TileDescriptor";
|
||||
import { TileWrapper } from "./TileWrapper";
|
||||
|
||||
interface TilePosition {
|
||||
x: number;
|
||||
@@ -39,7 +52,7 @@ interface TilePosition {
|
||||
}
|
||||
|
||||
interface Tile {
|
||||
key: Key;
|
||||
key: string;
|
||||
order: number;
|
||||
item: TileDescriptor;
|
||||
remove: boolean;
|
||||
@@ -717,20 +730,18 @@ interface DragTileData {
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ChildrenProperties extends ReactDOMAttributes {
|
||||
key: Key;
|
||||
export interface ChildrenProperties {
|
||||
ref: Ref<HTMLElement>;
|
||||
style: ComponentProps<typeof animated.div>["style"];
|
||||
/**
|
||||
* The width this tile will have once its animations have settled.
|
||||
*/
|
||||
targetWidth: number;
|
||||
/**
|
||||
* The height this tile will have once its animations have settled.
|
||||
*/
|
||||
targetHeight: number;
|
||||
item: TileDescriptor;
|
||||
opacity: SpringValue<number>;
|
||||
scale: SpringValue<number>;
|
||||
shadow: SpringValue<number>;
|
||||
zIndex: SpringValue<number>;
|
||||
x: SpringValue<number>;
|
||||
y: SpringValue<number>;
|
||||
width: SpringValue<number>;
|
||||
height: SpringValue<number>;
|
||||
[index: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VideoGridProps {
|
||||
@@ -1063,117 +1074,132 @@ export function VideoGrid({
|
||||
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
|
||||
);
|
||||
|
||||
const bindTile = useDrag(
|
||||
({ args: [key], active, xy, movement, tap, last, event }) => {
|
||||
event.preventDefault();
|
||||
// Callback for useDrag. We could call useDrag here, but the default
|
||||
// pattern of spreading {...bind()} across the children to bind the gesture
|
||||
// ends up breaking memoization and ruining this component's performance.
|
||||
// Instead, we pass this callback to each tile via a ref, to let them bind the
|
||||
// gesture using the much more sensible ref-based method.
|
||||
const onTileDrag = (
|
||||
tileId: string,
|
||||
{
|
||||
active,
|
||||
xy,
|
||||
movement,
|
||||
tap,
|
||||
last,
|
||||
event,
|
||||
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (tap) {
|
||||
onTap(key);
|
||||
return;
|
||||
}
|
||||
if (tap) {
|
||||
onTap(tileId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (layout !== "freedom") return;
|
||||
if (layout !== "freedom") return;
|
||||
|
||||
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
|
||||
const dragTile = tiles[dragTileIndex];
|
||||
const dragTilePosition = tilePositions[dragTile.order];
|
||||
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
|
||||
const dragTile = tiles[dragTileIndex];
|
||||
const dragTilePosition = tilePositions[dragTile.order];
|
||||
|
||||
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
||||
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
||||
|
||||
let newTiles = tiles;
|
||||
let newTiles = tiles;
|
||||
|
||||
if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
|
||||
// We're in 1:1 mode, so only the local tile should be draggable
|
||||
if (!dragTile.item.isLocal) return;
|
||||
if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
|
||||
// We're in 1:1 mode, so only the local tile should be draggable
|
||||
if (!dragTile.item.isLocal) return;
|
||||
|
||||
// Position should only update on the very last event, to avoid
|
||||
// compounding the offset on every drag event
|
||||
if (last) {
|
||||
const remotePosition = tilePositions[1];
|
||||
// Position should only update on the very last event, to avoid
|
||||
// compounding the offset on every drag event
|
||||
if (last) {
|
||||
const remotePosition = tilePositions[1];
|
||||
|
||||
const pipGap = getPipGap(
|
||||
gridBounds.width / gridBounds.height,
|
||||
gridBounds.width
|
||||
);
|
||||
const pipMinX = remotePosition.x + pipGap;
|
||||
const pipMinY = remotePosition.y + pipGap;
|
||||
const pipMaxX =
|
||||
remotePosition.x +
|
||||
remotePosition.width -
|
||||
dragTilePosition.width -
|
||||
pipGap;
|
||||
const pipMaxY =
|
||||
remotePosition.y +
|
||||
remotePosition.height -
|
||||
dragTilePosition.height -
|
||||
pipGap;
|
||||
|
||||
const newPipXRatio =
|
||||
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
|
||||
const newPipYRatio =
|
||||
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
|
||||
|
||||
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
|
||||
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
|
||||
}
|
||||
} else {
|
||||
const hoverTile = tiles.find(
|
||||
(tile) =>
|
||||
tile.key !== key &&
|
||||
isInside(cursorPosition, tilePositions[tile.order])
|
||||
const pipGap = getPipGap(
|
||||
gridBounds.width / gridBounds.height,
|
||||
gridBounds.width
|
||||
);
|
||||
const pipMinX = remotePosition.x + pipGap;
|
||||
const pipMinY = remotePosition.y + pipGap;
|
||||
const pipMaxX =
|
||||
remotePosition.x +
|
||||
remotePosition.width -
|
||||
dragTilePosition.width -
|
||||
pipGap;
|
||||
const pipMaxY =
|
||||
remotePosition.y +
|
||||
remotePosition.height -
|
||||
dragTilePosition.height -
|
||||
pipGap;
|
||||
|
||||
if (hoverTile) {
|
||||
// Shift the tiles into their new order
|
||||
newTiles = newTiles.map((tile) => {
|
||||
let order = tile.order;
|
||||
if (order < dragTile.order) {
|
||||
if (order >= hoverTile.order) order++;
|
||||
} else if (order > dragTile.order) {
|
||||
if (order <= hoverTile.order) order--;
|
||||
} else {
|
||||
order = hoverTile.order;
|
||||
}
|
||||
const newPipXRatio =
|
||||
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
|
||||
const newPipYRatio =
|
||||
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
|
||||
|
||||
let focused;
|
||||
if (tile === hoverTile) {
|
||||
focused = dragTile.focused;
|
||||
} else if (tile === dragTile) {
|
||||
focused = hoverTile.focused;
|
||||
} else {
|
||||
focused = tile.focused;
|
||||
}
|
||||
|
||||
return { ...tile, order, focused };
|
||||
});
|
||||
|
||||
reorderTiles(newTiles, layout);
|
||||
|
||||
setTileState((state) => ({ ...state, tiles: newTiles }));
|
||||
}
|
||||
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
|
||||
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
|
||||
}
|
||||
} else {
|
||||
const hoverTile = tiles.find(
|
||||
(tile) =>
|
||||
tile.key !== tileId &&
|
||||
isInside(cursorPosition, tilePositions[tile.order])
|
||||
);
|
||||
|
||||
if (active) {
|
||||
if (!draggingTileRef.current) {
|
||||
draggingTileRef.current = {
|
||||
key: dragTile.key,
|
||||
offsetX: dragTilePosition.x,
|
||||
offsetY: dragTilePosition.y,
|
||||
x: movement[0],
|
||||
y: movement[1],
|
||||
};
|
||||
} else {
|
||||
draggingTileRef.current.x = movement[0];
|
||||
draggingTileRef.current.y = movement[1];
|
||||
}
|
||||
if (hoverTile) {
|
||||
// Shift the tiles into their new order
|
||||
newTiles = newTiles.map((tile) => {
|
||||
let order = tile.order;
|
||||
if (order < dragTile.order) {
|
||||
if (order >= hoverTile.order) order++;
|
||||
} else if (order > dragTile.order) {
|
||||
if (order <= hoverTile.order) order--;
|
||||
} else {
|
||||
order = hoverTile.order;
|
||||
}
|
||||
|
||||
let focused;
|
||||
if (tile === hoverTile) {
|
||||
focused = dragTile.focused;
|
||||
} else if (tile === dragTile) {
|
||||
focused = hoverTile.focused;
|
||||
} else {
|
||||
focused = tile.focused;
|
||||
}
|
||||
|
||||
return { ...tile, order, focused };
|
||||
});
|
||||
|
||||
reorderTiles(newTiles, layout);
|
||||
|
||||
setTileState((state) => ({ ...state, tiles: newTiles }));
|
||||
}
|
||||
}
|
||||
|
||||
if (active) {
|
||||
if (!draggingTileRef.current) {
|
||||
draggingTileRef.current = {
|
||||
key: dragTile.key,
|
||||
offsetX: dragTilePosition.x,
|
||||
offsetY: dragTilePosition.y,
|
||||
x: movement[0],
|
||||
y: movement[1],
|
||||
};
|
||||
} else {
|
||||
draggingTileRef.current = null;
|
||||
draggingTileRef.current.x = movement[0];
|
||||
draggingTileRef.current.y = movement[1];
|
||||
}
|
||||
} else {
|
||||
draggingTileRef.current = null;
|
||||
}
|
||||
|
||||
api.start(animate(newTiles));
|
||||
},
|
||||
{ filterTaps: true, pointer: { buttons: [1] } }
|
||||
);
|
||||
api.start(animate(newTiles));
|
||||
};
|
||||
|
||||
const onTileDragRef = useRef(onTileDrag);
|
||||
onTileDragRef.current = onTileDrag;
|
||||
|
||||
const onGridGesture = useCallback(
|
||||
(
|
||||
@@ -1220,18 +1246,23 @@ export function VideoGrid({
|
||||
|
||||
return (
|
||||
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
|
||||
{springs.map((style, i) => {
|
||||
{springs.map((spring, i) => {
|
||||
const tile = tiles[i];
|
||||
const tilePosition = tilePositions[tile.order];
|
||||
|
||||
return children({
|
||||
...bindTile(tile.key),
|
||||
...style,
|
||||
key: tile.item.id,
|
||||
targetWidth: tilePosition.width,
|
||||
targetHeight: tilePosition.height,
|
||||
item: tile.item,
|
||||
});
|
||||
return (
|
||||
<TileWrapper
|
||||
key={tile.key}
|
||||
id={tile.key}
|
||||
onDragRef={onTileDragRef}
|
||||
targetWidth={tilePosition.width}
|
||||
targetHeight={tilePosition.height}
|
||||
item={tile.item}
|
||||
{...spring}
|
||||
>
|
||||
{children}
|
||||
</TileWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user