2022-05-04 17:09:48 +01:00
|
|
|
/*
|
|
|
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
import React, { Key, useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
import { FullGestureState, useDrag, useGesture } from "@use-gesture/react";
|
|
|
|
import { Interpolation, SpringValue, useSprings } from "@react-spring/web";
|
2022-04-07 14:22:36 -07:00
|
|
|
import useMeasure from "react-use-measure";
|
|
|
|
import { ResizeObserver } from "@juggle/resize-observer";
|
2022-08-12 19:27:34 +02:00
|
|
|
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
|
|
|
|
|
2022-04-07 14:22:36 -07:00
|
|
|
import styles from "./VideoGrid.module.css";
|
2022-08-12 19:27:34 +02:00
|
|
|
import { Layout } from "../room/GridLayoutMenu";
|
|
|
|
import { Participant } from "../room/InCallView";
|
|
|
|
|
|
|
|
interface TilePosition {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
zIndex: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Tile {
|
|
|
|
key: Key;
|
|
|
|
order: number;
|
|
|
|
item: Participant;
|
|
|
|
remove: boolean;
|
|
|
|
focused: boolean;
|
|
|
|
presenter: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
type LayoutDirection = "vertical" | "horizontal";
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
|
|
|
|
layout: Layout;
|
|
|
|
setLayout: (layout: Layout) => void;
|
|
|
|
} {
|
|
|
|
const layoutRef = useRef<Layout>("freedom");
|
|
|
|
const revertLayoutRef = useRef<Layout>("freedom");
|
2022-04-07 14:22:36 -07:00
|
|
|
const prevHasScreenshareFeeds = useRef(hasScreenshareFeeds);
|
|
|
|
const [, forceUpdate] = useState({});
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const setLayout = useCallback((layout: Layout) => {
|
2022-04-07 14:22:36 -07:00
|
|
|
// Store the user's set layout to revert to after a screenshare is finished
|
|
|
|
revertLayoutRef.current = layout;
|
|
|
|
layoutRef.current = layout;
|
|
|
|
forceUpdate({});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// Note: We need the returned layout to update synchronously with a change in hasScreenshareFeeds
|
|
|
|
// so use refs and avoid useEffect.
|
|
|
|
if (prevHasScreenshareFeeds.current !== hasScreenshareFeeds) {
|
|
|
|
if (hasScreenshareFeeds) {
|
|
|
|
// Automatically switch to spotlight layout when there's a screenshare
|
|
|
|
layoutRef.current = "spotlight";
|
|
|
|
} else {
|
|
|
|
// When the screenshares have ended, revert to the previous layout
|
|
|
|
layoutRef.current = revertLayoutRef.current;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
prevHasScreenshareFeeds.current = hasScreenshareFeeds;
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
return { layout: layoutRef.current, setLayout };
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
2022-05-17 17:35:35 -04:00
|
|
|
const GAP = 8;
|
|
|
|
|
2022-04-07 14:22:36 -07:00
|
|
|
function useIsMounted() {
|
2022-08-12 19:27:34 +02:00
|
|
|
const isMountedRef = useRef<boolean>(false);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
isMountedRef.current = true;
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
isMountedRef.current = false;
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return isMountedRef;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function isInside([x, y]: number[], targetTile: TilePosition): boolean {
|
2022-04-07 14:22:36 -07:00
|
|
|
const left = targetTile.x;
|
|
|
|
const top = targetTile.y;
|
|
|
|
const bottom = targetTile.y + targetTile.height;
|
|
|
|
const right = targetTile.x + targetTile.width;
|
|
|
|
|
|
|
|
if (x < left || x > right || y < top || y > bottom) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const getPipGap = (gridAspectRatio: number): number =>
|
|
|
|
gridAspectRatio < 1 ? 12 : 24;
|
2022-05-17 17:35:35 -04:00
|
|
|
|
2022-04-07 14:22:36 -07:00
|
|
|
function getTilePositions(
|
2022-08-12 19:27:34 +02:00
|
|
|
tileCount: number,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount: number,
|
|
|
|
hasPresenter: boolean,
|
2022-08-12 19:27:34 +02:00
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number,
|
|
|
|
pipXRatio: number,
|
|
|
|
pipYRatio: number,
|
|
|
|
layout: Layout
|
|
|
|
): TilePosition[] {
|
2022-04-07 14:22:36 -07:00
|
|
|
if (layout === "freedom") {
|
2022-10-24 10:22:51 -04:00
|
|
|
if (tileCount === 2 && !hasPresenter && focusedTileCount === 0) {
|
2022-05-17 17:35:35 -04:00
|
|
|
return getOneOnOneLayoutTilePositions(
|
|
|
|
gridWidth,
|
|
|
|
gridHeight,
|
|
|
|
pipXRatio,
|
|
|
|
pipYRatio
|
|
|
|
);
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return getFreedomLayoutTilePositions(
|
|
|
|
tileCount,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount,
|
2022-04-07 14:22:36 -07:00
|
|
|
gridWidth,
|
|
|
|
gridHeight
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return getSpotlightLayoutTilePositions(tileCount, gridWidth, gridHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-17 17:35:35 -04:00
|
|
|
function getOneOnOneLayoutTilePositions(
|
2022-08-12 19:27:34 +02:00
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number,
|
|
|
|
pipXRatio: number,
|
|
|
|
pipYRatio: number
|
|
|
|
): TilePosition[] {
|
2022-05-23 09:59:55 -04:00
|
|
|
const [remotePosition] = getFreedomLayoutTilePositions(
|
|
|
|
1,
|
|
|
|
0,
|
|
|
|
gridWidth,
|
|
|
|
gridHeight
|
|
|
|
);
|
|
|
|
|
2022-04-07 14:22:36 -07:00
|
|
|
const gridAspectRatio = gridWidth / gridHeight;
|
|
|
|
|
|
|
|
const pipWidth = gridAspectRatio < 1 ? 114 : 230;
|
|
|
|
const pipHeight = gridAspectRatio < 1 ? 163 : 155;
|
2022-05-17 17:35:35 -04:00
|
|
|
const pipGap = getPipGap(gridAspectRatio);
|
|
|
|
|
2022-05-23 09:59:55 -04:00
|
|
|
const pipMinX = remotePosition.x + pipGap;
|
|
|
|
const pipMinY = remotePosition.y + pipGap;
|
|
|
|
const pipMaxX = remotePosition.x + remotePosition.width - pipWidth - pipGap;
|
|
|
|
const pipMaxY = remotePosition.y + remotePosition.height - pipHeight - pipGap;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
2022-05-17 17:35:35 -04:00
|
|
|
// Apply the PiP position as a proportion of the available space
|
|
|
|
x: pipMinX + pipXRatio * (pipMaxX - pipMinX),
|
|
|
|
y: pipMinY + pipYRatio * (pipMaxY - pipMinY),
|
2022-04-07 14:22:36 -07:00
|
|
|
width: pipWidth,
|
|
|
|
height: pipHeight,
|
|
|
|
zIndex: 1,
|
|
|
|
},
|
2022-05-23 09:59:55 -04:00
|
|
|
remotePosition,
|
2022-04-07 14:22:36 -07:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function getSpotlightLayoutTilePositions(
|
|
|
|
tileCount: number,
|
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number
|
|
|
|
): TilePosition[] {
|
|
|
|
const tilePositions: TilePosition[] = [];
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const gridAspectRatio = gridWidth / gridHeight;
|
|
|
|
|
|
|
|
if (gridAspectRatio < 1) {
|
|
|
|
// Vertical layout (mobile)
|
|
|
|
const spotlightTileHeight =
|
2022-05-17 17:35:35 -04:00
|
|
|
tileCount > 1 ? (gridHeight - GAP * 3) * (4 / 5) : gridHeight - GAP * 2;
|
2022-04-07 14:22:36 -07:00
|
|
|
const spectatorTileSize =
|
2022-05-17 17:35:35 -04:00
|
|
|
tileCount > 1 ? gridHeight - GAP * 3 - spotlightTileHeight : 0;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
for (let i = 0; i < tileCount; i++) {
|
|
|
|
if (i === 0) {
|
|
|
|
// Spotlight tile
|
|
|
|
tilePositions.push({
|
2022-05-17 17:35:35 -04:00
|
|
|
x: GAP,
|
|
|
|
y: GAP,
|
|
|
|
width: gridWidth - GAP * 2,
|
2022-04-07 14:22:36 -07:00
|
|
|
height: spotlightTileHeight,
|
|
|
|
zIndex: 0,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Spectator tile
|
|
|
|
tilePositions.push({
|
2022-05-17 17:35:35 -04:00
|
|
|
x: (GAP + spectatorTileSize) * (i - 1) + GAP,
|
|
|
|
y: spotlightTileHeight + GAP * 2,
|
2022-04-07 14:22:36 -07:00
|
|
|
width: spectatorTileSize,
|
|
|
|
height: spectatorTileSize,
|
|
|
|
zIndex: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Horizontal layout (desktop)
|
|
|
|
const spotlightTileWidth =
|
2022-05-17 17:35:35 -04:00
|
|
|
tileCount > 1 ? ((gridWidth - GAP * 3) * 4) / 5 : gridWidth - GAP * 2;
|
2022-04-07 14:22:36 -07:00
|
|
|
const spectatorTileWidth =
|
2022-05-17 17:35:35 -04:00
|
|
|
tileCount > 1 ? gridWidth - GAP * 3 - spotlightTileWidth : 0;
|
2022-04-07 14:22:36 -07:00
|
|
|
const spectatorTileHeight = spectatorTileWidth * (9 / 16);
|
|
|
|
|
|
|
|
for (let i = 0; i < tileCount; i++) {
|
|
|
|
if (i === 0) {
|
|
|
|
tilePositions.push({
|
2022-05-17 17:35:35 -04:00
|
|
|
x: GAP,
|
|
|
|
y: GAP,
|
2022-04-07 14:22:36 -07:00
|
|
|
width: spotlightTileWidth,
|
2022-05-17 17:35:35 -04:00
|
|
|
height: gridHeight - GAP * 2,
|
2022-04-07 14:22:36 -07:00
|
|
|
zIndex: 0,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
tilePositions.push({
|
2022-05-17 17:35:35 -04:00
|
|
|
x: GAP * 2 + spotlightTileWidth,
|
|
|
|
y: (GAP + spectatorTileHeight) * (i - 1) + GAP,
|
2022-04-07 14:22:36 -07:00
|
|
|
width: spectatorTileWidth,
|
|
|
|
height: spectatorTileHeight,
|
|
|
|
zIndex: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return tilePositions;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFreedomLayoutTilePositions(
|
2022-08-12 19:27:34 +02:00
|
|
|
tileCount: number,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount: number,
|
2022-08-12 19:27:34 +02:00
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number
|
|
|
|
): TilePosition[] {
|
2022-04-07 14:22:36 -07:00
|
|
|
if (tileCount === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tileCount > 12) {
|
|
|
|
console.warn("Over 12 tiles is not currently supported");
|
|
|
|
}
|
|
|
|
|
|
|
|
const { layoutDirection, itemGridRatio } = getGridLayout(
|
|
|
|
tileCount,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount,
|
2022-04-07 14:22:36 -07:00
|
|
|
gridWidth,
|
|
|
|
gridHeight
|
|
|
|
);
|
|
|
|
|
|
|
|
let itemGridWidth;
|
|
|
|
let itemGridHeight;
|
|
|
|
|
|
|
|
if (layoutDirection === "vertical") {
|
|
|
|
itemGridWidth = gridWidth;
|
|
|
|
itemGridHeight = Math.round(gridHeight * itemGridRatio);
|
|
|
|
} else {
|
|
|
|
itemGridWidth = Math.round(gridWidth * itemGridRatio);
|
|
|
|
itemGridHeight = gridHeight;
|
|
|
|
}
|
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
const itemTileCount = tileCount - focusedTileCount;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const {
|
|
|
|
columnCount: itemColumnCount,
|
|
|
|
rowCount: itemRowCount,
|
|
|
|
tileAspectRatio: itemTileAspectRatio,
|
|
|
|
} = getSubGridLayout(itemTileCount, itemGridWidth, itemGridHeight);
|
|
|
|
|
|
|
|
const itemGridPositions = getSubGridPositions(
|
|
|
|
itemTileCount,
|
|
|
|
itemColumnCount,
|
|
|
|
itemRowCount,
|
|
|
|
itemTileAspectRatio,
|
|
|
|
itemGridWidth,
|
2022-05-17 17:35:35 -04:00
|
|
|
itemGridHeight
|
2022-04-07 14:22:36 -07:00
|
|
|
);
|
|
|
|
const itemGridBounds = getSubGridBoundingBox(itemGridPositions);
|
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
let focusedGridWidth: number;
|
|
|
|
let focusedGridHeight: number;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
if (focusedTileCount === 0) {
|
|
|
|
focusedGridWidth = 0;
|
|
|
|
focusedGridHeight = 0;
|
2022-04-07 14:22:36 -07:00
|
|
|
} else if (layoutDirection === "vertical") {
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedGridWidth = gridWidth;
|
|
|
|
focusedGridHeight =
|
2022-05-17 17:35:35 -04:00
|
|
|
gridHeight - (itemGridBounds.height + (itemTileCount ? GAP * 2 : 0));
|
2022-04-07 14:22:36 -07:00
|
|
|
} else {
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedGridWidth =
|
2022-05-17 17:35:35 -04:00
|
|
|
gridWidth - (itemGridBounds.width + (itemTileCount ? GAP * 2 : 0));
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedGridHeight = gridHeight;
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
2022-10-18 00:30:37 -04:00
|
|
|
columnCount: focusedColumnCount,
|
|
|
|
rowCount: focusedRowCount,
|
|
|
|
tileAspectRatio: focusedTileAspectRatio,
|
2022-10-18 00:48:29 -04:00
|
|
|
} = getSubGridLayout(focusedTileCount, focusedGridWidth, focusedGridHeight);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
const focusedGridPositions = getSubGridPositions(
|
|
|
|
focusedTileCount,
|
|
|
|
focusedColumnCount,
|
|
|
|
focusedRowCount,
|
|
|
|
focusedTileAspectRatio,
|
|
|
|
focusedGridWidth,
|
|
|
|
focusedGridHeight
|
2022-04-07 14:22:36 -07:00
|
|
|
);
|
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
const tilePositions = [...focusedGridPositions, ...itemGridPositions];
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-10-18 00:48:29 -04:00
|
|
|
centerTiles(focusedGridPositions, focusedGridWidth, focusedGridHeight, 0, 0);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
if (layoutDirection === "vertical") {
|
|
|
|
centerTiles(
|
|
|
|
itemGridPositions,
|
|
|
|
gridWidth,
|
2022-10-18 00:30:37 -04:00
|
|
|
gridHeight - focusedGridHeight,
|
2022-04-07 14:22:36 -07:00
|
|
|
0,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedGridHeight
|
2022-04-07 14:22:36 -07:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
centerTiles(
|
|
|
|
itemGridPositions,
|
2022-10-18 00:30:37 -04:00
|
|
|
gridWidth - focusedGridWidth,
|
2022-04-07 14:22:36 -07:00
|
|
|
gridHeight,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedGridWidth,
|
2022-04-07 14:22:36 -07:00
|
|
|
0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return tilePositions;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function getSubGridBoundingBox(positions: TilePosition[]): {
|
|
|
|
left: number;
|
|
|
|
right: number;
|
|
|
|
top: number;
|
|
|
|
bottom: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
} {
|
2022-04-07 14:22:36 -07:00
|
|
|
let left = 0;
|
|
|
|
let right = 0;
|
|
|
|
let top = 0;
|
|
|
|
let bottom = 0;
|
|
|
|
|
|
|
|
for (let i = 0; i < positions.length; i++) {
|
|
|
|
const { x, y, width, height } = positions[i];
|
|
|
|
|
|
|
|
if (i === 0) {
|
|
|
|
left = x;
|
|
|
|
right = x + width;
|
|
|
|
top = y;
|
|
|
|
bottom = y + height;
|
|
|
|
} else {
|
|
|
|
if (x < left) {
|
|
|
|
left = x;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (y < top) {
|
|
|
|
top = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (x + width > right) {
|
|
|
|
right = x + width;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (y + height > bottom) {
|
|
|
|
bottom = y + height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
left,
|
|
|
|
right,
|
|
|
|
top,
|
|
|
|
bottom,
|
|
|
|
width: right - left,
|
|
|
|
height: bottom - top,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function isMobileBreakpoint(gridWidth: number, gridHeight: number): boolean {
|
2022-04-07 14:22:36 -07:00
|
|
|
const gridAspectRatio = gridWidth / gridHeight;
|
|
|
|
return gridAspectRatio < 1;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function getGridLayout(
|
|
|
|
tileCount: number,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount: number,
|
2022-08-12 19:27:34 +02:00
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number
|
|
|
|
): { itemGridRatio: number; layoutDirection: LayoutDirection } {
|
|
|
|
let layoutDirection: LayoutDirection = "horizontal";
|
2022-04-07 14:22:36 -07:00
|
|
|
let itemGridRatio = 1;
|
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
if (focusedTileCount === 0) {
|
2022-04-07 14:22:36 -07:00
|
|
|
return { itemGridRatio, layoutDirection };
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isMobileBreakpoint(gridWidth, gridHeight)) {
|
|
|
|
layoutDirection = "vertical";
|
|
|
|
itemGridRatio = 1 / 3;
|
|
|
|
} else {
|
|
|
|
layoutDirection = "horizontal";
|
|
|
|
itemGridRatio = 1 / 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { itemGridRatio, layoutDirection };
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function centerTiles(
|
|
|
|
positions: TilePosition[],
|
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number,
|
|
|
|
offsetLeft: number,
|
|
|
|
offsetTop: number
|
|
|
|
) {
|
2022-04-07 14:22:36 -07:00
|
|
|
const bounds = getSubGridBoundingBox(positions);
|
|
|
|
|
|
|
|
const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
|
|
|
|
const topOffset = Math.round((gridHeight - bounds.height) / 2) + offsetTop;
|
|
|
|
|
|
|
|
applyTileOffsets(positions, leftOffset, topOffset);
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function applyTileOffsets(
|
|
|
|
positions: TilePosition[],
|
|
|
|
leftOffset: number,
|
|
|
|
topOffset: number
|
|
|
|
) {
|
2022-04-07 14:22:36 -07:00
|
|
|
for (const position of positions) {
|
|
|
|
position.x += leftOffset;
|
|
|
|
position.y += topOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
function getSubGridLayout(
|
|
|
|
tileCount: number,
|
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number
|
|
|
|
): { columnCount: number; rowCount: number; tileAspectRatio: number } {
|
2022-04-07 14:22:36 -07:00
|
|
|
const gridAspectRatio = gridWidth / gridHeight;
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
let columnCount: number;
|
|
|
|
let rowCount: number;
|
|
|
|
let tileAspectRatio: number = 16 / 9;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
if (gridAspectRatio < 3 / 4) {
|
|
|
|
// Phone
|
|
|
|
if (tileCount === 1) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = 1;
|
|
|
|
tileAspectRatio = 0;
|
|
|
|
} else if (tileCount <= 4) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = tileCount;
|
|
|
|
} else if (tileCount <= 12) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = Math.ceil(tileCount / columnCount);
|
|
|
|
tileAspectRatio = 0;
|
|
|
|
} else {
|
|
|
|
// Unsupported
|
|
|
|
columnCount = 3;
|
|
|
|
rowCount = Math.ceil(tileCount / columnCount);
|
|
|
|
tileAspectRatio = 1;
|
|
|
|
}
|
|
|
|
} else if (gridAspectRatio < 1) {
|
|
|
|
// Tablet
|
|
|
|
if (tileCount === 1) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = 1;
|
|
|
|
tileAspectRatio = 0;
|
|
|
|
} else if (tileCount <= 4) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = tileCount;
|
|
|
|
} else if (tileCount <= 12) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = Math.ceil(tileCount / columnCount);
|
|
|
|
} else {
|
|
|
|
// Unsupported
|
|
|
|
columnCount = 3;
|
|
|
|
rowCount = Math.ceil(tileCount / columnCount);
|
|
|
|
tileAspectRatio = 1;
|
|
|
|
}
|
|
|
|
} else if (gridAspectRatio < 17 / 9) {
|
|
|
|
// Computer
|
|
|
|
if (tileCount === 1) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = 1;
|
|
|
|
} else if (tileCount === 2) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = 1;
|
|
|
|
} else if (tileCount <= 4) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = 2;
|
|
|
|
} else if (tileCount <= 6) {
|
|
|
|
columnCount = 3;
|
|
|
|
rowCount = 2;
|
|
|
|
} else if (tileCount <= 8) {
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 2;
|
|
|
|
tileAspectRatio = 1;
|
|
|
|
} else if (tileCount <= 12) {
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 3;
|
|
|
|
tileAspectRatio = 1;
|
|
|
|
} else {
|
|
|
|
// Unsupported
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 4;
|
|
|
|
}
|
|
|
|
} else if (gridAspectRatio <= 32 / 9) {
|
|
|
|
// Ultrawide
|
|
|
|
if (tileCount === 1) {
|
|
|
|
columnCount = 1;
|
|
|
|
rowCount = 1;
|
|
|
|
} else if (tileCount === 2) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = 1;
|
|
|
|
} else if (tileCount <= 4) {
|
|
|
|
columnCount = 2;
|
|
|
|
rowCount = 2;
|
|
|
|
} else if (tileCount <= 6) {
|
|
|
|
columnCount = 3;
|
|
|
|
rowCount = 2;
|
|
|
|
} else if (tileCount <= 8) {
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 2;
|
|
|
|
} else if (tileCount <= 12) {
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 3;
|
|
|
|
} else {
|
|
|
|
// Unsupported
|
|
|
|
columnCount = 4;
|
|
|
|
rowCount = 4;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Super Ultrawide
|
|
|
|
if (tileCount <= 6) {
|
|
|
|
columnCount = tileCount;
|
|
|
|
rowCount = 1;
|
|
|
|
} else {
|
|
|
|
columnCount = Math.ceil(tileCount / 2);
|
|
|
|
rowCount = 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { columnCount, rowCount, tileAspectRatio };
|
|
|
|
}
|
|
|
|
|
|
|
|
function getSubGridPositions(
|
2022-08-12 19:27:34 +02:00
|
|
|
tileCount: number,
|
|
|
|
columnCount: number,
|
|
|
|
rowCount: number,
|
|
|
|
tileAspectRatio: number,
|
|
|
|
gridWidth: number,
|
|
|
|
gridHeight: number
|
2022-04-07 14:22:36 -07:00
|
|
|
) {
|
|
|
|
if (tileCount === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const newTilePositions: TilePosition[] = [];
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const boxWidth = Math.round(
|
2022-05-17 17:35:35 -04:00
|
|
|
(gridWidth - GAP * (columnCount + 1)) / columnCount
|
2022-04-07 14:22:36 -07:00
|
|
|
);
|
2022-05-17 17:35:35 -04:00
|
|
|
const boxHeight = Math.round((gridHeight - GAP * (rowCount + 1)) / rowCount);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
let tileWidth: number;
|
|
|
|
let tileHeight: number;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
if (tileAspectRatio) {
|
|
|
|
const boxAspectRatio = boxWidth / boxHeight;
|
|
|
|
|
|
|
|
if (boxAspectRatio > tileAspectRatio) {
|
|
|
|
tileWidth = boxHeight * tileAspectRatio;
|
|
|
|
tileHeight = boxHeight;
|
|
|
|
} else {
|
|
|
|
tileWidth = boxWidth;
|
|
|
|
tileHeight = boxWidth / tileAspectRatio;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tileWidth = boxWidth;
|
|
|
|
tileHeight = boxHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < tileCount; i++) {
|
|
|
|
const verticalIndex = Math.floor(i / columnCount);
|
2022-05-17 17:35:35 -04:00
|
|
|
const top = verticalIndex * GAP + verticalIndex * tileHeight;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
let rowItemCount: number;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
if (verticalIndex + 1 === rowCount && tileCount % columnCount !== 0) {
|
|
|
|
rowItemCount = tileCount % columnCount;
|
|
|
|
} else {
|
|
|
|
rowItemCount = columnCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
const horizontalIndex = i % columnCount;
|
|
|
|
|
|
|
|
let centeringPadding = 0;
|
|
|
|
|
|
|
|
if (rowItemCount < columnCount) {
|
2022-05-17 17:35:35 -04:00
|
|
|
const subgridWidth = tileWidth * columnCount + (GAP * columnCount - 1);
|
2022-04-07 14:22:36 -07:00
|
|
|
centeringPadding = Math.round(
|
2022-05-17 17:35:35 -04:00
|
|
|
(subgridWidth - (tileWidth * rowItemCount + (GAP * rowItemCount - 1))) /
|
2022-04-07 14:22:36 -07:00
|
|
|
2
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const left =
|
2022-05-17 17:35:35 -04:00
|
|
|
centeringPadding + GAP * horizontalIndex + tileWidth * horizontalIndex;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
newTilePositions.push({
|
|
|
|
width: tileWidth,
|
|
|
|
height: tileHeight,
|
|
|
|
x: left,
|
|
|
|
y: top,
|
|
|
|
zIndex: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return newTilePositions;
|
|
|
|
}
|
|
|
|
|
2022-09-22 12:03:57 +01:00
|
|
|
// Sets the 'order' property on tiles based on the layout param and
|
|
|
|
// other properties of the tiles, eg. 'focused' and 'presenter'
|
2022-08-12 19:27:34 +02:00
|
|
|
function reorderTiles(tiles: Tile[], layout: Layout) {
|
2022-10-18 00:48:29 -04:00
|
|
|
if (
|
|
|
|
layout === "freedom" &&
|
|
|
|
tiles.length === 2 &&
|
2022-10-24 10:22:51 -04:00
|
|
|
!tiles.some((t) => t.presenter || t.focused)
|
2022-10-18 00:48:29 -04:00
|
|
|
) {
|
2022-05-24 16:54:33 -04:00
|
|
|
// 1:1 layout
|
2022-05-24 16:55:53 -04:00
|
|
|
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
|
2022-05-24 16:54:33 -04:00
|
|
|
} else {
|
2022-08-12 19:27:34 +02:00
|
|
|
const focusedTiles: Tile[] = [];
|
|
|
|
const otherTiles: Tile[] = [];
|
2022-05-24 16:54:33 -04:00
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const orderedTiles: Tile[] = new Array(tiles.length);
|
2022-05-24 16:54:33 -04:00
|
|
|
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
|
|
|
|
|
|
|
|
orderedTiles.forEach((tile) =>
|
2022-10-18 00:30:37 -04:00
|
|
|
(tile.focused ? focusedTiles : otherTiles).push(tile)
|
2022-05-24 16:54:33 -04:00
|
|
|
);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-10-18 00:48:29 -04:00
|
|
|
[...focusedTiles, ...otherTiles].forEach((tile, i) => (tile.order = i));
|
2022-05-24 16:54:33 -04:00
|
|
|
}
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
interface DragTileData {
|
|
|
|
offsetX: number;
|
|
|
|
offsetY: number;
|
|
|
|
key: Key;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ChildrenProperties extends ReactDOMAttributes {
|
|
|
|
key: Key;
|
|
|
|
style: {
|
|
|
|
scale: SpringValue<number>;
|
|
|
|
opacity: SpringValue<number>;
|
|
|
|
boxShadow: Interpolation<number, string>;
|
|
|
|
};
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
item: Participant;
|
|
|
|
[index: string]: unknown;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface VideoGridProps {
|
|
|
|
items: Participant[];
|
|
|
|
layout: Layout;
|
|
|
|
disableAnimations?: boolean;
|
|
|
|
children: (props: ChildrenProperties) => React.ReactNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function VideoGrid({
|
|
|
|
items,
|
|
|
|
layout,
|
|
|
|
disableAnimations,
|
|
|
|
children,
|
|
|
|
}: VideoGridProps) {
|
2022-05-17 17:35:35 -04:00
|
|
|
// Place the PiP in the bottom right corner by default
|
|
|
|
const [pipXRatio, setPipXRatio] = useState(1);
|
|
|
|
const [pipYRatio, setPipYRatio] = useState(1);
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const [{ tiles, tilePositions }, setTileState] = useState<{
|
|
|
|
tiles: Tile[];
|
|
|
|
tilePositions: TilePosition[];
|
|
|
|
}>({
|
2022-04-07 14:22:36 -07:00
|
|
|
tiles: [],
|
|
|
|
tilePositions: [],
|
|
|
|
});
|
2022-08-12 19:27:34 +02:00
|
|
|
const [scrollPosition, setScrollPosition] = useState<number>(0);
|
|
|
|
const draggingTileRef = useRef<DragTileData>(null);
|
|
|
|
const lastTappedRef = useRef<{ [index: Key]: number }>({});
|
|
|
|
const lastLayoutRef = useRef<Layout>(layout);
|
2022-04-07 14:22:36 -07:00
|
|
|
const isMounted = useIsMounted();
|
|
|
|
|
|
|
|
const [gridRef, gridBounds] = useMeasure({ polyfill: ResizeObserver });
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setTileState(({ tiles, ...rest }) => {
|
2022-08-12 19:27:34 +02:00
|
|
|
const newTiles: Tile[] = [];
|
|
|
|
const removedTileKeys: Set<Key> = new Set();
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
for (const tile of tiles) {
|
|
|
|
let item = items.find((item) => item.id === tile.key);
|
|
|
|
|
|
|
|
let remove = false;
|
|
|
|
|
|
|
|
if (!item) {
|
|
|
|
remove = true;
|
|
|
|
item = tile.item;
|
2022-05-24 16:37:24 -04:00
|
|
|
removedTileKeys.add(tile.key);
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
let focused: boolean;
|
2022-04-07 14:22:36 -07:00
|
|
|
if (layout === "spotlight") {
|
|
|
|
focused = item.focused;
|
|
|
|
} else {
|
|
|
|
focused = layout === lastLayoutRef.current ? tile.focused : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
newTiles.push({
|
|
|
|
key: item.id,
|
2022-05-24 16:37:24 -04:00
|
|
|
order: tile.order,
|
2022-04-07 14:22:36 -07:00
|
|
|
item,
|
|
|
|
remove,
|
|
|
|
focused,
|
2022-10-18 00:30:37 -04:00
|
|
|
presenter: item.presenter,
|
2022-04-07 14:22:36 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
const existingTileIndex = newTiles.findIndex(
|
|
|
|
({ key }) => item.id === key
|
|
|
|
);
|
|
|
|
|
|
|
|
const existingTile = newTiles[existingTileIndex];
|
|
|
|
|
|
|
|
if (existingTile && !existingTile.remove) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:27:34 +02:00
|
|
|
const newTile: Tile = {
|
2022-04-07 14:22:36 -07:00
|
|
|
key: item.id,
|
2022-05-24 16:37:24 -04:00
|
|
|
order: existingTile?.order ?? newTiles.length,
|
2022-04-07 14:22:36 -07:00
|
|
|
item,
|
|
|
|
remove: false,
|
|
|
|
focused: layout === "spotlight" && item.focused,
|
2022-10-18 00:30:37 -04:00
|
|
|
presenter: item.presenter,
|
2022-04-07 14:22:36 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
if (existingTile) {
|
|
|
|
// Replace an existing tile
|
|
|
|
newTiles.splice(existingTileIndex, 1, newTile);
|
|
|
|
} else {
|
|
|
|
// Added tiles
|
|
|
|
newTiles.push(newTile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-24 16:54:33 -04:00
|
|
|
reorderTiles(newTiles, layout);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
if (removedTileKeys.size > 0) {
|
2022-04-07 14:22:36 -07:00
|
|
|
setTimeout(() => {
|
|
|
|
if (!isMounted.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setTileState(({ tiles, ...rest }) => {
|
2022-08-12 19:27:34 +02:00
|
|
|
const newTiles: Tile[] = tiles
|
2022-05-24 16:37:24 -04:00
|
|
|
.filter((tile) => !removedTileKeys.has(tile.key))
|
|
|
|
.map((tile) => ({ ...tile })); // clone before reordering
|
2022-05-24 16:54:33 -04:00
|
|
|
reorderTiles(newTiles, layout);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
const focusedTileCount = newTiles.reduce(
|
2022-04-07 14:22:36 -07:00
|
|
|
(count, tile) => count + (tile.focused ? 1 : 0),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
...rest,
|
|
|
|
tiles: newTiles,
|
|
|
|
tilePositions: getTilePositions(
|
|
|
|
newTiles.length,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount,
|
2022-10-18 00:48:29 -04:00
|
|
|
newTiles.some((t) => t.presenter),
|
2022-04-07 14:22:36 -07:00
|
|
|
gridBounds.width,
|
|
|
|
gridBounds.height,
|
2022-05-17 17:35:35 -04:00
|
|
|
pipXRatio,
|
|
|
|
pipYRatio,
|
2022-04-07 14:22:36 -07:00
|
|
|
layout
|
|
|
|
),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}, 250);
|
|
|
|
}
|
|
|
|
|
2022-10-18 00:30:37 -04:00
|
|
|
const focusedTileCount = newTiles.reduce(
|
2022-04-07 14:22:36 -07:00
|
|
|
(count, tile) => count + (tile.focused ? 1 : 0),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
lastLayoutRef.current = layout;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...rest,
|
|
|
|
tiles: newTiles,
|
|
|
|
tilePositions: getTilePositions(
|
|
|
|
newTiles.length,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount,
|
2022-10-18 00:48:29 -04:00
|
|
|
newTiles.some((t) => t.presenter),
|
2022-04-07 14:22:36 -07:00
|
|
|
gridBounds.width,
|
|
|
|
gridBounds.height,
|
2022-05-17 17:35:35 -04:00
|
|
|
pipXRatio,
|
|
|
|
pipYRatio,
|
2022-04-07 14:22:36 -07:00
|
|
|
layout
|
|
|
|
),
|
|
|
|
};
|
|
|
|
});
|
2022-05-17 17:35:35 -04:00
|
|
|
}, [items, gridBounds, layout, isMounted, pipXRatio, pipYRatio]);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const animate = useCallback(
|
2022-08-12 19:27:34 +02:00
|
|
|
(tiles: Tile[]) => (tileIndex: number) => {
|
2022-04-07 14:22:36 -07:00
|
|
|
const tile = tiles[tileIndex];
|
2022-05-24 16:37:24 -04:00
|
|
|
const tilePosition = tilePositions[tile.order];
|
2022-04-07 14:22:36 -07:00
|
|
|
const draggingTile = draggingTileRef.current;
|
|
|
|
const dragging = draggingTile && tile.key === draggingTile.key;
|
|
|
|
const remove = tile.remove;
|
|
|
|
|
|
|
|
if (dragging) {
|
|
|
|
return {
|
|
|
|
width: tilePosition.width,
|
|
|
|
height: tilePosition.height,
|
|
|
|
x: draggingTile.offsetX + draggingTile.x,
|
|
|
|
y: draggingTile.offsetY + draggingTile.y,
|
|
|
|
scale: 1.1,
|
|
|
|
opacity: 1,
|
|
|
|
zIndex: 2,
|
|
|
|
shadow: 15,
|
2022-08-12 19:27:34 +02:00
|
|
|
immediate: (key: string) =>
|
2022-04-07 14:22:36 -07:00
|
|
|
disableAnimations ||
|
|
|
|
key === "zIndex" ||
|
|
|
|
key === "x" ||
|
|
|
|
key === "y" ||
|
|
|
|
key === "shadow",
|
|
|
|
from: {
|
|
|
|
shadow: 0,
|
|
|
|
scale: 0,
|
|
|
|
opacity: 0,
|
|
|
|
},
|
|
|
|
reset: false,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const isMobile = isMobileBreakpoint(
|
|
|
|
gridBounds.width,
|
|
|
|
gridBounds.height
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
x:
|
|
|
|
tilePosition.x +
|
2022-09-22 12:03:57 +01:00
|
|
|
(layout === "spotlight" && tile.order !== 0 && isMobile
|
2022-04-07 14:22:36 -07:00
|
|
|
? scrollPosition
|
|
|
|
: 0),
|
|
|
|
y:
|
|
|
|
tilePosition.y +
|
2022-09-22 12:03:57 +01:00
|
|
|
(layout === "spotlight" && tile.order !== 0 && !isMobile
|
2022-04-07 14:22:36 -07:00
|
|
|
? scrollPosition
|
|
|
|
: 0),
|
|
|
|
width: tilePosition.width,
|
|
|
|
height: tilePosition.height,
|
|
|
|
scale: remove ? 0 : 1,
|
|
|
|
opacity: remove ? 0 : 1,
|
|
|
|
zIndex: tilePosition.zIndex,
|
|
|
|
shadow: 1,
|
|
|
|
from: {
|
|
|
|
shadow: 1,
|
|
|
|
scale: 0,
|
|
|
|
opacity: 0,
|
|
|
|
},
|
|
|
|
reset: false,
|
2022-08-12 19:27:34 +02:00
|
|
|
immediate: (key: string) =>
|
2022-04-07 14:22:36 -07:00
|
|
|
disableAnimations || key === "zIndex" || key === "shadow",
|
2022-05-24 16:37:24 -04:00
|
|
|
// If we just stopped dragging a tile, give it time for its animation
|
|
|
|
// to settle before pushing its z-index back down
|
2022-08-12 19:27:34 +02:00
|
|
|
delay: (key: string) => (key === "zIndex" ? 500 : 0),
|
2022-04-07 14:22:36 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[tilePositions, disableAnimations, scrollPosition, layout, gridBounds]
|
|
|
|
);
|
|
|
|
|
|
|
|
const [springs, api] = useSprings(tiles.length, animate(tiles), [
|
|
|
|
tilePositions,
|
|
|
|
tiles,
|
|
|
|
scrollPosition,
|
|
|
|
]);
|
|
|
|
|
|
|
|
const onTap = useCallback(
|
2022-08-12 19:27:34 +02:00
|
|
|
(tileKey: Key) => {
|
2022-04-07 14:22:36 -07:00
|
|
|
const lastTapped = lastTappedRef.current[tileKey];
|
|
|
|
|
|
|
|
if (!lastTapped || Date.now() - lastTapped > 500) {
|
|
|
|
lastTappedRef.current[tileKey] = Date.now();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
lastTappedRef.current[tileKey] = 0;
|
|
|
|
|
|
|
|
const tile = tiles.find((tile) => tile.key === tileKey);
|
2022-05-24 16:37:24 -04:00
|
|
|
if (!tile || layout !== "freedom") return;
|
2022-04-07 14:22:36 -07:00
|
|
|
const item = tile.item;
|
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
setTileState(({ tiles, ...state }) => {
|
2022-10-18 00:30:37 -04:00
|
|
|
let focusedTileCount = 0;
|
2022-05-24 16:37:24 -04:00
|
|
|
const newTiles = tiles.map((tile) => {
|
2022-08-12 19:27:34 +02:00
|
|
|
const newTile = { ...tile }; // clone before reordering
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
if (tile.item === item) {
|
|
|
|
newTile.focused = !tile.focused;
|
|
|
|
}
|
|
|
|
if (newTile.focused) {
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount++;
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
return newTile;
|
|
|
|
});
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-05-24 16:54:33 -04:00
|
|
|
reorderTiles(newTiles, layout);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
tiles: newTiles,
|
|
|
|
tilePositions: getTilePositions(
|
|
|
|
newTiles.length,
|
2022-10-18 00:30:37 -04:00
|
|
|
focusedTileCount,
|
2022-10-18 00:48:29 -04:00
|
|
|
newTiles.some((t) => t.presenter),
|
2022-04-07 14:22:36 -07:00
|
|
|
gridBounds.width,
|
|
|
|
gridBounds.height,
|
2022-05-17 17:35:35 -04:00
|
|
|
pipXRatio,
|
|
|
|
pipYRatio,
|
2022-04-07 14:22:36 -07:00
|
|
|
layout
|
|
|
|
),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
2022-08-12 19:27:34 +02:00
|
|
|
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
|
2022-04-07 14:22:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const bindTile = useDrag(
|
2022-05-17 17:35:35 -04:00
|
|
|
({ args: [key], active, xy, movement, tap, last, event }) => {
|
2022-04-07 14:22:36 -07:00
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
if (tap) {
|
|
|
|
onTap(key);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-17 17:35:35 -04:00
|
|
|
if (layout !== "freedom") return;
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
|
|
|
|
const dragTile = tiles[dragTileIndex];
|
2022-05-24 16:37:24 -04:00
|
|
|
const dragTilePosition = tilePositions[dragTile.order];
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
let newTiles = tiles;
|
|
|
|
|
2022-10-24 10:22:51 -04:00
|
|
|
if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
|
2022-05-17 17:35:35 -04:00
|
|
|
// We're in 1:1 mode, so only the local tile should be draggable
|
2022-05-24 16:37:24 -04:00
|
|
|
if (!dragTile.item.isLocal) return;
|
2022-05-17 17:35:35 -04:00
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
// Position should only update on the very last event, to avoid
|
|
|
|
// compounding the offset on every drag event
|
2022-05-17 17:35:35 -04:00
|
|
|
if (last) {
|
2022-05-23 09:59:55 -04:00
|
|
|
const remotePosition = tilePositions[1];
|
|
|
|
|
2022-05-17 17:35:35 -04:00
|
|
|
const pipGap = getPipGap(gridBounds.width / gridBounds.height);
|
2022-05-23 09:59:55 -04:00
|
|
|
const pipMinX = remotePosition.x + pipGap;
|
|
|
|
const pipMinY = remotePosition.y + pipGap;
|
2022-05-17 17:35:35 -04:00
|
|
|
const pipMaxX =
|
2022-05-23 09:59:55 -04:00
|
|
|
remotePosition.x +
|
|
|
|
remotePosition.width -
|
|
|
|
dragTilePosition.width -
|
|
|
|
pipGap;
|
2022-05-17 17:35:35 -04:00
|
|
|
const pipMaxY =
|
2022-05-23 09:59:55 -04:00
|
|
|
remotePosition.y +
|
|
|
|
remotePosition.height -
|
|
|
|
dragTilePosition.height -
|
|
|
|
pipGap;
|
2022-05-17 17:35:35 -04:00
|
|
|
|
|
|
|
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)));
|
|
|
|
}
|
2022-05-24 16:37:24 -04:00
|
|
|
} else {
|
|
|
|
const hoverTile = tiles.find(
|
|
|
|
(tile) =>
|
|
|
|
tile.key !== key &&
|
|
|
|
isInside(cursorPosition, tilePositions[tile.order])
|
|
|
|
);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
if (hoverTile) {
|
|
|
|
// Shift the tiles into their new order
|
2022-04-07 14:22:36 -07:00
|
|
|
newTiles = newTiles.map((tile) => {
|
2022-05-24 16:37:24 -04:00
|
|
|
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;
|
2022-04-07 14:22:36 -07:00
|
|
|
if (tile === hoverTile) {
|
2022-05-24 16:37:24 -04:00
|
|
|
focused = dragTile.focused;
|
2022-04-07 14:22:36 -07:00
|
|
|
} else if (tile === dragTile) {
|
2022-05-24 16:37:24 -04:00
|
|
|
focused = hoverTile.focused;
|
2022-04-07 14:22:36 -07:00
|
|
|
} else {
|
2022-05-24 16:37:24 -04:00
|
|
|
focused = tile.focused;
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
2022-05-24 16:37:24 -04:00
|
|
|
|
|
|
|
return { ...tile, order, focused };
|
2022-04-07 14:22:36 -07:00
|
|
|
});
|
|
|
|
|
2022-05-24 16:54:33 -04:00
|
|
|
reorderTiles(newTiles, layout);
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
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.x = movement[0];
|
|
|
|
draggingTileRef.current.y = movement[1];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
draggingTileRef.current = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
api.start(animate(newTiles));
|
|
|
|
},
|
|
|
|
{ filterTaps: true, pointer: { buttons: [1] } }
|
|
|
|
);
|
|
|
|
|
|
|
|
const onGridGesture = useCallback(
|
2022-08-12 19:27:34 +02:00
|
|
|
(
|
|
|
|
e:
|
|
|
|
| Omit<FullGestureState<"wheel">, "event">
|
|
|
|
| Omit<FullGestureState<"drag">, "event">,
|
|
|
|
isWheel: boolean
|
|
|
|
) => {
|
2022-04-07 14:22:36 -07:00
|
|
|
if (layout !== "spotlight") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isMobile = isMobileBreakpoint(gridBounds.width, gridBounds.height);
|
2022-08-12 19:27:34 +02:00
|
|
|
|
2022-04-07 14:22:36 -07:00
|
|
|
let movement = e.delta[isMobile ? 0 : 1];
|
|
|
|
|
|
|
|
if (isWheel) {
|
|
|
|
movement = -movement;
|
|
|
|
}
|
|
|
|
|
|
|
|
let min = 0;
|
|
|
|
|
|
|
|
if (tilePositions.length > 1) {
|
|
|
|
const lastTile = tilePositions[tilePositions.length - 1];
|
|
|
|
min = isMobile
|
2022-05-17 17:35:35 -04:00
|
|
|
? gridBounds.width - lastTile.x - lastTile.width - GAP
|
|
|
|
: gridBounds.height - lastTile.y - lastTile.height - GAP;
|
2022-04-07 14:22:36 -07:00
|
|
|
}
|
|
|
|
|
2022-05-24 16:37:24 -04:00
|
|
|
setScrollPosition((scrollPosition) =>
|
|
|
|
Math.min(Math.max(movement + scrollPosition, min), 0)
|
|
|
|
);
|
2022-04-07 14:22:36 -07:00
|
|
|
},
|
|
|
|
[layout, gridBounds, tilePositions]
|
|
|
|
);
|
|
|
|
|
|
|
|
const bindGrid = useGesture(
|
|
|
|
{
|
|
|
|
onWheel: (e) => onGridGesture(e, true),
|
|
|
|
onDrag: (e) => onGridGesture(e, false),
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
|
|
|
|
{springs.map(({ shadow, ...style }, i) => {
|
|
|
|
const tile = tiles[i];
|
2022-05-24 16:37:24 -04:00
|
|
|
const tilePosition = tilePositions[tile.order];
|
2022-04-07 14:22:36 -07:00
|
|
|
|
|
|
|
return children({
|
|
|
|
...bindTile(tile.key),
|
|
|
|
key: tile.key,
|
|
|
|
style: {
|
|
|
|
boxShadow: shadow.to(
|
|
|
|
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
|
|
|
|
),
|
|
|
|
...style,
|
|
|
|
},
|
|
|
|
width: tilePosition.width,
|
|
|
|
height: tilePosition.height,
|
|
|
|
item: tile.item,
|
|
|
|
});
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
VideoGrid.defaultProps = {
|
|
|
|
layout: "freedom",
|
|
|
|
};
|