Merge branch 'main' into livekit-experiment
This commit is contained in:
commit
4d5c3cd773
10 changed files with 332 additions and 376 deletions
|
@ -82,6 +82,9 @@ limitations under the License.
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
.avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
@ -89,11 +92,12 @@ limitations under the License.
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
/* To make avatars scale smoothly with their tiles during animations, we
|
/* To make avatars scale smoothly with their tiles during animations, we
|
||||||
override the styles set on the element */
|
override the styles set on the element */
|
||||||
--avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2);
|
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
|
||||||
width: var(--avatarSize) !important;
|
width: var(--avatarSize) !important;
|
||||||
height: var(--avatarSize) !important;
|
height: var(--avatarSize) !important;
|
||||||
border-radius: 10000px !important;
|
border-radius: 10000px !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-height: 300px) {
|
@media (min-height: 300px) {
|
||||||
.inRoom {
|
.inRoom {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
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");
|
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,18 +1076,32 @@ 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
|
||||||
|
// 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();
|
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];
|
||||||
|
|
||||||
|
@ -1138,7 +1146,7 @@ export function VideoGrid<T>({
|
||||||
} else {
|
} else {
|
||||||
const hoverTile = tiles.find(
|
const hoverTile = tiles.find(
|
||||||
(tile) =>
|
(tile) =>
|
||||||
tile.key !== key &&
|
tile.key !== tileId &&
|
||||||
isInside(cursorPosition, tilePositions[tile.order])
|
isInside(cursorPosition, tilePositions[tile.order])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1190,9 +1198,10 @@ export function VideoGrid<T>({
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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