Merge branch 'livekit-experiment' into livekit-load-test
This commit is contained in:
commit
520f241efa
13 changed files with 352 additions and 427 deletions
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"{{count}} stars|one": "{{count}} star",
|
||||
"{{count}} stars|other": "{{count}} stars",
|
||||
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
||||
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
||||
"{{name}} is presenting": "{{name}} is presenting",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||
|
@ -102,7 +102,6 @@
|
|||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Unmute microphone": "Unmute microphone",
|
||||
"Use the upcoming grid system": "Use the upcoming grid system",
|
||||
"User menu": "User menu",
|
||||
"Username": "Username",
|
||||
"Version: {{version}}": "Version: {{version}}",
|
||||
|
|
|
@ -82,19 +82,6 @@ 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;
|
||||
}
|
||||
|
||||
@media (min-height: 300px) {
|
||||
.inRoom {
|
||||
--footerPadding: 24px;
|
||||
|
|
|
@ -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";
|
||||
|
@ -55,29 +55,29 @@ import {
|
|||
useVideoGridLayout,
|
||||
TileDescriptor,
|
||||
} from "../video-grid/VideoGrid";
|
||||
import { useNewGrid, useShowInspector } from "../settings/useSetting";
|
||||
import { useShowInspector } from "../settings/useSetting";
|
||||
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
|
||||
|
@ -258,8 +258,9 @@ export function InCallView({
|
|||
[noControls, items]
|
||||
);
|
||||
|
||||
const [newGrid] = useNewGrid();
|
||||
const Grid = newGrid ? NewVideoGrid : VideoGrid;
|
||||
const Grid =
|
||||
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
|
||||
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
|
||||
const renderContent = (): JSX.Element => {
|
||||
|
@ -272,12 +273,11 @@ export function InCallView({
|
|||
}
|
||||
if (maximisedParticipant) {
|
||||
return (
|
||||
<VideoTileContainer
|
||||
<VideoTile
|
||||
targetHeight={bounds.height}
|
||||
targetWidth={bounds.width}
|
||||
id={maximisedParticipant.id}
|
||||
key={maximisedParticipant.id}
|
||||
item={maximisedParticipant.data}
|
||||
data={maximisedParticipant.data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -288,7 +288,9 @@ export function InCallView({
|
|||
layout={layout}
|
||||
disableAnimations={prefersReducedMotion || isSafari}
|
||||
>
|
||||
{(child) => <VideoTileContainer item={child.data} {...child} />}
|
||||
{(props) => (
|
||||
<VideoTile {...props} ref={props.ref as Ref<HTMLDivElement>} />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
@ -462,7 +464,6 @@ function useParticipantTiles(
|
|||
focused: false,
|
||||
local: sfuParticipant.isLocal,
|
||||
data: {
|
||||
id,
|
||||
member,
|
||||
sfuParticipant,
|
||||
content: TileContent.UserMedia,
|
||||
|
|
|
@ -33,7 +33,6 @@ import { MediaDevicesState } from "./mediaDevices";
|
|||
import {
|
||||
useShowInspector,
|
||||
useOptInAnalytics,
|
||||
useNewGrid,
|
||||
useDeveloperSettingsTab,
|
||||
} from "./useSetting";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
|
@ -60,7 +59,6 @@ export const SettingsModal = (props: Props) => {
|
|||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||
const [developerSettingsTab, setDeveloperSettingsTab] =
|
||||
useDeveloperSettingsTab();
|
||||
const [newGrid, setNewGrid] = useNewGrid();
|
||||
|
||||
const downloadDebugLog = useDownloadDebugLog();
|
||||
|
||||
|
@ -235,17 +233,6 @@ export const SettingsModal = (props: Props) => {
|
|||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="newGrid"
|
||||
label={t("Use the upcoming grid system")}
|
||||
type="checkbox"
|
||||
checked={newGrid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setNewGrid(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Button onPress={downloadDebugLog}>
|
||||
{t("Download debug logs")}
|
||||
|
|
|
@ -98,7 +98,5 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
|||
return [false, null];
|
||||
};
|
||||
|
||||
export const useNewGrid = () => useSetting("new-grid", false);
|
||||
|
||||
export const useDeveloperSettingsTab = () =>
|
||||
useSetting("developer-settings-tab", false);
|
||||
|
|
|
@ -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,18 +1076,32 @@ export function VideoGrid<T>({
|
|||
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
|
||||
);
|
||||
|
||||
const bindTile = useDrag(
|
||||
({ args: [key], active, xy, movement, tap, last, event }) => {
|
||||
// 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);
|
||||
onTap(tileId);
|
||||
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 dragTilePosition = tilePositions[dragTile.order];
|
||||
|
||||
|
@ -1138,7 +1146,7 @@ export function VideoGrid<T>({
|
|||
} else {
|
||||
const hoverTile = tiles.find(
|
||||
(tile) =>
|
||||
tile.key !== key &&
|
||||
tile.key !== tileId &&
|
||||
isInside(cursorPosition, tilePositions[tile.order])
|
||||
);
|
||||
|
||||
|
@ -1190,9 +1198,10 @@ export function VideoGrid<T>({
|
|||
}
|
||||
|
||||
api.start(animate(newTiles));
|
||||
},
|
||||
{ filterTaps: true, pointer: { buttons: [1] } }
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
|
@ -195,3 +193,20 @@ limitations under the License.
|
|||
--tileRadius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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,59 +24,60 @@ import {
|
|||
VideoTrack,
|
||||
useMediaTrack,
|
||||
} from "@livekit/components-react";
|
||||
import {
|
||||
RoomMember,
|
||||
RoomMemberEvent,
|
||||
} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoTile.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||
|
||||
export interface ItemData {
|
||||
member?: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
}
|
||||
|
||||
export enum TileContent {
|
||||
UserMedia = "user-media",
|
||||
ScreenShare = "screen-share",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
data: ItemData;
|
||||
|
||||
// TODO: Refactor this set of props.
|
||||
// See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404
|
||||
avatar?: JSX.Element;
|
||||
// TODO: Refactor these props.
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
className?: string;
|
||||
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>;
|
||||
style?: React.ComponentProps<typeof animated.div>["style"];
|
||||
}
|
||||
|
||||
export const VideoTile = forwardRef<HTMLElement, Props>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
sfuParticipant,
|
||||
content,
|
||||
avatar,
|
||||
className,
|
||||
opacity,
|
||||
scale,
|
||||
shadow,
|
||||
shadowSpread,
|
||||
zIndex,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
export const VideoTile = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ data, className, style, targetWidth, targetHeight }, tileRef) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { content, sfuParticipant, member } = data;
|
||||
|
||||
// Handle display name changes.
|
||||
const [displayName, setDisplayName] = React.useState<string>("[👻]");
|
||||
React.useEffect(() => {
|
||||
if (member) {
|
||||
setDisplayName(member.rawDisplayName);
|
||||
|
||||
const updateName = () => {
|
||||
setDisplayName(member.rawDisplayName);
|
||||
};
|
||||
|
||||
member!.on(RoomMemberEvent.Name, updateName);
|
||||
return () => {
|
||||
member!.removeListener(RoomMemberEvent.Name, updateName);
|
||||
};
|
||||
}
|
||||
}, [member]);
|
||||
|
||||
const audioEl = React.useRef<HTMLAudioElement>(null);
|
||||
const { isMuted: microphoneMuted } = useMediaTrack(
|
||||
content === TileContent.UserMedia
|
||||
|
@ -88,6 +89,9 @@ 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, {
|
||||
|
@ -96,38 +100,30 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
|
|||
[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 && (
|
||||
{content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && (
|
||||
<>
|
||||
<div className={styles.videoMutedOverlay} />
|
||||
{avatar}
|
||||
<Avatar
|
||||
key={member?.userId}
|
||||
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
|
||||
src={member?.getMxcAvatarUrl()}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{sfuParticipant.isScreenShareEnabled ? (
|
||||
{content == TileContent.ScreenShare ? (
|
||||
<div className={styles.presenterLabel}>
|
||||
<span>{t("{{name}} is presenting", { name })}</span>
|
||||
<span>{t("{{displayName}} is presenting", { displayName })}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||
{microphoneMuted ? <MicMutedIcon /> : <MicIcon />}
|
||||
<span title={name}>{name}</span>
|
||||
<span title={displayName}>{displayName}</span>
|
||||
<ConnectionQualityIndicator participant={sfuParticipant} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,115 +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 from "react";
|
||||
import {
|
||||
RoomMember,
|
||||
RoomMemberEvent,
|
||||
} from "matrix-js-sdk/src/models/room-member";
|
||||
import { LocalParticipant, RemoteParticipant } from "livekit-client";
|
||||
import { SpringValue } from "@react-spring/web";
|
||||
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
||||
|
||||
import { TileContent, VideoTile } from "./VideoTile";
|
||||
import { Avatar } from "../Avatar";
|
||||
import Styles from "../room/InCallView.module.css";
|
||||
|
||||
export interface ItemData {
|
||||
member?: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
content: TileContent;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: ItemData;
|
||||
|
||||
// TODO: Refactor this set of props.
|
||||
// See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404
|
||||
id: string;
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
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?: React.RefObject<
|
||||
(
|
||||
tileId: string,
|
||||
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => void
|
||||
>;
|
||||
}
|
||||
|
||||
export const VideoTileContainer: React.FC<Props> = React.memo(
|
||||
({ item, id, targetWidth, targetHeight, onDragRef, ...rest }) => {
|
||||
// Handle display name changes.
|
||||
const [displayName, setDisplayName] = React.useState<string>("[👻]");
|
||||
React.useEffect(() => {
|
||||
const member = item.member;
|
||||
|
||||
if (member) {
|
||||
setDisplayName(member.rawDisplayName);
|
||||
|
||||
const updateName = () => {
|
||||
setDisplayName(member.rawDisplayName);
|
||||
};
|
||||
|
||||
member!.on(RoomMemberEvent.Name, updateName);
|
||||
return () => {
|
||||
member!.removeListener(RoomMemberEvent.Name, updateName);
|
||||
};
|
||||
}
|
||||
}, [item.member]);
|
||||
|
||||
// Create an avatar.
|
||||
const avatar = (
|
||||
<Avatar
|
||||
key={item.member?.userId}
|
||||
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
|
||||
src={item.member?.getMxcAvatarUrl()}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
className={Styles.avatar}
|
||||
/>
|
||||
);
|
||||
|
||||
// Make sure that the tile is draggable and work well within video grid layout.
|
||||
//
|
||||
// Firefox doesn't respect the disablePictureInPicture attribute
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
||||
const tileRef = React.useRef<HTMLElement | null>(null);
|
||||
useDrag((state) => onDragRef?.current!(id, state), {
|
||||
target: tileRef,
|
||||
filterTaps: true,
|
||||
preventScroll: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<VideoTile
|
||||
ref={tileRef}
|
||||
sfuParticipant={item.sfuParticipant}
|
||||
content={item.content}
|
||||
name={displayName}
|
||||
avatar={avatar}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
Loading…
Add table
Reference in a new issue