mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-30 19:39:22 +00:00
WIP
This commit is contained in:
@@ -245,6 +245,23 @@ export function SettingsButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SimulateJoinButton(props: {
|
||||||
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => "Add a fake participant", [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button variant="toolbar" {...props}>
|
||||||
|
<AddUserIcon width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function InviteButton({
|
export function InviteButton({
|
||||||
className,
|
className,
|
||||||
variant = "toolbar",
|
variant = "toolbar",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
ScreenshareButton,
|
ScreenshareButton,
|
||||||
SettingsButton,
|
SettingsButton,
|
||||||
InviteButton,
|
InviteButton,
|
||||||
|
SimulateJoinButton,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import {
|
import {
|
||||||
Header,
|
Header,
|
||||||
@@ -58,6 +59,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useShowInspector,
|
useShowInspector,
|
||||||
useShowConnectionStats,
|
useShowConnectionStats,
|
||||||
|
useVideoGridSandboxMode,
|
||||||
} from "../settings/useSetting";
|
} from "../settings/useSetting";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
@@ -83,6 +85,8 @@ import { useFullscreen } from "./useFullscreen";
|
|||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
||||||
import { E2EELock } from "../E2EELock";
|
import { E2EELock } from "../E2EELock";
|
||||||
|
import { useFakeTiles } from "./useFakeTiles";
|
||||||
|
import { FakeItemData, FakeVideoTile } from "../video-grid/FakeVideoTile";
|
||||||
|
|
||||||
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
|
||||||
@@ -160,6 +164,9 @@ export function InCallView({
|
|||||||
|
|
||||||
const [showInspector] = useShowInspector();
|
const [showInspector] = useShowInspector();
|
||||||
const [showConnectionStats] = useShowConnectionStats();
|
const [showConnectionStats] = useShowConnectionStats();
|
||||||
|
const [videoGridSandboxMode] = useVideoGridSandboxMode()
|
||||||
|
|
||||||
|
const { fakeTiles: fakeItems, simulateJoin } = useFakeTiles()
|
||||||
|
|
||||||
const { hideScreensharing } = useUrlParams();
|
const { hideScreensharing } = useUrlParams();
|
||||||
|
|
||||||
@@ -227,9 +234,11 @@ export function InCallView({
|
|||||||
const reducedControls = boundsValid && bounds.width <= 400;
|
const reducedControls = boundsValid && bounds.width <= 400;
|
||||||
const noControls = reducedControls && bounds.height <= 400;
|
const noControls = reducedControls && bounds.height <= 400;
|
||||||
|
|
||||||
const items = useParticipantTiles(livekitRoom, participants);
|
const realItems = useParticipantTiles(livekitRoom, participants);
|
||||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||||
useFullscreen(items);
|
useFullscreen(realItems);
|
||||||
|
|
||||||
|
const items = useMemo<TileDescriptor<ItemData | FakeItemData>[]>(() => videoGridSandboxMode ? [...realItems, ...fakeItems] : realItems, [videoGridSandboxMode, realItems, fakeItems])
|
||||||
|
|
||||||
// The maximised participant: either the participant that the user has
|
// The maximised participant: either the participant that the user has
|
||||||
// manually put in fullscreen, or the focused (active) participant if the
|
// manually put in fullscreen, or the focused (active) participant if the
|
||||||
@@ -238,9 +247,9 @@ export function InCallView({
|
|||||||
() =>
|
() =>
|
||||||
fullscreenItem ??
|
fullscreenItem ??
|
||||||
(noControls
|
(noControls
|
||||||
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
|
? realItems.find((item) => item.isSpeaker) ?? realItems.at(0) ?? null
|
||||||
: null),
|
: null),
|
||||||
[fullscreenItem, noControls, items]
|
[fullscreenItem, noControls, realItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
const Grid =
|
const Grid =
|
||||||
@@ -283,16 +292,19 @@ export function InCallView({
|
|||||||
disableAnimations={prefersReducedMotion || isSafari}
|
disableAnimations={prefersReducedMotion || isSafari}
|
||||||
layoutStates={layoutStates}
|
layoutStates={layoutStates}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => props.data.type === "real" ? (
|
||||||
<VideoTile
|
<VideoTile
|
||||||
maximised={false}
|
maximised={false}
|
||||||
fullscreen={false}
|
fullscreen={false}
|
||||||
onToggleFullscreen={toggleFullscreen}
|
onToggleFullscreen={toggleFullscreen}
|
||||||
showSpeakingIndicator={items.length > 2}
|
showSpeakingIndicator={realItems.length > 2}
|
||||||
showConnectionStats={showConnectionStats}
|
showConnectionStats={showConnectionStats}
|
||||||
{...props}
|
{...props}
|
||||||
|
data={props.data}
|
||||||
ref={props.ref as Ref<HTMLDivElement>}
|
ref={props.ref as Ref<HTMLDivElement>}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<FakeVideoTile {...props} data={props.data} />
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
@@ -381,6 +393,8 @@ export function InCallView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||||
|
if (videoGridSandboxMode)
|
||||||
|
buttons.push(<SimulateJoinButton key="5" onPress={simulateJoin} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
@@ -480,7 +494,7 @@ function useParticipantTiles(
|
|||||||
const member = matrixParticipants.get(id);
|
const member = matrixParticipants.get(id);
|
||||||
allGhosts &&= member === undefined;
|
allGhosts &&= member === undefined;
|
||||||
|
|
||||||
const userMediaTile = {
|
const userMediaTile: TileDescriptor<ItemData> = {
|
||||||
id,
|
id,
|
||||||
focused: false,
|
focused: false,
|
||||||
isPresenter: sfuParticipant.isScreenShareEnabled,
|
isPresenter: sfuParticipant.isScreenShareEnabled,
|
||||||
@@ -491,6 +505,7 @@ function useParticipantTiles(
|
|||||||
local: sfuParticipant.isLocal,
|
local: sfuParticipant.isLocal,
|
||||||
largeBaseSize: false,
|
largeBaseSize: false,
|
||||||
data: {
|
data: {
|
||||||
|
type: "real",
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
sfuParticipant,
|
sfuParticipant,
|
||||||
|
|||||||
96
src/room/useFakeTiles.ts
Normal file
96
src/room/useFakeTiles.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
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 { TileDescriptor } from '../video-grid/VideoGrid'
|
||||||
|
import { FakeItemData } from '../video-grid/FakeVideoTile'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { useLatest } from '../useLatest'
|
||||||
|
import { generateRandomName } from '../auth/generateRandomName'
|
||||||
|
|
||||||
|
interface FakeTiles {
|
||||||
|
fakeTiles: TileDescriptor<FakeItemData>[]
|
||||||
|
simulateJoin: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFakeTiles = (): FakeTiles => {
|
||||||
|
const [tiles, setTiles] = useState<TileDescriptor<FakeItemData>[]>([])
|
||||||
|
const latestSetTiles = useLatest(setTiles)
|
||||||
|
|
||||||
|
const simulateJoin = useCallback(() => {
|
||||||
|
const name = generateRandomName()
|
||||||
|
const newTile: TileDescriptor<FakeItemData> = {
|
||||||
|
id: name,
|
||||||
|
focused: false,
|
||||||
|
isPresenter: false,
|
||||||
|
isSpeaker: false,
|
||||||
|
hasVideo: false,
|
||||||
|
local: false,
|
||||||
|
largeBaseSize: false,
|
||||||
|
data: {
|
||||||
|
type: "fake",
|
||||||
|
name,
|
||||||
|
screenshare: false,
|
||||||
|
speaking: false,
|
||||||
|
simulateScreenshare: () => latestSetTiles.current(ts => {
|
||||||
|
if (ts.some(t => t.data.name === newTile.id && t.data.screenshare)) {
|
||||||
|
// No-op since they're already screensharing
|
||||||
|
return ts
|
||||||
|
} else {
|
||||||
|
const newScreenshareTile: TileDescriptor<FakeItemData> = {
|
||||||
|
id: `${name}:screenshare`,
|
||||||
|
focused: false,
|
||||||
|
isPresenter: false,
|
||||||
|
isSpeaker: false,
|
||||||
|
hasVideo: true,
|
||||||
|
local: false,
|
||||||
|
largeBaseSize: true,
|
||||||
|
placeNear: newTile.id,
|
||||||
|
data: {
|
||||||
|
type: "fake",
|
||||||
|
name,
|
||||||
|
screenshare: true,
|
||||||
|
speaking: false,
|
||||||
|
remove: () => latestSetTiles.current(ts => ts.filter(t => t.id !== newScreenshareTile.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...ts, newScreenshareTile]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
simulateSpeaking: () => latestSetTiles.current(ts => ts.map(t => {
|
||||||
|
if (t.id === newTile.id) {
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
isSpeaker: !t.isSpeaker,
|
||||||
|
data: {
|
||||||
|
...t.data,
|
||||||
|
speaking: !t.isSpeaker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
remove: () => latestSetTiles.current(ts => ts.filter(t => t.id !== newTile.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTiles(ts => [...ts, newTile])
|
||||||
|
}, [setTiles, latestSetTiles])
|
||||||
|
|
||||||
|
return {
|
||||||
|
fakeTiles: tiles,
|
||||||
|
simulateJoin,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,12 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { useCallback, useLayoutEffect, useRef } from "react";
|
import { useCallback, useLayoutEffect } from "react";
|
||||||
|
|
||||||
import { TileDescriptor } from "../video-grid/VideoGrid";
|
import { TileDescriptor } from "../video-grid/VideoGrid";
|
||||||
import { useReactiveState } from "../useReactiveState";
|
import { useReactiveState } from "../useReactiveState";
|
||||||
import { useEventTarget } from "../useEvents";
|
import { useEventTarget } from "../useEvents";
|
||||||
|
import { useLatest } from "../useLatest";
|
||||||
|
|
||||||
const isFullscreen = () =>
|
const isFullscreen = () =>
|
||||||
Boolean(document.fullscreenElement) ||
|
Boolean(document.fullscreenElement) ||
|
||||||
@@ -69,11 +70,8 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
|||||||
[items]
|
[items]
|
||||||
);
|
);
|
||||||
|
|
||||||
const latestItems = useRef<TileDescriptor<T>[]>(items);
|
const latestItems = useLatest(items)
|
||||||
latestItems.current = items;
|
const latestFullscreenItem = useLatest(fullscreenItem)
|
||||||
|
|
||||||
const latestFullscreenItem = useRef<TileDescriptor<T> | null>(fullscreenItem);
|
|
||||||
latestFullscreenItem.current = fullscreenItem;
|
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(
|
const toggleFullscreen = useCallback(
|
||||||
(itemId: string) => {
|
(itemId: string) => {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
useOptInAnalytics,
|
useOptInAnalytics,
|
||||||
useDeveloperSettingsTab,
|
useDeveloperSettingsTab,
|
||||||
useShowConnectionStats,
|
useShowConnectionStats,
|
||||||
|
useVideoGridSandboxMode,
|
||||||
} from "./useSetting";
|
} from "./useSetting";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
useDeveloperSettingsTab();
|
useDeveloperSettingsTab();
|
||||||
const [showConnectionStats, setShowConnectionStats] =
|
const [showConnectionStats, setShowConnectionStats] =
|
||||||
useShowConnectionStats();
|
useShowConnectionStats();
|
||||||
|
const [videoGridSandboxMode, setVideoGridSandboxMode] = useVideoGridSandboxMode()
|
||||||
|
|
||||||
const downloadDebugLog = useDownloadDebugLog();
|
const downloadDebugLog = useDownloadDebugLog();
|
||||||
|
|
||||||
@@ -249,6 +251,18 @@ export const SettingsModal = (props: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="videoGridSandboxMode"
|
||||||
|
name="video-grid-sandbox-mode"
|
||||||
|
label="Video grid sandbox mode"
|
||||||
|
type="checkbox"
|
||||||
|
checked={videoGridSandboxMode}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setVideoGridSandboxMode(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export const useDeveloperSettingsTab = () =>
|
|||||||
export const useShowConnectionStats = () =>
|
export const useShowConnectionStats = () =>
|
||||||
useSetting("show-connection-stats", false);
|
useSetting("show-connection-stats", false);
|
||||||
|
|
||||||
|
export const useVideoGridSandboxMode = () => useSetting("video-grid-sandbox-mode", false)
|
||||||
|
|
||||||
export const useDefaultDevices = () =>
|
export const useDefaultDevices = () =>
|
||||||
useSetting("defaultDevices", {
|
useSetting("defaultDevices", {
|
||||||
audioinput: "",
|
audioinput: "",
|
||||||
|
|||||||
27
src/useLatest.ts
Normal file
27
src/useLatest.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
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 { useRef } from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a ref that always holds the latest value passed to this hook.
|
||||||
|
*/
|
||||||
|
export const useLatest = <T,>(t: T): { current: T } => {
|
||||||
|
const latest = useRef(t)
|
||||||
|
latest.current = t
|
||||||
|
return latest
|
||||||
|
}
|
||||||
42
src/video-grid/FakeVideoTile.module.css
Normal file
42
src/video-grid/FakeVideoTile.module.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
background-color: var(--system);
|
||||||
|
--tileRadius: 20px;
|
||||||
|
border-radius: var(--tileRadius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaking {
|
||||||
|
border: 4px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: var(--font-size-caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
55
src/video-grid/FakeVideoTile.tsx
Normal file
55
src/video-grid/FakeVideoTile.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
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 { animated } from "@react-spring/web";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { ComponentProps, forwardRef, Ref } from "react"
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||||
|
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import styles from "./FakeVideoTile.module.css";
|
||||||
|
|
||||||
|
|
||||||
|
export interface FakeItemData {
|
||||||
|
type: "fake", // To differentiate from ItemData
|
||||||
|
name: string,
|
||||||
|
screenshare: boolean,
|
||||||
|
speaking: boolean,
|
||||||
|
simulateScreenshare?: () => void
|
||||||
|
simulateSpeaking?: () => void
|
||||||
|
remove: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: FakeItemData
|
||||||
|
className?: string;
|
||||||
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FakeVideoTile = forwardRef<HTMLElement, Props>(({ data, className, style }, ref) => {
|
||||||
|
return <animated.div className={classNames(className, styles.tile, { [styles.speaking]: data.speaking })} ref={ref as Ref<HTMLDivElement>} style={style}>
|
||||||
|
<Avatar fallback={data.name[0].toUpperCase()} />
|
||||||
|
<div className={styles.name}>
|
||||||
|
{data.screenshare ? `${data.name} (sharing screen)` : data.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
{data.simulateSpeaking && <MicIcon onClick={data.simulateSpeaking} />}
|
||||||
|
{data.simulateScreenshare && <ScreenshareIcon onClick={data.simulateScreenshare} />}
|
||||||
|
<HangupIcon onClick={data.remove} />
|
||||||
|
</div>
|
||||||
|
</animated.div>
|
||||||
|
})
|
||||||
23
src/video-grid/TileWrapper.module.css
Normal file
23
src/video-grid/TileWrapper.module.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
position: absolute;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
|||||||
import { SpringValue, to } from "@react-spring/web";
|
import { SpringValue, to } from "@react-spring/web";
|
||||||
|
|
||||||
import { ChildrenProperties } from "./VideoGrid";
|
import { ChildrenProperties } from "./VideoGrid";
|
||||||
|
import styles from "./TileWrapper.module.css"
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -77,6 +78,7 @@ export const TileWrapper = memo(
|
|||||||
<>
|
<>
|
||||||
{children({
|
{children({
|
||||||
ref,
|
ref,
|
||||||
|
className: styles.tile,
|
||||||
style: {
|
style: {
|
||||||
opacity,
|
opacity,
|
||||||
scale,
|
scale,
|
||||||
|
|||||||
@@ -802,6 +802,7 @@ interface DragTileData {
|
|||||||
|
|
||||||
export interface ChildrenProperties<T> {
|
export interface ChildrenProperties<T> {
|
||||||
ref: Ref<HTMLElement>;
|
ref: Ref<HTMLElement>;
|
||||||
|
className?: string;
|
||||||
style: ComponentProps<typeof animated.div>["style"];
|
style: ComponentProps<typeof animated.div>["style"];
|
||||||
/**
|
/**
|
||||||
* The width this tile will have once its animations have settled.
|
* The width this tile will have once its animations have settled.
|
||||||
|
|||||||
@@ -15,15 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.videoTile {
|
.videoTile {
|
||||||
position: absolute;
|
|
||||||
contain: strict;
|
|
||||||
top: 0;
|
|
||||||
container-name: videoTile;
|
container-name: videoTile;
|
||||||
container-type: size;
|
container-type: size;
|
||||||
--tileRadius: 8px;
|
--tileRadius: 8px;
|
||||||
border-radius: var(--tileRadius);
|
border-radius: var(--tileRadius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
/* HACK: This has no visual effect due to the short duration, but allows the
|
/* HACK: This has no visual effect due to the short duration, but allows the
|
||||||
JS to detect movement via the transform property for audio spatialization */
|
JS to detect movement via the transform property for audio spatialization */
|
||||||
@@ -154,7 +150,7 @@ limitations under the License.
|
|||||||
.videoMutedOverlay {
|
.videoMutedOverlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #21262c;
|
background-color: var(--system);
|
||||||
}
|
}
|
||||||
|
|
||||||
.presenterLabel {
|
.presenterLabel {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentProps, forwardRef, useCallback, useEffect } from "react";
|
import { ComponentProps, forwardRef, Ref, useCallback, useEffect } from "react";
|
||||||
import { animated } 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";
|
||||||
@@ -39,6 +39,7 @@ import { useModalTriggerState } from "../Modal";
|
|||||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||||
|
|
||||||
export interface ItemData {
|
export interface ItemData {
|
||||||
|
type: "real", // To differentiate from FakeItemData
|
||||||
id: string;
|
id: string;
|
||||||
member?: RoomMember;
|
member?: RoomMember;
|
||||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||||
@@ -64,7 +65,7 @@ interface Props {
|
|||||||
showConnectionStats: boolean;
|
showConnectionStats: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
export const VideoTile = forwardRef<HTMLElement, Props>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
data,
|
data,
|
||||||
@@ -159,7 +160,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
[styles.maximised]: maximised,
|
[styles.maximised]: maximised,
|
||||||
})}
|
})}
|
||||||
style={style}
|
style={style}
|
||||||
ref={tileRef}
|
ref={tileRef as Ref<HTMLDivElement>}
|
||||||
data-testid="videoTile"
|
data-testid="videoTile"
|
||||||
>
|
>
|
||||||
{toolbarButtons.length > 0 && (!maximised || fullscreen) && (
|
{toolbarButtons.length > 0 && (!maximised || fullscreen) && (
|
||||||
|
|||||||
Reference in New Issue
Block a user