diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 0e510a7a..5638b108 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -82,17 +82,21 @@ limitations under the License. bottom: 0; } -.avatar { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - /* To make avatars scale smoothly with their tiles during animations, we - override the styles set on the element */ - --avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2); - width: var(--avatarSize) !important; - height: var(--avatarSize) !important; - border-radius: 10000px !important; +/* CSS makes us put a condition here, even though all we want to do is +unconditionally select the container so we can use cqmin units */ +@container videoTile (width > 0) { + .avatar { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + /* To make avatars scale smoothly with their tiles during animations, we + override the styles set on the element */ + --avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */ + width: var(--avatarSize) !important; + height: var(--avatarSize) !important; + border-radius: 10000px !important; + } } @media (min-height: 300px) { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 3446d4ac..bf38693c 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -44,12 +44,7 @@ import { RoomHeaderInfo, VersionMismatchWarning, } from "../Header"; -import { - VideoGrid, - useVideoGridLayout, - ChildrenProperties, -} from "../video-grid/VideoGrid"; -import { VideoTileContainer } from "../video-grid/VideoTileContainer"; +import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid"; import { GroupCallInspector } from "./GroupCallInspector"; import { GridLayoutMenu } from "./GridLayoutMenu"; import { Avatar } from "../Avatar"; @@ -77,6 +72,7 @@ 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 @@ -303,7 +299,7 @@ export function InCallView({ } if (maximisedParticipant) { return ( - ); } @@ -325,17 +321,16 @@ export function InCallView({ layout={layout} disableAnimations={prefersReducedMotion || isSafari} > - {({ item, ...rest }: ChildrenProperties) => ( - ( + 2} + {...props} /> )} diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts index 3fab929d..8139c154 100644 --- a/src/useMergedRefs.ts +++ b/src/useMergedRefs.ts @@ -21,14 +21,14 @@ import { MutableRefObject, RefCallback, useCallback } from "react"; * same DOM node. */ export const useMergedRefs = ( - ...refs: (MutableRefObject | RefCallback)[] + ...refs: (MutableRefObject | RefCallback | null)[] ): RefCallback => useCallback( (value) => refs.forEach((ref) => { if (typeof ref === "function") { ref(value); - } else { + } else if (ref !== null) { ref.current = value; } }), diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 3e74cddb..7e7b916a 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -45,6 +45,7 @@ import { cycleTileSize, appendItems, } from "./model"; +import { TileWrapper } from "./TileWrapper"; interface GridState extends Grid { /** @@ -452,16 +453,19 @@ export const NewVideoGrid: FC = ({ > {slots} - {tileTransitions((style, tile) => - children({ - ...style, - key: tile.item.id, - targetWidth: tile.width, - targetHeight: tile.height, - item: tile.item, - onDragRef: onTileDragRef, - }) - )} + {tileTransitions((spring, tile) => ( + + {children} + + ))} ); }; diff --git a/src/video-grid/TileWrapper.tsx b/src/video-grid/TileWrapper.tsx new file mode 100644 index 00000000..6eca3ed9 --- /dev/null +++ b/src/video-grid/TileWrapper.tsx @@ -0,0 +1,101 @@ +/* +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 { TileDescriptor } from "./TileDescriptor"; +import { ChildrenProperties } from "./VideoGrid"; + +interface Props { + id: string; + onDragRef: RefObject< + ( + tileId: string, + state: Parameters>[0] + ) => void + >; + targetWidth: number; + targetHeight: number; + item: TileDescriptor; + opacity: SpringValue; + scale: SpringValue; + shadow: SpringValue; + shadowSpread: SpringValue; + zIndex: SpringValue; + x: SpringValue; + y: SpringValue; + width: SpringValue; + height: SpringValue; + children: (props: ChildrenProperties) => ReactNode; +} + +/** + * A wrapper around a tile in a video grid. This component exists to decouple + * child components from the grid. + */ +export const TileWrapper: FC = memo( + ({ + id, + onDragRef, + targetWidth, + targetHeight, + item, + opacity, + scale, + shadow, + shadowSpread, + zIndex, + x, + y, + width, + height, + children, + }) => { + const ref = useRef(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, + item, + })} + + ); + } +); diff --git a/src/video-grid/VideoGrid.stories.tsx b/src/video-grid/VideoGrid.stories.tsx deleted file mode 100644 index 859e593f..00000000 --- a/src/video-grid/VideoGrid.stories.tsx +++ /dev/null @@ -1,97 +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 { useMemo } from "react"; -import { RoomMember } from "matrix-js-sdk"; - -import { VideoGrid, useVideoGridLayout } from "./VideoGrid"; -import { VideoTile } from "./VideoTile"; -import { Button } from "../button"; -import { ConnectionState } from "../room/useGroupCall"; -import { TileDescriptor } from "./TileDescriptor"; - -export default { - title: "VideoGrid", - parameters: { - layout: "fullscreen", - }, -}; - -export const ParticipantsTest = () => { - const { layout, setLayout } = useVideoGridLayout(false); - const [participantCount, setParticipantCount] = useState(1); - - const items: TileDescriptor[] = useMemo( - () => - new Array(participantCount).fill(undefined).map((_, i) => ({ - id: (i + 1).toString(), - member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`), - focused: false, - presenter: false, - connectionState: ConnectionState.Connected, - })), - [participantCount] - ); - - return ( - <> -
- - {participantCount < 12 && ( - - )} - {participantCount > 0 && ( - - )} -
-
- - {({ item, ...rest }) => ( - - )} - -
- - ); -}; - -ParticipantsTest.args = { - layout: "freedom", - participantCount: 1, -}; diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 5fd6163d..0d70663b 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -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,21 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { Key, useCallback, useEffect, useRef, useState } from "react"; -import { FullGestureState, useDrag, useGesture } from "@use-gesture/react"; +import React, { + ComponentProps, + Key, + Ref, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { + EventTypes, + FullGestureState, + Handler, + 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 { TileDescriptor } from "./TileDescriptor"; +import { TileWrapper } from "./TileWrapper"; interface TilePosition { x: number; @@ -39,7 +52,7 @@ interface TilePosition { } interface Tile { - key: Key; + key: string; order: number; item: TileDescriptor; remove: boolean; @@ -717,20 +730,18 @@ interface DragTileData { y: number; } -export interface ChildrenProperties extends ReactDOMAttributes { - key: Key; +export interface ChildrenProperties { + ref: Ref; + style: ComponentProps["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; item: TileDescriptor; - opacity: SpringValue; - scale: SpringValue; - shadow: SpringValue; - zIndex: SpringValue; - x: SpringValue; - y: SpringValue; - width: SpringValue; - height: SpringValue; - [index: string]: unknown; } export interface VideoGridProps { @@ -1063,117 +1074,132 @@ export function VideoGrid({ [tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio] ); - const bindTile = useDrag( - ({ args: [key], active, xy, movement, tap, last, event }) => { - event.preventDefault(); + // Callback for useDrag. We could call useDrag here, but the default + // pattern of spreading {...bind()} across the children to bind the gesture + // ends up breaking memoization and ruining this component's performance. + // Instead, we pass this callback to each tile via a ref, to let them bind the + // gesture using the much more sensible ref-based method. + const onTileDrag = ( + tileId: string, + { + active, + xy, + movement, + tap, + last, + event, + }: Parameters>[0] + ) => { + event.preventDefault(); - if (tap) { - onTap(key); - return; - } + if (tap) { + onTap(tileId); + return; + } - if (layout !== "freedom") return; + if (layout !== "freedom") return; - const dragTileIndex = tiles.findIndex((tile) => tile.key === key); - const dragTile = tiles[dragTileIndex]; - const dragTilePosition = tilePositions[dragTile.order]; + const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId); + const dragTile = tiles[dragTileIndex]; + const dragTilePosition = tilePositions[dragTile.order]; - const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; + const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; - let newTiles = tiles; + let newTiles = tiles; - if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) { - // We're in 1:1 mode, so only the local tile should be draggable - if (!dragTile.item.isLocal) return; + if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) { + // We're in 1:1 mode, so only the local tile should be draggable + if (!dragTile.item.isLocal) return; - // Position should only update on the very last event, to avoid - // compounding the offset on every drag event - if (last) { - const remotePosition = tilePositions[1]; + // Position should only update on the very last event, to avoid + // compounding the offset on every drag event + if (last) { + const remotePosition = tilePositions[1]; - const pipGap = getPipGap( - gridBounds.width / gridBounds.height, - gridBounds.width - ); - const pipMinX = remotePosition.x + pipGap; - const pipMinY = remotePosition.y + pipGap; - const pipMaxX = - remotePosition.x + - remotePosition.width - - dragTilePosition.width - - pipGap; - const pipMaxY = - remotePosition.y + - remotePosition.height - - dragTilePosition.height - - pipGap; - - const newPipXRatio = - (dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX); - const newPipYRatio = - (dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY); - - setPipXRatio(Math.max(0, Math.min(1, newPipXRatio))); - setPipYRatio(Math.max(0, Math.min(1, newPipYRatio))); - } - } else { - const hoverTile = tiles.find( - (tile) => - tile.key !== key && - isInside(cursorPosition, tilePositions[tile.order]) + const pipGap = getPipGap( + gridBounds.width / gridBounds.height, + gridBounds.width ); + const pipMinX = remotePosition.x + pipGap; + const pipMinY = remotePosition.y + pipGap; + const pipMaxX = + remotePosition.x + + remotePosition.width - + dragTilePosition.width - + pipGap; + const pipMaxY = + remotePosition.y + + remotePosition.height - + dragTilePosition.height - + pipGap; - if (hoverTile) { - // Shift the tiles into their new order - newTiles = newTiles.map((tile) => { - let order = tile.order; - if (order < dragTile.order) { - if (order >= hoverTile.order) order++; - } else if (order > dragTile.order) { - if (order <= hoverTile.order) order--; - } else { - order = hoverTile.order; - } + const newPipXRatio = + (dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX); + const newPipYRatio = + (dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY); - let focused; - if (tile === hoverTile) { - focused = dragTile.focused; - } else if (tile === dragTile) { - focused = hoverTile.focused; - } else { - focused = tile.focused; - } - - return { ...tile, order, focused }; - }); - - reorderTiles(newTiles, layout); - - setTileState((state) => ({ ...state, tiles: newTiles })); - } + setPipXRatio(Math.max(0, Math.min(1, newPipXRatio))); + setPipYRatio(Math.max(0, Math.min(1, newPipYRatio))); } + } else { + const hoverTile = tiles.find( + (tile) => + tile.key !== tileId && + isInside(cursorPosition, tilePositions[tile.order]) + ); - if (active) { - if (!draggingTileRef.current) { - draggingTileRef.current = { - key: dragTile.key, - offsetX: dragTilePosition.x, - offsetY: dragTilePosition.y, - x: movement[0], - y: movement[1], - }; - } else { - draggingTileRef.current.x = movement[0]; - draggingTileRef.current.y = movement[1]; - } + if (hoverTile) { + // Shift the tiles into their new order + newTiles = newTiles.map((tile) => { + let order = tile.order; + if (order < dragTile.order) { + if (order >= hoverTile.order) order++; + } else if (order > dragTile.order) { + if (order <= hoverTile.order) order--; + } else { + order = hoverTile.order; + } + + let focused; + if (tile === hoverTile) { + focused = dragTile.focused; + } else if (tile === dragTile) { + focused = hoverTile.focused; + } else { + focused = tile.focused; + } + + return { ...tile, order, focused }; + }); + + reorderTiles(newTiles, layout); + + setTileState((state) => ({ ...state, tiles: newTiles })); + } + } + + if (active) { + if (!draggingTileRef.current) { + draggingTileRef.current = { + key: dragTile.key, + offsetX: dragTilePosition.x, + offsetY: dragTilePosition.y, + x: movement[0], + y: movement[1], + }; } else { - draggingTileRef.current = null; + draggingTileRef.current.x = movement[0]; + draggingTileRef.current.y = movement[1]; } + } else { + draggingTileRef.current = null; + } - api.start(animate(newTiles)); - }, - { filterTaps: true, pointer: { buttons: [1] } } - ); + api.start(animate(newTiles)); + }; + + const onTileDragRef = useRef(onTileDrag); + onTileDragRef.current = onTileDrag; const onGridGesture = useCallback( ( @@ -1220,18 +1246,23 @@ export function VideoGrid({ return (
- {springs.map((style, i) => { + {springs.map((spring, i) => { const tile = tiles[i]; const tilePosition = tilePositions[tile.order]; - return children({ - ...bindTile(tile.key), - ...style, - key: tile.item.id, - targetWidth: tilePosition.width, - targetHeight: tilePosition.height, - item: tile.item, - }); + return ( + + {children} + + ); })}
); diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index 8c2f7afb..1627160b 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -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; diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index b9a24539..2f92cb4c 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -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,86 +14,105 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ForwardedRef, forwardRef } from "react"; -import { animated, SpringValue } from "@react-spring/web"; +import React, { ComponentProps, forwardRef, useCallback } from "react"; +import { animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; import styles from "./VideoTile.module.css"; import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg"; import { AudioButton, FullscreenButton } from "../button/Button"; import { ConnectionState } from "../room/useGroupCall"; +import { TileDescriptor } from "./TileDescriptor"; +import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; +import { useCallFeed } from "./useCallFeed"; +import { useSpatialMediaStream } from "./useMediaStream"; +import { useRoomMemberName } from "./useRoomMemberName"; +import { useModalTriggerState } from "../Modal"; +import { useMergedRefs } from "../useMergedRefs"; interface Props { - name: string; - connectionState: ConnectionState; - speaking?: boolean; - audioMuted?: boolean; - videoMuted?: boolean; - screenshare?: boolean; - avatar?: JSX.Element; - mediaRef?: React.RefObject; - onOptionsPress?: () => void; - localVolume?: number; - hasAudio?: boolean; - maximised?: boolean; - fullscreen?: boolean; - onFullscreen?: () => void; + item: TileDescriptor; + maximised: boolean; + fullscreen: boolean; + onFullscreen: (participant: TileDescriptor) => void; className?: string; - showOptions?: boolean; - isLocal?: boolean; - disableSpeakingIndicator?: boolean; - opacity?: SpringValue; - scale?: SpringValue; - shadow?: SpringValue; - shadowSpread?: SpringValue; - zIndex?: SpringValue; - x?: SpringValue; - y?: SpringValue; - width?: SpringValue; - height?: SpringValue; + showSpeakingIndicator: boolean; + style?: ComponentProps["style"]; + targetWidth: number; + targetHeight: number; + getAvatar: ( + roomMember: RoomMember, + width: number, + height: number + ) => JSX.Element; + audioContext: AudioContext; + audioDestination: AudioNode; } export const VideoTile = forwardRef( ( { - name, - connectionState, - speaking, - audioMuted, - videoMuted, - screenshare, - avatar, - mediaRef, - onOptionsPress, - localVolume, - hasAudio, + item, maximised, fullscreen, onFullscreen, className, - showOptions, - isLocal, - // TODO: disableSpeakingIndicator is not used atm. - disableSpeakingIndicator, - opacity, - scale, - shadow, - shadowSpread, - zIndex, - x, - y, - width, - height, - ...rest + showSpeakingIndicator, + style, + targetWidth, + targetHeight, + getAvatar, + audioContext, + audioDestination, }, - ref + tileRef1 ) => { const { t } = useTranslation(); + const { + isLocal, + audioMuted, + videoMuted, + localVolume, + hasAudio, + speaking, + stream, + purpose, + } = useCallFeed(item.callFeed); + const screenshare = purpose === SDPStreamMetadataPurpose.Screenshare; + const { rawDisplayName: name } = useRoomMemberName(item.member); + + const [tileRef2, mediaRef] = useSpatialMediaStream( + stream ?? null, + audioContext, + audioDestination, + localVolume, + // The feed is muted if it's local audio (because we don't want our own audio, + // but it's a hook and we can't call it conditionally so we're stuck with it) + // or if there's a maximised feed in which case we always render audio via audio + // elements because we wire it up at the video tile container level and only one + // video tile container is displayed. + isLocal || maximised + ); + + const tileRef = useMergedRefs(tileRef1, tileRef2); + + const { + modalState: videoTileSettingsModalState, + modalProps: videoTileSettingsModalProps, + } = useModalTriggerState(); + const onOptionsPress = videoTileSettingsModalState.open; + + const onFullscreenCallback = useCallback(() => { + onFullscreen(item); + }, [onFullscreen, item]); + const toolbarButtons: JSX.Element[] = []; - if (connectionState == ConnectionState.Connected && !isLocal) { + if (item.connectionState == ConnectionState.Connected && !isLocal) { if (hasAudio) { toolbarButtons.push( ( key="fullscreen" className={styles.button} fullscreen={fullscreen} - onPress={onFullscreen} + onPress={onFullscreenCallback} /> ); } } let caption: string; - switch (connectionState) { + switch (item.connectionState) { case ConnectionState.EstablishingCall: caption = t("{{name}} (Connecting...)", { name }); break; @@ -131,68 +150,65 @@ export const VideoTile = forwardRef( break; } + // Firefox doesn't respect the disablePictureInPicture attribute + // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 + return ( - `${w}px`), - "--tileHeight": height?.to((h) => `${h}px`), - "--tileShadow": shadow?.to((s) => `${s}px`), - "--tileShadowSpread": shadowSpread?.to((s) => `${s}px`), - }} - ref={ref as ForwardedRef} - data-testid="videoTile" - {...rest} - > - {toolbarButtons.length > 0 && !maximised && ( -
{toolbarButtons}
- )} - {videoMuted && ( - <> -
- {avatar} - - )} - {!maximised && - (screenshare ? ( -
- {t("{{name}} is presenting", { name })} -
- ) : ( -
- { - /* If the user is speaking, it's safe to say they're unmuted. + <> + + {toolbarButtons.length > 0 && !maximised && ( +
{toolbarButtons}
+ )} + {videoMuted && ( + <> +
+ {getAvatar(item.member, targetWidth, targetHeight)} + + )} + {!maximised && + (screenshare ? ( +
+ {t("{{name}} is presenting", { name })} +
+ ) : ( +
+ { + /* If the user is speaking, it's safe to say they're unmuted. Mute state is currently sent over to-device messages, which aren't quite real-time, so this is an important kludge to make sure no one appears muted when they've clearly begun talking. */ - speaking || !audioMuted ? : - } - - {caption} - -
- ))} -
+ ))} +
+ {videoTileSettingsModalState.isOpen && !maximised && item.callFeed && ( + + )} + ); } ); diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx deleted file mode 100644 index f3f50597..00000000 --- a/src/video-grid/VideoTileContainer.tsx +++ /dev/null @@ -1,157 +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 { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; -import React, { FC, memo, RefObject } from "react"; -import { useCallback } 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 { useCallFeed } from "./useCallFeed"; -import { useSpatialMediaStream } from "./useMediaStream"; -import { useRoomMemberName } from "./useRoomMemberName"; -import { VideoTile } from "./VideoTile"; -import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; -import { useModalTriggerState } from "../Modal"; -import { TileDescriptor } from "./TileDescriptor"; - -interface Props { - item: TileDescriptor; - targetWidth: number; - targetHeight: number; - getAvatar: ( - roomMember: RoomMember, - width: number, - height: number - ) => JSX.Element; - audioContext: AudioContext; - audioDestination: AudioNode; - disableSpeakingIndicator: boolean; - maximised: boolean; - fullscreen: boolean; - onFullscreen: (item: TileDescriptor) => void; - opacity?: SpringValue; - scale?: SpringValue; - shadow?: SpringValue; - shadowSpread?: SpringValue; - zIndex?: SpringValue; - x?: SpringValue; - y?: SpringValue; - width?: SpringValue; - height?: SpringValue; - onDragRef?: RefObject< - ( - tileId: string, - state: Parameters>[0] - ) => void - >; -} - -export const VideoTileContainer: FC = memo( - ({ - item, - targetWidth, - targetHeight, - getAvatar, - audioContext, - audioDestination, - disableSpeakingIndicator, - maximised, - fullscreen, - onFullscreen, - onDragRef, - ...rest - }) => { - const { - isLocal, - audioMuted, - videoMuted, - localVolume, - hasAudio, - speaking, - stream, - purpose, - } = useCallFeed(item.callFeed); - const { rawDisplayName } = useRoomMemberName(item.member); - - const [tileRef, mediaRef] = useSpatialMediaStream( - stream ?? null, - audioContext, - audioDestination, - localVolume, - // The feed is muted if it's local audio (because we don't want our own audio, - // but it's a hook and we can't call it conditionally so we're stuck with it) - // or if there's a maximised feed in which case we always render audio via audio - // elements because we wire it up at the video tile container level and only one - // video tile container is displayed. - isLocal || maximised - ); - - useDrag((state) => onDragRef?.current!(item.id, state), { - target: tileRef, - filterTaps: true, - preventScroll: true, - }); - - const { - modalState: videoTileSettingsModalState, - modalProps: videoTileSettingsModalProps, - } = useModalTriggerState(); - const onOptionsPress = () => { - videoTileSettingsModalState.open(); - }; - - const onFullscreenCallback = useCallback(() => { - onFullscreen(item); - }, [onFullscreen, item]); - - // Firefox doesn't respect the disablePictureInPicture attribute - // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 - - return ( - <> - - {videoTileSettingsModalState.isOpen && !maximised && item.callFeed && ( - - )} - - ); - } -);