Merge branch 'main' into livekit-experiment

This commit is contained in:
Robin Townsend 2023-06-13 12:33:46 -04:00
commit 4d5c3cd773
10 changed files with 332 additions and 376 deletions

View file

@ -82,17 +82,21 @@ limitations under the License.
bottom: 0; bottom: 0;
} }
.avatar { /* CSS makes us put a condition here, even though all we want to do is
position: absolute; unconditionally select the container so we can use cqmin units */
top: 50%; @container videoTile (width > 0) {
left: 50%; .avatar {
transform: translate(-50%, -50%); position: absolute;
/* To make avatars scale smoothly with their tiles during animations, we top: 50%;
override the styles set on the element */ left: 50%;
--avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2); transform: translate(-50%, -50%);
width: var(--avatarSize) !important; /* To make avatars scale smoothly with their tiles during animations, we
height: var(--avatarSize) !important; override the styles set on the element */
border-radius: 10000px !important; --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) { @media (min-height: 300px) {

View file

@ -28,7 +28,7 @@ import { Room, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; 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 { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays"; import { OverlayTriggerState } from "@react-stately/overlays";
@ -61,24 +61,24 @@ import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { MediaDevicesState } from "../settings/mediaDevices"; import { MediaDevicesState } from "../settings/mediaDevices";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion"; import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ItemData, VideoTileContainer } from "../video-grid/VideoTileContainer";
import { ElementWidgetActions, widget } from "../widget"; import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu"; import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector"; import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css"; import styles from "./InCallView.module.css";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { MatrixInfo } from "./VideoPreview"; import { MatrixInfo } from "./VideoPreview";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall"; import { ParticipantInfo } from "./useGroupCall";
import { TileContent } from "../video-grid/VideoTile"; import { ItemData, TileContent } from "../video-grid/VideoTile";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { NewVideoGrid } 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";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
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
@ -291,14 +291,13 @@ export function InCallView({
} }
if (maximisedParticipant) { if (maximisedParticipant) {
return ( return (
<VideoTileContainer <VideoTile
targetHeight={bounds.height} targetHeight={bounds.height}
targetWidth={bounds.width} targetWidth={bounds.width}
key={maximisedParticipant.id} key={maximisedParticipant.id}
item={maximisedParticipant.data} data={maximisedParticipant.data}
getAvatar={renderAvatar} getAvatar={renderAvatar}
disableSpeakingIndicator={true} showSpeakingIndicator={false}
maximised={Boolean(maximisedParticipant)}
/> />
); );
} }
@ -309,11 +308,12 @@ export function InCallView({
layout={layout} layout={layout}
disableAnimations={prefersReducedMotion || isSafari} disableAnimations={prefersReducedMotion || isSafari}
> >
{(child) => ( {(props) => (
<VideoTileContainer <VideoTile
getAvatar={renderAvatar} getAvatar={renderAvatar}
item={child.data} showSpeakingIndicator={items.length > 2}
{...child} {...props}
ref={props.ref as Ref<HTMLDivElement>}
/> />
)} )}
</Grid> </Grid>

View file

@ -21,14 +21,14 @@ import { MutableRefObject, RefCallback, useCallback } from "react";
* same DOM node. * same DOM node.
*/ */
export const useMergedRefs = <T>( export const useMergedRefs = <T>(
...refs: (MutableRefObject<T | null> | RefCallback<T | null>)[] ...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
): RefCallback<T | null> => ): RefCallback<T | null> =>
useCallback( useCallback(
(value) => (value) =>
refs.forEach((ref) => { refs.forEach((ref) => {
if (typeof ref === "function") { if (typeof ref === "function") {
ref(value); ref(value);
} else { } else if (ref !== null) {
ref.current = value; ref.current = value;
} }
}), }),

View file

@ -18,7 +18,6 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
import { EventTypes, Handler, useScroll } from "@use-gesture/react"; import { EventTypes, Handler, useScroll } from "@use-gesture/react";
import React, { import React, {
Dispatch, Dispatch,
FC,
ReactNode, ReactNode,
SetStateAction, SetStateAction,
useCallback, useCallback,
@ -35,6 +34,7 @@ import {
VideoGridProps as Props, VideoGridProps as Props,
TileSpring, TileSpring,
TileDescriptor, TileDescriptor,
ChildrenProperties,
} from "./VideoGrid"; } from "./VideoGrid";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
import { useMergedRefs } from "../useMergedRefs"; import { useMergedRefs } from "../useMergedRefs";
@ -48,6 +48,7 @@ import {
cycleTileSize, cycleTileSize,
appendItems, appendItems,
} from "./model"; } from "./model";
import { TileWrapper } from "./TileWrapper";
interface GridState extends Grid { interface GridState extends Grid {
/** /**
@ -144,11 +145,11 @@ interface DragState {
/** /**
* An interactive, animated grid of video tiles. * An interactive, animated grid of video tiles.
*/ */
export const NewVideoGrid: FC<Props<unknown>> = ({ export function NewVideoGrid<T>({
items, items,
disableAnimations, disableAnimations,
children, children,
}) => { }: Props<T>) {
// Overview: This component lays out tiles by rendering an invisible template // 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 // 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 // get the dimensions of each slot, feeding these numbers back into
@ -455,16 +456,19 @@ export const NewVideoGrid: FC<Props<unknown>> = ({
> >
{slots} {slots}
</div> </div>
{tileTransitions((style, tile) => {tileTransitions((spring, tile) => (
children({ <TileWrapper
...style, key={tile.item.id}
key: tile.item.id, id={tile.item.id}
targetWidth: tile.width, onDragRef={onTileDragRef}
targetHeight: tile.height, targetWidth={tile.width}
data: tile.item.data, targetHeight={tile.height}
onDragRef: onTileDragRef, data={tile.item.data}
}) {...spring}
)} >
{children as (props: ChildrenProperties<unknown>) => ReactNode}
</TileWrapper>
))}
</div> </div>
); );
}; }

View 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,
})}
</>
);
}
);

View file

@ -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,
};

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,8 +15,10 @@ limitations under the License.
*/ */
import React, { import React, {
ComponentProps,
Key, Key,
RefObject, ReactNode,
Ref,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
@ -26,21 +28,20 @@ import {
EventTypes, EventTypes,
FullGestureState, FullGestureState,
Handler, Handler,
useDrag,
useGesture, useGesture,
} from "@use-gesture/react"; } from "@use-gesture/react";
import { import {
animated,
SpringRef, SpringRef,
SpringValue,
SpringValues, SpringValues,
useSprings, useSprings,
} from "@react-spring/web"; } from "@react-spring/web";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer"; 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 styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu"; import { Layout } from "../room/GridLayoutMenu";
import { TileWrapper } from "./TileWrapper";
interface TilePosition { interface TilePosition {
x: number; x: number;
@ -51,7 +52,7 @@ interface TilePosition {
} }
interface Tile<T> { interface Tile<T> {
key: Key; key: string;
order: number; order: number;
item: TileDescriptor<T>; item: TileDescriptor<T>;
remove: boolean; remove: boolean;
@ -727,25 +728,18 @@ interface DragTileData {
y: number; y: number;
} }
export interface ChildrenProperties<T> extends ReactDOMAttributes { export interface ChildrenProperties<T> {
key: Key; ref: Ref<HTMLElement>;
data: T; style: ComponentProps<typeof animated.div>["style"];
/**
* The width this tile will have once its animations have settled.
*/
targetWidth: number; targetWidth: number;
/**
* The height this tile will have once its animations have settled.
*/
targetHeight: number; targetHeight: number;
opacity: SpringValue<number>; data: T;
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
>;
} }
export interface VideoGridProps<T> { export interface VideoGridProps<T> {
@ -768,7 +762,7 @@ export function VideoGrid<T>({
items, items,
layout, layout,
disableAnimations, disableAnimations,
children: createChild, children,
}: VideoGridProps<T>) { }: VideoGridProps<T>) {
// Place the PiP in the bottom right corner by default // Place the PiP in the bottom right corner by default
const [pipXRatio, setPipXRatio] = useState(1); const [pipXRatio, setPipXRatio] = useState(1);
@ -1082,117 +1076,132 @@ export function VideoGrid<T>({
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio] [tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
); );
const bindTile = useDrag( // Callback for useDrag. We could call useDrag here, but the default
({ args: [key], active, xy, movement, tap, last, event }) => { // pattern of spreading {...bind()} across the children to bind the gesture
event.preventDefault(); // 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) { if (tap) {
onTap(key); onTap(tileId);
return; return;
} }
if (layout !== "freedom") return; if (layout !== "freedom") return;
const dragTileIndex = tiles.findIndex((tile) => tile.key === key); const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
const dragTile = tiles[dragTileIndex]; const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTile.order]; 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)) { if (tiles.length === 2 && !tiles.some((t) => t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable // We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.local) return; if (!dragTile.item.local) return;
// Position should only update on the very last event, to avoid // Position should only update on the very last event, to avoid
// compounding the offset on every drag event // compounding the offset on every drag event
if (last) { if (last) {
const remotePosition = tilePositions[1]; const remotePosition = tilePositions[1];
const pipGap = getPipGap( const pipGap = getPipGap(
gridBounds.width / gridBounds.height, gridBounds.width / gridBounds.height,
gridBounds.width 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 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) { const newPipXRatio =
// Shift the tiles into their new order (dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
newTiles = newTiles.map((tile) => { const newPipYRatio =
let order = tile.order; (dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
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; setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
if (tile === hoverTile) { setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
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 }));
}
} }
} else {
const hoverTile = tiles.find(
(tile) =>
tile.key !== tileId &&
isInside(cursorPosition, tilePositions[tile.order])
);
if (active) { if (hoverTile) {
if (!draggingTileRef.current) { // Shift the tiles into their new order
draggingTileRef.current = { newTiles = newTiles.map((tile) => {
key: dragTile.key, let order = tile.order;
offsetX: dragTilePosition.x, if (order < dragTile.order) {
offsetY: dragTilePosition.y, if (order >= hoverTile.order) order++;
x: movement[0], } else if (order > dragTile.order) {
y: movement[1], if (order <= hoverTile.order) order--;
}; } else {
} else { order = hoverTile.order;
draggingTileRef.current.x = movement[0]; }
draggingTileRef.current.y = movement[1];
} 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 { } else {
draggingTileRef.current = null; draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
} }
} else {
draggingTileRef.current = null;
}
api.start(animate(newTiles)); api.start(animate(newTiles));
}, };
{ filterTaps: true, pointer: { buttons: [1] } }
); const onTileDragRef = useRef(onTileDrag);
onTileDragRef.current = onTileDrag;
const onGridGesture = useCallback( const onGridGesture = useCallback(
( (
@ -1239,18 +1248,23 @@ export function VideoGrid<T>({
return ( return (
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}> <div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
{springs.map((style, i) => { {springs.map((spring, i) => {
const tile = tiles[i]; const tile = tiles[i];
const tilePosition = tilePositions[tile.order]; const tilePosition = tilePositions[tile.order];
return createChild({ return (
...bindTile(tile.key), <TileWrapper
...style, key={tile.key}
key: tile.key, id={tile.key}
data: tile.item.data, onDragRef={onTileDragRef}
targetWidth: tilePosition.width, targetWidth={tilePosition.width}
targetHeight: tilePosition.height, targetHeight={tilePosition.height}
}); data={tile.item.data}
{...spring}
>
{children as (props: ChildrenProperties<unknown>) => ReactNode}
</TileWrapper>
);
})} })}
</div> </div>
); );

View file

@ -18,12 +18,10 @@ limitations under the License.
position: absolute; position: absolute;
contain: strict; contain: strict;
top: 0; top: 0;
width: var(--tileWidth); container-name: videoTile;
height: var(--tileHeight); container-type: size;
--tileRadius: 8px; --tileRadius: 8px;
border-radius: var(--tileRadius); border-radius: var(--tileRadius);
box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow)
calc(2 * var(--tileShadow)) var(--tileShadowSpread);
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import React, { ForwardedRef, forwardRef } from "react"; import React, { ComponentProps, forwardRef } from "react";
import { animated, SpringValue } from "@react-spring/web"; import { animated } from "@react-spring/web";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { LocalParticipant, RemoteParticipant, Track } from "livekit-client"; import { LocalParticipant, RemoteParticipant, Track } from "livekit-client";
@ -24,10 +24,19 @@ import {
VideoTrack, VideoTrack,
useMediaTrack, useMediaTrack,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import styles from "./VideoTile.module.css"; import styles from "./VideoTile.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.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 { export enum TileContent {
UserMedia = "user-media", UserMedia = "user-media",
@ -35,52 +44,37 @@ export enum TileContent {
} }
interface Props { interface Props {
avatar?: JSX.Element; data: ItemData;
className?: string; className?: string;
name: string; showSpeakingIndicator: boolean;
sfuParticipant: LocalParticipant | RemoteParticipant; style?: ComponentProps<typeof animated.div>["style"];
content: TileContent; targetWidth: number;
showOptions?: boolean; targetHeight: number;
isLocal?: boolean; getAvatar: (
disableSpeakingIndicator?: boolean; roomMember: RoomMember,
opacity?: SpringValue<number>; width: number,
scale?: SpringValue<number>; height: number
shadow?: SpringValue<number>; ) => JSX.Element;
shadowSpread?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
width?: SpringValue<number>;
height?: SpringValue<number>;
} }
export const VideoTile = forwardRef<HTMLElement, Props>( export const VideoTile = forwardRef<HTMLDivElement, Props>(
( (
{ {
name, data,
sfuParticipant,
content,
avatar,
className, className,
showOptions, showSpeakingIndicator,
isLocal, style,
// TODO: disableSpeakingIndicator is not used atm. targetWidth,
disableSpeakingIndicator, targetHeight,
opacity, getAvatar,
scale,
shadow,
shadowSpread,
zIndex,
x,
y,
width,
height,
...rest
}, },
ref tileRef
) => { ) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { content, sfuParticipant } = data;
const { rawDisplayName: name } = useRoomMemberName(data.member);
const audioEl = React.useRef<HTMLAudioElement>(null); const audioEl = React.useRef<HTMLAudioElement>(null);
const { isMuted: microphoneMuted } = useMediaTrack( const { isMuted: microphoneMuted } = useMediaTrack(
content === TileContent.UserMedia 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 ( return (
<animated.div <animated.div
className={classNames(styles.videoTile, className, { className={classNames(styles.videoTile, className, {
[styles.isLocal]: sfuParticipant.isLocal, [styles.isLocal]: sfuParticipant.isLocal,
[styles.speaking]: sfuParticipant.isSpeaking, [styles.speaking]: sfuParticipant.isSpeaking && showSpeakingIndicator,
[styles.muted]: microphoneMuted, [styles.muted]: microphoneMuted,
[styles.screenshare]: content === TileContent.ScreenShare, [styles.screenshare]: content === TileContent.ScreenShare,
})} })}
style={{ style={style}
opacity, ref={tileRef}
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>}
data-testid="videoTile" data-testid="videoTile"
{...rest}
> >
{!sfuParticipant.isCameraEnabled && ( {!sfuParticipant.isCameraEnabled && (
<> <>
<div className={styles.videoMutedOverlay} /> <div className={styles.videoMutedOverlay} />
{avatar} {getAvatar(data.member, targetWidth, targetHeight)}
</> </>
)} )}
{!false && {!false &&

View file

@ -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}
/>
);
}
);