diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 8c37d465..ef565731 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -17,6 +17,7 @@ limitations under the License. .inRoom { position: relative; display: flex; + gap: 8px; flex-direction: column; overflow: hidden; min-height: 100%; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 22145012..88330dcc 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -64,6 +64,7 @@ import { ParticipantInfo } from "./useGroupCall"; import { TileDescriptor } from "../video-grid/TileDescriptor"; import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; +import { NewVideoGrid } from "../video-grid/NewVideoGrid"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -305,7 +306,7 @@ export function InCallView({ } return ( - )} - + ); }; diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css new file mode 100644 index 00000000..b035e655 --- /dev/null +++ b/src/video-grid/NewVideoGrid.module.css @@ -0,0 +1,18 @@ +.grid { + position: relative; + flex-grow: 1; + padding: 0 22px; + overflow-y: scroll; +} + +.slotGrid { + position: relative; + display: grid; + grid-auto-rows: 183px; + column-gap: 18px; + row-gap: 21px; +} + +.slot { + background-color: red; +} diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx new file mode 100644 index 00000000..6bc78e15 --- /dev/null +++ b/src/video-grid/NewVideoGrid.tsx @@ -0,0 +1,132 @@ +import { useTransition } from "@react-spring/web"; +import React, { FC, memo, ReactNode, useMemo, useRef } from "react"; +import useMeasure from "react-use-measure"; +import styles from "./NewVideoGrid.module.css"; +import { TileDescriptor } from "./TileDescriptor"; +import { VideoGridProps as Props } from "./VideoGrid"; + +interface Cell { + /** + * The item held by the slot containing this cell. + */ + item: TileDescriptor + /** + * Whether this cell is the first cell of the containing slot. + */ + slot: boolean + /** + * The width, in columns, of the containing slot. + */ + columns: number + /** + * The height, in rows, of the containing slot. + */ + rows: number +} + +interface Rect { + x: number + y: number + width: number + height: number +} + +interface Tile extends Rect { + item: TileDescriptor + dragging: boolean +} + +interface SlotsProps { + count: number +} + +/** + * Generates a number of empty slot divs. + */ +const Slots: FC = memo(({ count }) => { + const slots = new Array(count) + for (let i = 0; i < count; i++) slots[i] =
+ return <>{slots} +}) + +export const NewVideoGrid: FC = ({ items, children }) => { + const slotGridRef = useRef(null); + const [gridRef, gridBounds] = useMeasure(); + + const slotRects = useMemo(() => { + if (slotGridRef.current === null) return []; + + const slots = slotGridRef.current.getElementsByClassName(styles.slot) + const rects = new Array(slots.length) + for (let i = 0; i < slots.length; i++) { + const slot = slots[i] as HTMLElement + rects[i] = { + x: slot.offsetLeft, + y: slot.offsetTop, + width: slot.offsetWidth, + height: slot.offsetHeight, + } + } + + return rects; + }, [items, gridBounds]); + + const cells: Cell[] = useMemo(() => items.map(item => ({ + item, + slot: true, + columns: 1, + rows: 1, + })), [items]) + + const slotCells = useMemo(() => cells.filter(cell => cell.slot), [cells]) + + const tiles: Tile[] = useMemo(() => slotRects.map((slot, i) => { + const cell = slotCells[i] + return { + item: cell.item, + x: slot.x, + y: slot.y, + width: slot.width, + height: slot.height, + dragging: false, + } + }), [slotRects, cells]) + + const [tileTransitions] = useTransition(tiles, () => ({ + key: ({ item }: Tile) => item.id, + from: { opacity: 0 }, + enter: ({ x, y, width, height }: Tile) => ({ opacity: 1, x, y, width, height }), + update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), + leave: { opacity: 0 }, + }), [tiles]) + + const slotGridStyle = useMemo(() => { + const columnCount = gridBounds.width >= 800 ? 6 : 3; + return { + gridTemplateColumns: `repeat(${columnCount}, 1fr)`, + }; + }, [gridBounds]); + + // Render nothing if the bounds are not yet known + if (gridBounds.width === 0) { + return
+ } + + return ( +
+
+ +
+ {tileTransitions((style, tile) => children({ + key: tile.item.id, + style: style as any, + width: tile.width, + height: tile.height, + item: tile.item, + }))} +
+ ); +}; diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 70633e9c..fc6e44d8 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -705,7 +705,7 @@ interface ChildrenProperties extends ReactDOMAttributes { [index: string]: unknown; } -interface VideoGridProps { +export interface VideoGridProps { items: TileDescriptor[]; layout: Layout; disableAnimations?: boolean; diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index d6f6e066..c13976d1 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -16,6 +16,7 @@ limitations under the License. .videoTile { position: absolute; + top: 0; will-change: transform, width, height, opacity, box-shadow; border-radius: 20px; overflow: hidden;