Merge branch 'main' into livekit-experiment
This commit is contained in:
commit
4d5c3cd773
10 changed files with 332 additions and 376 deletions
|
@ -82,17 +82,21 @@ limitations under the License.
|
|||
bottom: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* To make avatars scale smoothly with their tiles during animations, we
|
||||
override the styles set on the element */
|
||||
--avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2);
|
||||
width: var(--avatarSize) !important;
|
||||
height: var(--avatarSize) !important;
|
||||
border-radius: 10000px !important;
|
||||
/* CSS makes us put a condition here, even though all we want to do is
|
||||
unconditionally select the container so we can use cqmin units */
|
||||
@container videoTile (width > 0) {
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* To make avatars scale smoothly with their tiles during animations, we
|
||||
override the styles set on the element */
|
||||
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
|
||||
width: var(--avatarSize) !important;
|
||||
height: var(--avatarSize) !important;
|
||||
border-radius: 10000px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 300px) {
|
||||
|
|
|
@ -28,7 +28,7 @@ import { Room, Track } from "livekit-client";
|
|||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import React, { Ref, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
|
@ -61,24 +61,24 @@ import { useModalTriggerState } from "../Modal";
|
|||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { MediaDevicesState } from "../settings/mediaDevices";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||
import { ItemData, VideoTileContainer } from "../video-grid/VideoTileContainer";
|
||||
import { ElementWidgetActions, widget } from "../widget";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import styles from "./InCallView.module.css";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { MatrixInfo } from "./VideoPreview";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { ParticipantInfo } from "./useGroupCall";
|
||||
import { TileContent } from "../video-grid/VideoTile";
|
||||
import { ItemData, TileContent } from "../video-grid/VideoTile";
|
||||
import { Config } from "../config/Config";
|
||||
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { VideoTile } from "../video-grid/VideoTile";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
|
@ -291,14 +291,13 @@ export function InCallView({
|
|||
}
|
||||
if (maximisedParticipant) {
|
||||
return (
|
||||
<VideoTileContainer
|
||||
<VideoTile
|
||||
targetHeight={bounds.height}
|
||||
targetWidth={bounds.width}
|
||||
key={maximisedParticipant.id}
|
||||
item={maximisedParticipant.data}
|
||||
data={maximisedParticipant.data}
|
||||
getAvatar={renderAvatar}
|
||||
disableSpeakingIndicator={true}
|
||||
maximised={Boolean(maximisedParticipant)}
|
||||
showSpeakingIndicator={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -309,11 +308,12 @@ export function InCallView({
|
|||
layout={layout}
|
||||
disableAnimations={prefersReducedMotion || isSafari}
|
||||
>
|
||||
{(child) => (
|
||||
<VideoTileContainer
|
||||
{(props) => (
|
||||
<VideoTile
|
||||
getAvatar={renderAvatar}
|
||||
item={child.data}
|
||||
{...child}
|
||||
showSpeakingIndicator={items.length > 2}
|
||||
{...props}
|
||||
ref={props.ref as Ref<HTMLDivElement>}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
|
|
|
@ -21,14 +21,14 @@ import { MutableRefObject, RefCallback, useCallback } from "react";
|
|||
* same DOM node.
|
||||
*/
|
||||
export const useMergedRefs = <T>(
|
||||
...refs: (MutableRefObject<T | null> | RefCallback<T | null>)[]
|
||||
...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
|
||||
): RefCallback<T | null> =>
|
||||
useCallback(
|
||||
(value) =>
|
||||
refs.forEach((ref) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(value);
|
||||
} else {
|
||||
} else if (ref !== null) {
|
||||
ref.current = value;
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -18,7 +18,6 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
|
|||
import { EventTypes, Handler, useScroll } from "@use-gesture/react";
|
||||
import React, {
|
||||
Dispatch,
|
||||
FC,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
|
@ -35,6 +34,7 @@ import {
|
|||
VideoGridProps as Props,
|
||||
TileSpring,
|
||||
TileDescriptor,
|
||||
ChildrenProperties,
|
||||
} from "./VideoGrid";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { useMergedRefs } from "../useMergedRefs";
|
||||
|
@ -48,6 +48,7 @@ import {
|
|||
cycleTileSize,
|
||||
appendItems,
|
||||
} from "./model";
|
||||
import { TileWrapper } from "./TileWrapper";
|
||||
|
||||
interface GridState extends Grid {
|
||||
/**
|
||||
|
@ -144,11 +145,11 @@ interface DragState {
|
|||
/**
|
||||
* An interactive, animated grid of video tiles.
|
||||
*/
|
||||
export const NewVideoGrid: FC<Props<unknown>> = ({
|
||||
export function NewVideoGrid<T>({
|
||||
items,
|
||||
disableAnimations,
|
||||
children,
|
||||
}) => {
|
||||
}: Props<T>) {
|
||||
// Overview: This component lays out tiles by rendering an invisible template
|
||||
// grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to
|
||||
// get the dimensions of each slot, feeding these numbers back into
|
||||
|
@ -455,16 +456,19 @@ export const NewVideoGrid: FC<Props<unknown>> = ({
|
|||
>
|
||||
{slots}
|
||||
</div>
|
||||
{tileTransitions((style, tile) =>
|
||||
children({
|
||||
...style,
|
||||
key: tile.item.id,
|
||||
targetWidth: tile.width,
|
||||
targetHeight: tile.height,
|
||||
data: tile.item.data,
|
||||
onDragRef: onTileDragRef,
|
||||
})
|
||||
)}
|
||||
{tileTransitions((spring, tile) => (
|
||||
<TileWrapper
|
||||
key={tile.item.id}
|
||||
id={tile.item.id}
|
||||
onDragRef={onTileDragRef}
|
||||
targetWidth={tile.width}
|
||||
targetHeight={tile.height}
|
||||
data={tile.item.data}
|
||||
{...spring}
|
||||
>
|
||||
{children as (props: ChildrenProperties<unknown>) => ReactNode}
|
||||
</TileWrapper>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
100
src/video-grid/TileWrapper.tsx
Normal file
100
src/video-grid/TileWrapper.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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 React, { FC, memo, ReactNode, RefObject, useRef } from "react";
|
||||
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
||||
import { SpringValue, to } from "@react-spring/web";
|
||||
|
||||
import { ChildrenProperties } from "./VideoGrid";
|
||||
|
||||
interface Props<T> {
|
||||
id: string;
|
||||
onDragRef: RefObject<
|
||||
(
|
||||
tileId: string,
|
||||
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => void
|
||||
>;
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
data: T;
|
||||
opacity: SpringValue<number>;
|
||||
scale: SpringValue<number>;
|
||||
shadow: SpringValue<number>;
|
||||
shadowSpread: SpringValue<number>;
|
||||
zIndex: SpringValue<number>;
|
||||
x: SpringValue<number>;
|
||||
y: SpringValue<number>;
|
||||
width: SpringValue<number>;
|
||||
height: SpringValue<number>;
|
||||
children: (props: ChildrenProperties<T>) => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a tile in a video grid. This component exists to decouple
|
||||
* child components from the grid.
|
||||
*/
|
||||
export const TileWrapper: FC<Props<unknown>> = memo(
|
||||
({
|
||||
id,
|
||||
onDragRef,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
data,
|
||||
opacity,
|
||||
scale,
|
||||
shadow,
|
||||
shadowSpread,
|
||||
zIndex,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
}) => {
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
|
||||
useDrag((state) => onDragRef?.current!(id, state), {
|
||||
target: ref,
|
||||
filterTaps: true,
|
||||
preventScroll: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({
|
||||
ref,
|
||||
style: {
|
||||
opacity,
|
||||
scale,
|
||||
zIndex,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
boxShadow: to(
|
||||
[shadow, shadowSpread],
|
||||
(s, ss) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px ${ss}px`
|
||||
),
|
||||
},
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
data,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 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 React, { useState } from "react";
|
||||
|
||||
import { useVideoGridLayout } from "./VideoGrid";
|
||||
import { Button } from "../button";
|
||||
|
||||
export default {
|
||||
title: "VideoGrid",
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
};
|
||||
|
||||
const ParticipantsTest = () => {
|
||||
const { layout, setLayout } = useVideoGridLayout(false);
|
||||
const [participantCount, setParticipantCount] = useState(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "flex", width: "100vw", height: "32px" }}>
|
||||
<Button
|
||||
onPress={() =>
|
||||
setLayout(layout === "freedom" ? "spotlight" : "freedom")
|
||||
}
|
||||
>
|
||||
Toggle Layout
|
||||
</Button>
|
||||
{participantCount < 12 && (
|
||||
<Button onPress={() => setParticipantCount((count) => count + 1)}>
|
||||
Add Participant
|
||||
</Button>
|
||||
)}
|
||||
{participantCount > 0 && (
|
||||
<Button onPress={() => setParticipantCount((count) => count - 1)}>
|
||||
Remove Participant
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ParticipantsTest.args = {
|
||||
layout: "freedom",
|
||||
participantCount: 1,
|
||||
};
|
|
@ -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.
|
||||
|
@ -15,8 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {
|
||||
ComponentProps,
|
||||
Key,
|
||||
RefObject,
|
||||
ReactNode,
|
||||
Ref,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
|
@ -26,21 +28,20 @@ import {
|
|||
EventTypes,
|
||||
FullGestureState,
|
||||
Handler,
|
||||
useDrag,
|
||||
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 { TileWrapper } from "./TileWrapper";
|
||||
|
||||
interface TilePosition {
|
||||
x: number;
|
||||
|
@ -51,7 +52,7 @@ interface TilePosition {
|
|||
}
|
||||
|
||||
interface Tile<T> {
|
||||
key: Key;
|
||||
key: string;
|
||||
order: number;
|
||||
item: TileDescriptor<T>;
|
||||
remove: boolean;
|
||||
|
@ -727,25 +728,18 @@ interface DragTileData {
|
|||
y: number;
|
||||
}
|
||||
|
||||
export interface ChildrenProperties<T> extends ReactDOMAttributes {
|
||||
key: Key;
|
||||
data: T;
|
||||
export interface ChildrenProperties<T> {
|
||||
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;
|
||||
opacity: SpringValue<number>;
|
||||
scale: SpringValue<number>;
|
||||
shadow: SpringValue<number>;
|
||||
zIndex: SpringValue<number>;
|
||||
x: SpringValue<number>;
|
||||
y: SpringValue<number>;
|
||||
width: SpringValue<number>;
|
||||
height: SpringValue<number>;
|
||||
onDragRef?: RefObject<
|
||||
(
|
||||
tileId: string,
|
||||
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => void
|
||||
>;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface VideoGridProps<T> {
|
||||
|
@ -768,7 +762,7 @@ export function VideoGrid<T>({
|
|||
items,
|
||||
layout,
|
||||
disableAnimations,
|
||||
children: createChild,
|
||||
children,
|
||||
}: VideoGridProps<T>) {
|
||||
// Place the PiP in the bottom right corner by default
|
||||
const [pipXRatio, setPipXRatio] = useState(1);
|
||||
|
@ -1082,117 +1076,132 @@ export function VideoGrid<T>({
|
|||
[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.focused)) {
|
||||
// We're in 1:1 mode, so only the local tile should be draggable
|
||||
if (!dragTile.item.local) return;
|
||||
if (tiles.length === 2 && !tiles.some((t) => t.focused)) {
|
||||
// We're in 1:1 mode, so only the local tile should be draggable
|
||||
if (!dragTile.item.local) 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(
|
||||
(
|
||||
|
@ -1239,18 +1248,23 @@ export function VideoGrid<T>({
|
|||
|
||||
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 createChild({
|
||||
...bindTile(tile.key),
|
||||
...style,
|
||||
key: tile.key,
|
||||
data: tile.item.data,
|
||||
targetWidth: tilePosition.width,
|
||||
targetHeight: tilePosition.height,
|
||||
});
|
||||
return (
|
||||
<TileWrapper
|
||||
key={tile.key}
|
||||
id={tile.key}
|
||||
onDragRef={onTileDragRef}
|
||||
targetWidth={tilePosition.width}
|
||||
targetHeight={tilePosition.height}
|
||||
data={tile.item.data}
|
||||
{...spring}
|
||||
>
|
||||
{children as (props: ChildrenProperties<unknown>) => ReactNode}
|
||||
</TileWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,12 +18,10 @@ limitations under the License.
|
|||
position: absolute;
|
||||
contain: strict;
|
||||
top: 0;
|
||||
width: var(--tileWidth);
|
||||
height: var(--tileHeight);
|
||||
container-name: videoTile;
|
||||
container-type: size;
|
||||
--tileRadius: 8px;
|
||||
border-radius: var(--tileRadius);
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow)
|
||||
calc(2 * var(--tileShadow)) var(--tileShadowSpread);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
|
|
|
@ -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,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ForwardedRef, forwardRef } from "react";
|
||||
import { animated, SpringValue } from "@react-spring/web";
|
||||
import React, { ComponentProps, forwardRef } from "react";
|
||||
import { animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LocalParticipant, RemoteParticipant, Track } from "livekit-client";
|
||||
|
@ -24,10 +24,19 @@ import {
|
|||
VideoTrack,
|
||||
useMediaTrack,
|
||||
} from "@livekit/components-react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import styles from "./VideoTile.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||
import { useRoomMemberName } from "./useRoomMemberName";
|
||||
|
||||
export interface ItemData {
|
||||
id: string;
|
||||
member: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
}
|
||||
|
||||
export enum TileContent {
|
||||
UserMedia = "user-media",
|
||||
|
@ -35,52 +44,37 @@ export enum TileContent {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
avatar?: JSX.Element;
|
||||
data: ItemData;
|
||||
className?: string;
|
||||
name: string;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
showOptions?: boolean;
|
||||
isLocal?: boolean;
|
||||
disableSpeakingIndicator?: boolean;
|
||||
opacity?: SpringValue<number>;
|
||||
scale?: SpringValue<number>;
|
||||
shadow?: SpringValue<number>;
|
||||
shadowSpread?: SpringValue<number>;
|
||||
zIndex?: SpringValue<number>;
|
||||
x?: SpringValue<number>;
|
||||
y?: SpringValue<number>;
|
||||
width?: SpringValue<number>;
|
||||
height?: SpringValue<number>;
|
||||
showSpeakingIndicator: boolean;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
getAvatar: (
|
||||
roomMember: RoomMember,
|
||||
width: number,
|
||||
height: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export const VideoTile = forwardRef<HTMLElement, Props>(
|
||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
sfuParticipant,
|
||||
content,
|
||||
avatar,
|
||||
data,
|
||||
className,
|
||||
showOptions,
|
||||
isLocal,
|
||||
// TODO: disableSpeakingIndicator is not used atm.
|
||||
disableSpeakingIndicator,
|
||||
opacity,
|
||||
scale,
|
||||
shadow,
|
||||
shadowSpread,
|
||||
zIndex,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
...rest
|
||||
showSpeakingIndicator,
|
||||
style,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
getAvatar,
|
||||
},
|
||||
ref
|
||||
tileRef
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { content, sfuParticipant } = data;
|
||||
const { rawDisplayName: name } = useRoomMemberName(data.member);
|
||||
|
||||
const audioEl = React.useRef<HTMLAudioElement>(null);
|
||||
const { isMuted: microphoneMuted } = useMediaTrack(
|
||||
content === TileContent.UserMedia
|
||||
|
@ -92,36 +86,25 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
|
|||
}
|
||||
);
|
||||
|
||||
// Firefox doesn't respect the disablePictureInPicture attribute
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
className={classNames(styles.videoTile, className, {
|
||||
[styles.isLocal]: sfuParticipant.isLocal,
|
||||
[styles.speaking]: sfuParticipant.isSpeaking,
|
||||
[styles.speaking]: sfuParticipant.isSpeaking && showSpeakingIndicator,
|
||||
[styles.muted]: microphoneMuted,
|
||||
[styles.screenshare]: content === TileContent.ScreenShare,
|
||||
})}
|
||||
style={{
|
||||
opacity,
|
||||
scale,
|
||||
zIndex,
|
||||
x,
|
||||
y,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore React does in fact support assigning custom properties,
|
||||
// but React's types say no
|
||||
"--tileWidth": width?.to((w) => `${w}px`),
|
||||
"--tileHeight": height?.to((h) => `${h}px`),
|
||||
"--tileShadow": shadow?.to((s) => `${s}px`),
|
||||
"--tileShadowSpread": shadowSpread?.to((s) => `${s}px`),
|
||||
}}
|
||||
ref={ref as ForwardedRef<HTMLDivElement>}
|
||||
style={style}
|
||||
ref={tileRef}
|
||||
data-testid="videoTile"
|
||||
{...rest}
|
||||
>
|
||||
{!sfuParticipant.isCameraEnabled && (
|
||||
<>
|
||||
<div className={styles.videoMutedOverlay} />
|
||||
{avatar}
|
||||
{getAvatar(data.member, targetWidth, targetHeight)}
|
||||
</>
|
||||
)}
|
||||
{!false &&
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 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 { LocalParticipant, RemoteParticipant } from "livekit-client";
|
||||
import React, { FC, memo, RefObject, useRef } from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { SpringValue } from "@react-spring/web";
|
||||
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
||||
|
||||
import { useRoomMemberName } from "./useRoomMemberName";
|
||||
import { TileContent, VideoTile } from "./VideoTile";
|
||||
|
||||
export interface ItemData {
|
||||
id: string;
|
||||
member: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: ItemData;
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
getAvatar: (
|
||||
roomMember: RoomMember,
|
||||
width: number,
|
||||
height: number
|
||||
) => JSX.Element;
|
||||
disableSpeakingIndicator: boolean;
|
||||
maximised: boolean;
|
||||
opacity?: SpringValue<number>;
|
||||
scale?: SpringValue<number>;
|
||||
shadow?: SpringValue<number>;
|
||||
shadowSpread?: SpringValue<number>;
|
||||
zIndex?: SpringValue<number>;
|
||||
x?: SpringValue<number>;
|
||||
y?: SpringValue<number>;
|
||||
width?: SpringValue<number>;
|
||||
height?: SpringValue<number>;
|
||||
onDragRef?: RefObject<
|
||||
(
|
||||
tileId: string,
|
||||
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => void
|
||||
>;
|
||||
}
|
||||
|
||||
export const VideoTileContainer: FC<Props> = memo(
|
||||
({ item, targetWidth, targetHeight, getAvatar, onDragRef, ...rest }) => {
|
||||
const { rawDisplayName } = useRoomMemberName(item.member);
|
||||
const tileRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useDrag((state) => onDragRef?.current!(item.id, state), {
|
||||
target: tileRef,
|
||||
filterTaps: true,
|
||||
preventScroll: true,
|
||||
});
|
||||
|
||||
// Firefox doesn't respect the disablePictureInPicture attribute
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
||||
|
||||
return (
|
||||
<VideoTile
|
||||
ref={tileRef}
|
||||
sfuParticipant={item.sfuParticipant}
|
||||
content={item.content}
|
||||
name={rawDisplayName}
|
||||
avatar={getAvatar && getAvatar(item.member, targetWidth, targetHeight)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
Loading…
Add table
Reference in a new issue