diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx
index 3e6a62c..14cac9c 100644
--- a/src/room/InCallView.tsx
+++ b/src/room/InCallView.tsx
@@ -14,17 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useEffect, useCallback, useMemo, useRef } from "react";
-import { usePreventScroll } from "@react-aria/overlays";
-import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
-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 classNames from "classnames";
-import { useTranslation } from "react-i18next";
-import { JoinRule } from "matrix-js-sdk/src/@types/partials";
-import { Room, Track } from "livekit-client";
import {
useLiveKitRoom,
useLocalParticipant,
@@ -32,15 +22,19 @@ import {
useToken,
useTracks,
} from "@livekit/components-react";
+import { usePreventScroll } from "@react-aria/overlays";
+import classNames from "classnames";
+import { Room, Track } from "livekit-client";
+import { JoinRule } from "matrix-js-sdk/src/@types/partials";
+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 { useTranslation } from "react-i18next";
+import useMeasure from "react-use-measure";
import type { IWidgetApiRequest } from "matrix-widget-api";
-import styles from "./InCallView.module.css";
-import {
- HangupButton,
- MicButton,
- VideoButton,
- ScreenshareButton,
-} from "../button";
+import { Avatar } from "../Avatar";
import {
Header,
LeftNav,
@@ -48,27 +42,36 @@ import {
RoomHeaderInfo,
VersionMismatchWarning,
} from "../Header";
-import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
-import { VideoTileContainer } from "../video-grid/VideoTileContainer";
-import { GroupCallInspector } from "./GroupCallInspector";
-import { OverflowMenu } from "./OverflowMenu";
-import { GridLayoutMenu } from "./GridLayoutMenu";
-import { Avatar } from "../Avatar";
-import { UserMenuContainer } from "../UserMenuContainer";
-import { useRageshakeRequestModal } from "../settings/submit-rageshake";
-import { RageshakeRequestModal } from "./RageshakeRequestModal";
-import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../PosthogAnalytics";
-import { widget, ElementWidgetActions } from "../widget";
-import { useJoinRule } from "./useJoinRule";
import { useUrlParams } from "../UrlParams";
-import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
-import { ParticipantInfo } from "./useGroupCall";
-import { TileDescriptor } from "../video-grid/TileDescriptor";
-import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
+import { UserMenuContainer } from "../UserMenuContainer";
+import {
+ HangupButton,
+ MicButton,
+ ScreenshareButton,
+ VideoButton,
+} from "../button";
import { MediaDevicesState } from "../settings/mediaDevices";
+import { useRageshakeRequestModal } from "../settings/submit-rageshake";
+import { useShowInspector } from "../settings/useSetting";
+import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
+import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
+import {
+ TileDescriptor,
+ VideoGrid,
+ useVideoGridLayout,
+} from "../video-grid/VideoGrid";
+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 { OverflowMenu } from "./OverflowMenu";
+import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { MatrixInfo } from "./VideoPreview";
+import { useJoinRule } from "./useJoinRule";
+import { ParticipantInfo } from "./useGroupCall";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -228,46 +231,14 @@ export function InCallView({
}
}, [setLayout]);
- const sfuParticipants = useParticipants({
- room: livekitRoom,
- });
-
- const items = useMemo(() => {
- const localUserId = client.getUserId()!;
- const localDeviceId = client.getDeviceId()!;
-
- // One tile for each participant, to start with (we want a tile for everyone we
- // think should be in the call, even if we don't have a call feed for them yet)
- const tileDescriptors: TileDescriptor[] = [];
- for (const [member, participantMap] of participants) {
- for (const [deviceId, { presenter }] of participantMap) {
- const id = `${member.userId}:${deviceId}`;
- const sfuParticipant = sfuParticipants.find((p) => p.identity === id);
-
- const hasScreenShare =
- sfuParticipant?.getTrack(Track.Source.ScreenShare) !== undefined;
-
- tileDescriptors.push({
- id,
- member,
- focused: hasScreenShare && !sfuParticipant?.isLocal,
- isLocal: member.userId == localUserId && deviceId == localDeviceId,
- presenter,
- sfuParticipant,
- });
- }
- }
-
- PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
- tileDescriptors.length
- );
-
- return tileDescriptors;
- }, [client, participants, sfuParticipants]);
-
const reducedControls = boundsValid && bounds.width <= 400;
const noControls = reducedControls && bounds.height <= 400;
+ const items = useParticipantTiles(livekitRoom, participants, {
+ userId: client.getUserId()!,
+ deviceId: client.getDeviceId()!,
+ });
+
// The maximised participant: the focused (active) participant if the
// window is too small to show everyone.
const maximisedParticipant = useMemo(
@@ -309,7 +280,7 @@ export function InCallView({
height={bounds.height}
width={bounds.width}
key={maximisedParticipant.id}
- item={maximisedParticipant}
+ item={maximisedParticipant.data}
getAvatar={renderAvatar}
maximised={Boolean(maximisedParticipant)}
/>
@@ -322,19 +293,12 @@ export function InCallView({
layout={layout}
disableAnimations={prefersReducedMotion || isSafari}
>
- {({
- item,
- ...rest
- }: {
- item: TileDescriptor;
- [x: string]: unknown;
- }) => (
+ {(child) => (
)}
@@ -424,3 +388,53 @@ export function InCallView({
);
}
+
+interface ParticipantID {
+ userId: string;
+ deviceId: string;
+}
+
+function useParticipantTiles(
+ livekitRoom: Room,
+ participants: Map>,
+ local: ParticipantID
+): TileDescriptor[] {
+ const sfuParticipants = useParticipants({
+ room: livekitRoom,
+ });
+
+ const [localUserId, localDeviceId] = [local.userId, local.deviceId];
+
+ const items = useMemo(() => {
+ const tiles: TileDescriptor[] = [];
+ for (const [member, participantMap] of participants) {
+ for (const [deviceId] of participantMap) {
+ const id = `${member.userId}:${deviceId}`;
+ const sfuParticipant = sfuParticipants.find((p) => p.identity === id);
+
+ const hasScreenShare =
+ sfuParticipant?.getTrack(Track.Source.ScreenShare) !== undefined;
+
+ const descriptor = {
+ id,
+ focused: hasScreenShare && !sfuParticipant?.isLocal,
+ local: member.userId == localUserId && deviceId == localDeviceId,
+ data: {
+ member,
+ sfuParticipant,
+ },
+ };
+
+ tiles.push(descriptor);
+ }
+ }
+
+ PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
+ tiles.length
+ );
+
+ return tiles;
+ }, [localUserId, localDeviceId, participants, sfuParticipants]);
+
+ return items;
+}
diff --git a/src/video-grid/TileDescriptor.tsx b/src/video-grid/TileDescriptor.tsx
deleted file mode 100644
index c5533ac..0000000
--- a/src/video-grid/TileDescriptor.tsx
+++ /dev/null
@@ -1,29 +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 { RoomMember } from "matrix-js-sdk";
-import { LocalParticipant, RemoteParticipant } from "livekit-client";
-
-// Represents something that should get a tile on the layout,
-// ie. a user's video feed or a screen share feed.
-export interface TileDescriptor {
- id: string;
- member: RoomMember;
- focused: boolean;
- presenter: boolean;
- isLocal?: boolean;
- sfuParticipant?: LocalParticipant | RemoteParticipant;
-}
diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx
index 70633e9..21225cd 100644
--- a/src/video-grid/VideoGrid.tsx
+++ b/src/video-grid/VideoGrid.tsx
@@ -23,7 +23,6 @@ import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/typ
import styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu";
-import { TileDescriptor } from "./TileDescriptor";
interface TilePosition {
x: number;
@@ -33,13 +32,12 @@ interface TilePosition {
zIndex: number;
}
-interface Tile {
+interface Tile {
key: Key;
order: number;
- item: TileDescriptor;
+ item: TileDescriptor;
remove: boolean;
focused: boolean;
- presenter: boolean;
}
type LayoutDirection = "vertical" | "horizontal";
@@ -112,7 +110,6 @@ const getPipGap = (gridAspectRatio: number, gridWidth: number): number =>
function getTilePositions(
tileCount: number,
focusedTileCount: number,
- hasPresenter: boolean,
gridWidth: number,
gridHeight: number,
pipXRatio: number,
@@ -120,7 +117,7 @@ function getTilePositions(
layout: Layout
): TilePosition[] {
if (layout === "freedom") {
- if (tileCount === 2 && !hasPresenter && focusedTileCount === 0) {
+ if (tileCount === 2 && focusedTileCount === 0) {
return getOneOnOneLayoutTilePositions(
gridWidth,
gridHeight,
@@ -654,7 +651,7 @@ function getSubGridPositions(
// Sets the 'order' property on tiles based on the layout param and
// other properties of the tiles, eg. 'focused' and 'presenter'
-function reorderTiles(tiles: Tile[], layout: Layout) {
+function reorderTiles(tiles: Tile[], layout: Layout) {
// We use a special layout for 1:1 to always put the local tile first.
// We only do this if there are two tiles (obviously) and exactly one
// of them is local: during startup we can have tiles from other users
@@ -664,16 +661,16 @@ function reorderTiles(tiles: Tile[], layout: Layout) {
if (
layout === "freedom" &&
tiles.length === 2 &&
- tiles.filter((t) => t.item.isLocal).length === 1 &&
- !tiles.some((t) => t.presenter || t.focused)
+ tiles.filter((t) => t.item.local).length === 1 &&
+ !tiles.some((t) => t.focused)
) {
// 1:1 layout
- tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
+ tiles.forEach((tile) => (tile.order = tile.item.local ? 0 : 1));
} else {
- const focusedTiles: Tile[] = [];
- const otherTiles: Tile[] = [];
+ const focusedTiles: Tile[] = [];
+ const otherTiles: Tile[] = [];
- const orderedTiles: Tile[] = new Array(tiles.length);
+ const orderedTiles: Tile[] = new Array(tiles.length);
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
orderedTiles.forEach((tile) =>
@@ -692,8 +689,9 @@ interface DragTileData {
y: number;
}
-interface ChildrenProperties extends ReactDOMAttributes {
+interface ChildrenProperties extends ReactDOMAttributes {
key: Key;
+ data: T;
style: {
scale: SpringValue;
opacity: SpringValue;
@@ -701,29 +699,36 @@ interface ChildrenProperties extends ReactDOMAttributes {
};
width: number;
height: number;
- item: TileDescriptor;
- [index: string]: unknown;
}
-interface VideoGridProps {
- items: TileDescriptor[];
+interface VideoGridProps {
+ items: TileDescriptor[];
layout: Layout;
- disableAnimations?: boolean;
- children: (props: ChildrenProperties) => React.ReactNode;
+ disableAnimations: boolean;
+ children: (props: ChildrenProperties) => React.ReactNode;
}
-export function VideoGrid({
+// Represents something that should get a tile on the layout,
+// ie. a user's video feed or a screen share feed.
+export interface TileDescriptor {
+ id: string;
+ focused: boolean;
+ local: boolean;
+ data: T;
+}
+
+export function VideoGrid({
items,
layout,
disableAnimations,
- children,
-}: VideoGridProps) {
+ children: createChild,
+}: VideoGridProps) {
// Place the PiP in the bottom right corner by default
const [pipXRatio, setPipXRatio] = useState(1);
const [pipYRatio, setPipYRatio] = useState(1);
const [{ tiles, tilePositions }, setTileState] = useState<{
- tiles: Tile[];
+ tiles: Tile[];
tilePositions: TilePosition[];
}>({
tiles: [],
@@ -739,7 +744,7 @@ export function VideoGrid({
useEffect(() => {
setTileState(({ tiles, ...rest }) => {
- const newTiles: Tile[] = [];
+ const newTiles: Tile[] = [];
const removedTileKeys: Set = new Set();
for (const tile of tiles) {
@@ -766,7 +771,6 @@ export function VideoGrid({
item,
remove,
focused,
- presenter: item.presenter,
});
}
@@ -781,13 +785,12 @@ export function VideoGrid({
continue;
}
- const newTile: Tile = {
+ const newTile: Tile = {
key: item.id,
order: existingTile?.order ?? newTiles.length,
item,
remove: false,
focused: layout === "spotlight" && item.focused,
- presenter: item.presenter,
};
if (existingTile) {
@@ -808,7 +811,7 @@ export function VideoGrid({
}
setTileState(({ tiles, ...rest }) => {
- const newTiles: Tile[] = tiles
+ const newTiles: Tile[] = tiles
.filter((tile) => !removedTileKeys.has(tile.key))
.map((tile) => ({ ...tile })); // clone before reordering
reorderTiles(newTiles, layout);
@@ -824,7 +827,6 @@ export function VideoGrid({
tilePositions: getTilePositions(
newTiles.length,
focusedTileCount,
- newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -849,7 +851,6 @@ export function VideoGrid({
tilePositions: getTilePositions(
newTiles.length,
focusedTileCount,
- newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -863,7 +864,7 @@ export function VideoGrid({
const tilePositionsValid = useRef(false);
const animate = useCallback(
- (tiles: Tile[]) => {
+ (tiles: Tile[]) => {
// Whether the tile positions were valid at the time of the previous
// animation
const tilePositionsWereValid = tilePositionsValid.current;
@@ -1005,7 +1006,6 @@ export function VideoGrid({
tilePositions: getTilePositions(
newTiles.length,
focusedTileCount,
- newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -1037,9 +1037,9 @@ export function VideoGrid({
let newTiles = tiles;
- if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
+ if (tiles.length === 2 && !tiles.some((t) => t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable
- if (!dragTile.item.isLocal) return;
+ if (!dragTile.item.local) return;
// Position should only update on the very last event, to avoid
// compounding the offset on every drag event
@@ -1179,9 +1179,10 @@ export function VideoGrid({
const tile = tiles[i];
const tilePosition = tilePositions[tile.order];
- return children({
+ return createChild({
...bindTile(tile.key),
key: tile.key,
+ data: tile.item.data,
style: {
boxShadow: shadow.to(
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
@@ -1190,7 +1191,6 @@ export function VideoGrid({
},
width: tilePosition.width,
height: tilePosition.height,
- item: tile.item,
});
})}
diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx
index 6704861..86cceaa 100644
--- a/src/video-grid/VideoTileContainer.tsx
+++ b/src/video-grid/VideoTileContainer.tsx
@@ -16,13 +16,18 @@ limitations under the License.
import React from "react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { LocalParticipant, RemoteParticipant } from "livekit-client";
import { useRoomMemberName } from "./useRoomMemberName";
import { VideoTile } from "./VideoTile";
-import { TileDescriptor } from "./TileDescriptor";
+
+export interface ItemData {
+ member: RoomMember;
+ sfuParticipant?: LocalParticipant | RemoteParticipant;
+}
interface Props {
- item: TileDescriptor;
+ item: ItemData;
width?: number;
height?: number;
getAvatar: (