Improve typing around layouts and grid components
This commit is contained in:
parent
cc35f243f2
commit
1c6ef97457
7 changed files with 207 additions and 176 deletions
|
@ -73,7 +73,7 @@ import { useJoinRule } from "./useJoinRule";
|
||||||
import { ParticipantInfo } from "./useGroupCall";
|
import { ParticipantInfo } from "./useGroupCall";
|
||||||
import { ItemData, TileContent } from "../video-grid/VideoTile";
|
import { ItemData, TileContent } from "../video-grid/VideoTile";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { NewVideoGrid, useLayoutStates } from "../video-grid/NewVideoGrid";
|
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
||||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||||
import { SettingsModal } from "../settings/SettingsModal";
|
import { SettingsModal } from "../settings/SettingsModal";
|
||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
|
@ -83,6 +83,7 @@ import { VideoTile } from "../video-grid/VideoTile";
|
||||||
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
||||||
import { useMediaDevices } from "../livekit/useMediaDevices";
|
import { useMediaDevices } from "../livekit/useMediaDevices";
|
||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
|
|
|
@ -865,7 +865,8 @@ export const BigGrid: Layout<BigGridState> = {
|
||||||
emptyState: { columns: 4, cells: [] },
|
emptyState: { columns: 4, cells: [] },
|
||||||
updateTiles,
|
updateTiles,
|
||||||
updateBounds,
|
updateBounds,
|
||||||
getTiles: (g) => g.cells.filter((c) => c?.origin).map((c) => c!.item),
|
getTiles: <T,>(g) =>
|
||||||
|
g.cells.filter((c) => c?.origin).map((c) => c!.item as T),
|
||||||
canDragTile: () => true,
|
canDragTile: () => true,
|
||||||
dragTile,
|
dragTile,
|
||||||
toggleFocus: cycleTileSize,
|
toggleFocus: cycleTileSize,
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 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.
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ComponentType } from "react";
|
|
||||||
import type { RectReadOnly } from "react-use-measure";
|
|
||||||
import type { TileDescriptor } from "./VideoGrid";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A video grid layout system with concrete states of type State.
|
|
||||||
*/
|
|
||||||
export interface Layout<State> {
|
|
||||||
/**
|
|
||||||
* The layout state for zero tiles.
|
|
||||||
*/
|
|
||||||
readonly emptyState: State;
|
|
||||||
/**
|
|
||||||
* Updates/adds/removes tiles in a way that looks natural in the context of
|
|
||||||
* the given initial state.
|
|
||||||
*/
|
|
||||||
readonly updateTiles: (s: State, tiles: TileDescriptor<unknown>[]) => State;
|
|
||||||
/**
|
|
||||||
* Adapts the layout to a new container size.
|
|
||||||
*/
|
|
||||||
readonly updateBounds: (s: State, bounds: RectReadOnly) => State;
|
|
||||||
/**
|
|
||||||
* Gets tiles in the order created by the layout.
|
|
||||||
*/
|
|
||||||
readonly getTiles: (s: State) => TileDescriptor<unknown>[];
|
|
||||||
/**
|
|
||||||
* Determines whether a tile is draggable.
|
|
||||||
*/
|
|
||||||
readonly canDragTile: (s: State, tile: TileDescriptor<unknown>) => boolean;
|
|
||||||
/**
|
|
||||||
* Drags the tile 'from' to the location of the tile 'to' (if possible).
|
|
||||||
* The position parameters are numbers in the range [0, 1) describing the
|
|
||||||
* specific positions on 'from' and 'to' that the drag gesture is targeting.
|
|
||||||
*/
|
|
||||||
readonly dragTile: (
|
|
||||||
s: State,
|
|
||||||
from: TileDescriptor<unknown>,
|
|
||||||
to: TileDescriptor<unknown>,
|
|
||||||
xPositionOnFrom: number,
|
|
||||||
yPositionOnFrom: number,
|
|
||||||
xPositionOnTo: number,
|
|
||||||
yPositionOnTo: number
|
|
||||||
) => State;
|
|
||||||
/**
|
|
||||||
* Toggles the focus of the given tile (if this layout has the concept of
|
|
||||||
* focus).
|
|
||||||
*/
|
|
||||||
readonly toggleFocus?: (s: State, tile: TileDescriptor<unknown>) => State;
|
|
||||||
/**
|
|
||||||
* A React component generating the slot elements for a given layout state.
|
|
||||||
*/
|
|
||||||
readonly Slots: ComponentType<{ s: State }>;
|
|
||||||
/**
|
|
||||||
* Whether the state of this layout should be remembered even while a
|
|
||||||
* different layout is active.
|
|
||||||
*/
|
|
||||||
readonly rememberState: boolean;
|
|
||||||
}
|
|
178
src/video-grid/Layout.tsx
Normal file
178
src/video-grid/Layout.tsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ComponentType, useCallback, useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
import type { RectReadOnly } from "react-use-measure";
|
||||||
|
import { useReactiveState } from "../useReactiveState";
|
||||||
|
import type { TileDescriptor } from "./VideoGrid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A video grid layout system with concrete states of type State.
|
||||||
|
*/
|
||||||
|
// Ideally State would be parameterized by the tile data type, but then that
|
||||||
|
// makes Layout a higher-kinded type, which isn't achievable in TypeScript
|
||||||
|
// (unless you invoke some dark type-level computation magic… 😏)
|
||||||
|
// So we're stuck with these types being a little too strong.
|
||||||
|
export interface Layout<State> {
|
||||||
|
/**
|
||||||
|
* The layout state for zero tiles.
|
||||||
|
*/
|
||||||
|
readonly emptyState: State;
|
||||||
|
/**
|
||||||
|
* Updates/adds/removes tiles in a way that looks natural in the context of
|
||||||
|
* the given initial state.
|
||||||
|
*/
|
||||||
|
readonly updateTiles: <T>(s: State, tiles: TileDescriptor<T>[]) => State;
|
||||||
|
/**
|
||||||
|
* Adapts the layout to a new container size.
|
||||||
|
*/
|
||||||
|
readonly updateBounds: (s: State, bounds: RectReadOnly) => State;
|
||||||
|
/**
|
||||||
|
* Gets tiles in the order created by the layout.
|
||||||
|
*/
|
||||||
|
readonly getTiles: <T>(s: State) => TileDescriptor<T>[];
|
||||||
|
/**
|
||||||
|
* Determines whether a tile is draggable.
|
||||||
|
*/
|
||||||
|
readonly canDragTile: <T>(s: State, tile: TileDescriptor<T>) => boolean;
|
||||||
|
/**
|
||||||
|
* Drags the tile 'from' to the location of the tile 'to' (if possible).
|
||||||
|
* The position parameters are numbers in the range [0, 1) describing the
|
||||||
|
* specific positions on 'from' and 'to' that the drag gesture is targeting.
|
||||||
|
*/
|
||||||
|
readonly dragTile: <T>(
|
||||||
|
s: State,
|
||||||
|
from: TileDescriptor<T>,
|
||||||
|
to: TileDescriptor<T>,
|
||||||
|
xPositionOnFrom: number,
|
||||||
|
yPositionOnFrom: number,
|
||||||
|
xPositionOnTo: number,
|
||||||
|
yPositionOnTo: number
|
||||||
|
) => State;
|
||||||
|
/**
|
||||||
|
* Toggles the focus of the given tile (if this layout has the concept of
|
||||||
|
* focus).
|
||||||
|
*/
|
||||||
|
readonly toggleFocus?: <T>(s: State, tile: TileDescriptor<T>) => State;
|
||||||
|
/**
|
||||||
|
* A React component generating the slot elements for a given layout state.
|
||||||
|
*/
|
||||||
|
readonly Slots: ComponentType<{ s: State }>;
|
||||||
|
/**
|
||||||
|
* Whether the state of this layout should be remembered even while a
|
||||||
|
* different layout is active.
|
||||||
|
*/
|
||||||
|
readonly rememberState: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A version of Map with stronger types that allow us to save layout states in a
|
||||||
|
* type-safe way.
|
||||||
|
*/
|
||||||
|
export interface LayoutStatesMap {
|
||||||
|
get<State>(layout: Layout<State>): State | undefined;
|
||||||
|
set<State>(layout: Layout<State>, state: State): LayoutStatesMap;
|
||||||
|
delete<State>(layout: Layout<State>): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook creating a Map to store layout states in.
|
||||||
|
*/
|
||||||
|
export const useLayoutStates = (): LayoutStatesMap => {
|
||||||
|
const layoutStates = useRef<Map<unknown, unknown>>();
|
||||||
|
if (layoutStates.current === undefined) layoutStates.current = new Map();
|
||||||
|
return layoutStates.current as LayoutStatesMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook which uses the provided layout system to arrange a set of items into a
|
||||||
|
* concrete layout state, and provides callbacks for user interaction.
|
||||||
|
*/
|
||||||
|
export const useLayout = <State, T>(
|
||||||
|
layout: Layout<State>,
|
||||||
|
items: TileDescriptor<T>[],
|
||||||
|
bounds: RectReadOnly,
|
||||||
|
layoutStates: LayoutStatesMap
|
||||||
|
) => {
|
||||||
|
const prevLayout = useRef<Layout<unknown>>();
|
||||||
|
const prevState = layoutStates.get(layout);
|
||||||
|
|
||||||
|
const [state, setState] = useReactiveState<State>(() => {
|
||||||
|
// If the bounds aren't known yet, don't add anything to the layout
|
||||||
|
if (bounds.width === 0) {
|
||||||
|
return layout.emptyState;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
prevLayout.current !== undefined &&
|
||||||
|
layout !== prevLayout.current &&
|
||||||
|
!prevLayout.current.rememberState
|
||||||
|
)
|
||||||
|
layoutStates.delete(prevLayout.current);
|
||||||
|
|
||||||
|
const baseState = layoutStates.get(layout) ?? layout.emptyState;
|
||||||
|
return layout.updateTiles(layout.updateBounds(baseState, bounds), items);
|
||||||
|
}
|
||||||
|
}, [layout, items, bounds]);
|
||||||
|
|
||||||
|
const generation = useRef<number>(0);
|
||||||
|
if (state !== prevState) generation.current++;
|
||||||
|
|
||||||
|
prevLayout.current = layout as Layout<unknown>;
|
||||||
|
// No point in remembering an empty state, plus it would end up clobbering the
|
||||||
|
// real saved state while restoring a layout
|
||||||
|
if (state !== layout.emptyState) layoutStates.set(layout, state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
orderedItems: useMemo(() => layout.getTiles<T>(state), [layout, state]),
|
||||||
|
generation: generation.current,
|
||||||
|
canDragTile: useCallback(
|
||||||
|
(tile: TileDescriptor<T>) => layout.canDragTile(state, tile),
|
||||||
|
[layout, state]
|
||||||
|
),
|
||||||
|
dragTile: useCallback(
|
||||||
|
(
|
||||||
|
from: TileDescriptor<T>,
|
||||||
|
to: TileDescriptor<T>,
|
||||||
|
xPositionOnFrom: number,
|
||||||
|
yPositionOnFrom: number,
|
||||||
|
xPositionOnTo: number,
|
||||||
|
yPositionOnTo: number
|
||||||
|
) =>
|
||||||
|
setState((s) =>
|
||||||
|
layout.dragTile(
|
||||||
|
s,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
xPositionOnFrom,
|
||||||
|
yPositionOnFrom,
|
||||||
|
xPositionOnTo,
|
||||||
|
yPositionOnTo
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[layout, setState]
|
||||||
|
),
|
||||||
|
toggleFocus: useMemo(
|
||||||
|
() =>
|
||||||
|
layout.toggleFocus &&
|
||||||
|
((tile: TileDescriptor<T>) =>
|
||||||
|
setState((s) => layout.toggleFocus!(s, tile))),
|
||||||
|
[layout, setState]
|
||||||
|
),
|
||||||
|
slots: <layout.Slots s={state} />,
|
||||||
|
};
|
||||||
|
};
|
|
@ -20,13 +20,12 @@ import React, {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
FC,
|
FC,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import useMeasure, { RectReadOnly } from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { zip } from "lodash";
|
import { zip } from "lodash";
|
||||||
|
|
||||||
import styles from "./NewVideoGrid.module.css";
|
import styles from "./NewVideoGrid.module.css";
|
||||||
|
@ -40,84 +39,7 @@ import { useReactiveState } from "../useReactiveState";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { TileWrapper } from "./TileWrapper";
|
import { TileWrapper } from "./TileWrapper";
|
||||||
import { BigGrid } from "./BigGrid";
|
import { BigGrid } from "./BigGrid";
|
||||||
import { Layout } from "./Layout";
|
import { useLayout } from "./Layout";
|
||||||
|
|
||||||
export const useLayoutStates = () => {
|
|
||||||
const layoutStates = useRef<Map<Layout<unknown>, unknown>>();
|
|
||||||
if (layoutStates.current === undefined) layoutStates.current = new Map();
|
|
||||||
return layoutStates.current;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGrid = (
|
|
||||||
layout: Layout<unknown>,
|
|
||||||
items: TileDescriptor<unknown>[],
|
|
||||||
bounds: RectReadOnly,
|
|
||||||
layoutStates: Map<Layout<unknown>, unknown>
|
|
||||||
) => {
|
|
||||||
const prevLayout = useRef<Layout<unknown>>(layout);
|
|
||||||
const prevState = layoutStates.get(layout);
|
|
||||||
|
|
||||||
const [state, setState] = useReactiveState<unknown>(() => {
|
|
||||||
// If the bounds aren't known yet, don't add anything to the layout
|
|
||||||
if (bounds.width === 0) {
|
|
||||||
return layout.emptyState;
|
|
||||||
} else {
|
|
||||||
if (layout !== prevLayout.current && !prevLayout.current.rememberState)
|
|
||||||
layoutStates.delete(prevLayout.current);
|
|
||||||
|
|
||||||
const baseState = layoutStates.get(layout) ?? layout.emptyState;
|
|
||||||
return layout.updateTiles(layout.updateBounds(baseState, bounds), items);
|
|
||||||
}
|
|
||||||
}, [layout, items, bounds]);
|
|
||||||
|
|
||||||
const generation = useRef<number>(0);
|
|
||||||
if (state !== prevState) generation.current++;
|
|
||||||
|
|
||||||
prevLayout.current = layout;
|
|
||||||
// No point in remembering an empty state, plus it would end up clobbering the
|
|
||||||
// real saved state while restoring a layout
|
|
||||||
if (state !== layout.emptyState) layoutStates.set(layout, state);
|
|
||||||
|
|
||||||
return {
|
|
||||||
grid: state,
|
|
||||||
orderedItems: useMemo(() => layout.getTiles(state), [layout, state]),
|
|
||||||
generation: generation.current,
|
|
||||||
canDragTile: useCallback(
|
|
||||||
(tile: TileDescriptor<unknown>) => layout.canDragTile(state, tile),
|
|
||||||
[layout, state]
|
|
||||||
),
|
|
||||||
dragTile: useCallback(
|
|
||||||
(
|
|
||||||
from: TileDescriptor<unknown>,
|
|
||||||
to: TileDescriptor<unknown>,
|
|
||||||
xPositionOnFrom: number,
|
|
||||||
yPositionOnFrom: number,
|
|
||||||
xPositionOnTo: number,
|
|
||||||
yPositionOnTo: number
|
|
||||||
) =>
|
|
||||||
setState((s) =>
|
|
||||||
layout.dragTile(
|
|
||||||
s,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
xPositionOnFrom,
|
|
||||||
yPositionOnFrom,
|
|
||||||
xPositionOnTo,
|
|
||||||
yPositionOnTo
|
|
||||||
)
|
|
||||||
),
|
|
||||||
[layout, setState]
|
|
||||||
),
|
|
||||||
toggleFocus: useMemo(
|
|
||||||
() =>
|
|
||||||
layout.toggleFocus &&
|
|
||||||
((tile: TileDescriptor<unknown>) =>
|
|
||||||
setState((s) => layout.toggleFocus!(s, tile))),
|
|
||||||
[layout, setState]
|
|
||||||
),
|
|
||||||
slots: <layout.Slots s={state} />,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Rect {
|
interface Rect {
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -126,8 +48,8 @@ interface Rect {
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Tile extends Rect {
|
interface Tile<T> extends Rect {
|
||||||
item: TileDescriptor<unknown>;
|
item: TileDescriptor<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
|
@ -215,23 +137,23 @@ export function NewVideoGrid<T>({
|
||||||
// TODO: Implement more layouts and select the right one here
|
// TODO: Implement more layouts and select the right one here
|
||||||
const layout = BigGrid;
|
const layout = BigGrid;
|
||||||
const {
|
const {
|
||||||
grid,
|
state: grid,
|
||||||
orderedItems,
|
orderedItems,
|
||||||
generation,
|
generation,
|
||||||
canDragTile,
|
canDragTile,
|
||||||
dragTile,
|
dragTile,
|
||||||
toggleFocus,
|
toggleFocus,
|
||||||
slots,
|
slots,
|
||||||
} = useGrid(layout as Layout<unknown>, items, gridBounds, layoutStates);
|
} = useLayout(layout, items, gridBounds, layoutStates);
|
||||||
|
|
||||||
const [tiles] = useReactiveState<Tile[]>(
|
const [tiles] = useReactiveState<Tile<T>[]>(
|
||||||
(prevTiles) => {
|
(prevTiles) => {
|
||||||
// If React hasn't yet rendered the current generation of the grid, skip
|
// If React hasn't yet rendered the current generation of the grid, skip
|
||||||
// the update, because grid and slotRects will be out of sync
|
// the update, because grid and slotRects will be out of sync
|
||||||
if (renderedGeneration !== generation) return prevTiles ?? [];
|
if (renderedGeneration !== generation) return prevTiles ?? [];
|
||||||
|
|
||||||
const tileRects = new Map<TileDescriptor<unknown>, Rect>(
|
const tileRects = new Map(
|
||||||
zip(orderedItems, slotRects) as [TileDescriptor<unknown>, Rect][]
|
zip(orderedItems, slotRects) as [TileDescriptor<T>, Rect][]
|
||||||
);
|
);
|
||||||
// In order to not break drag gestures, it's critical that we render tiles
|
// In order to not break drag gestures, it's critical that we render tiles
|
||||||
// in a stable order (that of 'items')
|
// in a stable order (that of 'items')
|
||||||
|
@ -247,8 +169,8 @@ export function NewVideoGrid<T>({
|
||||||
const [tileTransitions, springRef] = useTransition(
|
const [tileTransitions, springRef] = useTransition(
|
||||||
tiles,
|
tiles,
|
||||||
() => ({
|
() => ({
|
||||||
key: ({ item }: Tile) => item.id,
|
key: ({ item }: Tile<T>) => item.id,
|
||||||
from: ({ x, y, width, height }: Tile) => ({
|
from: ({ x, y, width, height }: Tile<T>) => ({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0,
|
scale: 0,
|
||||||
shadow: 1,
|
shadow: 1,
|
||||||
|
@ -261,7 +183,7 @@ export function NewVideoGrid<T>({
|
||||||
immediate: disableAnimations,
|
immediate: disableAnimations,
|
||||||
}),
|
}),
|
||||||
enter: { opacity: 1, scale: 1, immediate: disableAnimations },
|
enter: { opacity: 1, scale: 1, immediate: disableAnimations },
|
||||||
update: ({ item, x, y, width, height }: Tile) =>
|
update: ({ item, x, y, width, height }: Tile<T>) =>
|
||||||
item.id === dragState.current?.tileId
|
item.id === dragState.current?.tileId
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
|
@ -275,7 +197,7 @@ export function NewVideoGrid<T>({
|
||||||
config: { mass: 0.7, tension: 252, friction: 25 },
|
config: { mass: 0.7, tension: 252, friction: 25 },
|
||||||
})
|
})
|
||||||
// react-spring's types are bugged and can't infer the spring type
|
// react-spring's types are bugged and can't infer the spring type
|
||||||
) as unknown as [TransitionFn<Tile, TileSpring>, SpringRef<TileSpring>];
|
) as unknown as [TransitionFn<Tile<T>, TileSpring>, SpringRef<TileSpring>];
|
||||||
|
|
||||||
// Because we're using react-spring in imperative mode, we're responsible for
|
// Because we're using react-spring in imperative mode, we're responsible for
|
||||||
// firing animations manually whenever the tiles array updates
|
// firing animations manually whenever the tiles array updates
|
||||||
|
@ -288,7 +210,7 @@ export function NewVideoGrid<T>({
|
||||||
const tile = tiles.find((t) => t.item.id === tileId)!;
|
const tile = tiles.find((t) => t.item.id === tileId)!;
|
||||||
|
|
||||||
springRef.current
|
springRef.current
|
||||||
.find((c) => (c.item as Tile).item.id === tileId)
|
.find((c) => (c.item as Tile<T>).item.id === tileId)
|
||||||
?.start(
|
?.start(
|
||||||
endOfGesture
|
endOfGesture
|
||||||
? {
|
? {
|
||||||
|
@ -353,10 +275,10 @@ export function NewVideoGrid<T>({
|
||||||
toggleFocus?.(items.find((i) => i.id === tileId)!);
|
toggleFocus?.(items.find((i) => i.id === tileId)!);
|
||||||
} else {
|
} else {
|
||||||
const tileController = springRef.current.find(
|
const tileController = springRef.current.find(
|
||||||
(c) => (c.item as Tile).item.id === tileId
|
(c) => (c.item as Tile<T>).item.id === tileId
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
if (canDragTile((tileController.item as Tile).item)) {
|
if (canDragTile((tileController.item as Tile<T>).item)) {
|
||||||
if (dragState.current === null) {
|
if (dragState.current === null) {
|
||||||
const tileSpring = tileController.get();
|
const tileSpring = tileController.get();
|
||||||
dragState.current = {
|
dragState.current = {
|
||||||
|
@ -422,7 +344,7 @@ export function NewVideoGrid<T>({
|
||||||
data={tile.item.data}
|
data={tile.item.data}
|
||||||
{...spring}
|
{...spring}
|
||||||
>
|
>
|
||||||
{children as (props: ChildrenProperties<unknown>) => ReactNode}
|
{children as (props: ChildrenProperties<T>) => ReactNode}
|
||||||
</TileWrapper>
|
</TileWrapper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, memo, ReactNode, RefObject, useRef } from "react";
|
import React, { memo, ReactNode, RefObject, useRef } from "react";
|
||||||
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
||||||
import { SpringValue, to } from "@react-spring/web";
|
import { SpringValue, to } from "@react-spring/web";
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ interface Props<T> {
|
||||||
* A wrapper around a tile in a video grid. This component exists to decouple
|
* A wrapper around a tile in a video grid. This component exists to decouple
|
||||||
* child components from the grid.
|
* child components from the grid.
|
||||||
*/
|
*/
|
||||||
export const TileWrapper: FC<Props<unknown>> = memo(
|
export const TileWrapper = memo(
|
||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
onDragRef,
|
onDragRef,
|
||||||
|
@ -97,4 +97,7 @@ export const TileWrapper: FC<Props<unknown>> = memo(
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
// We pretend this component is a simple function rather than a
|
||||||
|
// NamedExoticComponent, because that's the only way we can fit in a type
|
||||||
|
// parameter
|
||||||
|
) as <T>(props: Props<T>) => JSX.Element;
|
||||||
|
|
|
@ -42,7 +42,7 @@ import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer"
|
||||||
import styles from "./VideoGrid.module.css";
|
import styles from "./VideoGrid.module.css";
|
||||||
import { Layout } from "../room/GridLayoutMenu";
|
import { Layout } from "../room/GridLayoutMenu";
|
||||||
import { TileWrapper } from "./TileWrapper";
|
import { TileWrapper } from "./TileWrapper";
|
||||||
import { Layout as LayoutSystem } from "./Layout";
|
import { LayoutStatesMap } from "./Layout";
|
||||||
|
|
||||||
interface TilePosition {
|
interface TilePosition {
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -818,7 +818,7 @@ export interface VideoGridProps<T> {
|
||||||
items: TileDescriptor<T>[];
|
items: TileDescriptor<T>[];
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
disableAnimations: boolean;
|
disableAnimations: boolean;
|
||||||
layoutStates: Map<LayoutSystem<unknown>, unknown>;
|
layoutStates: LayoutStatesMap;
|
||||||
children: (props: ChildrenProperties<T>) => React.ReactNode;
|
children: (props: ChildrenProperties<T>) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue