mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +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({
|
||||
className,
|
||||
variant = "toolbar",
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
ScreenshareButton,
|
||||
SettingsButton,
|
||||
InviteButton,
|
||||
SimulateJoinButton,
|
||||
} from "../button";
|
||||
import {
|
||||
Header,
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
import {
|
||||
useShowInspector,
|
||||
useShowConnectionStats,
|
||||
useVideoGridSandboxMode,
|
||||
} from "../settings/useSetting";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
@@ -83,6 +85,8 @@ import { useFullscreen } from "./useFullscreen";
|
||||
import { useLayoutStates } from "../video-grid/Layout";
|
||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
||||
import { E2EELock } from "../E2EELock";
|
||||
import { useFakeTiles } from "./useFakeTiles";
|
||||
import { FakeItemData, FakeVideoTile } from "../video-grid/FakeVideoTile";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// 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 [showConnectionStats] = useShowConnectionStats();
|
||||
const [videoGridSandboxMode] = useVideoGridSandboxMode()
|
||||
|
||||
const { fakeTiles: fakeItems, simulateJoin } = useFakeTiles()
|
||||
|
||||
const { hideScreensharing } = useUrlParams();
|
||||
|
||||
@@ -227,9 +234,11 @@ export function InCallView({
|
||||
const reducedControls = boundsValid && bounds.width <= 400;
|
||||
const noControls = reducedControls && bounds.height <= 400;
|
||||
|
||||
const items = useParticipantTiles(livekitRoom, participants);
|
||||
const realItems = useParticipantTiles(livekitRoom, participants);
|
||||
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
|
||||
// manually put in fullscreen, or the focused (active) participant if the
|
||||
@@ -238,9 +247,9 @@ export function InCallView({
|
||||
() =>
|
||||
fullscreenItem ??
|
||||
(noControls
|
||||
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
|
||||
? realItems.find((item) => item.isSpeaker) ?? realItems.at(0) ?? null
|
||||
: null),
|
||||
[fullscreenItem, noControls, items]
|
||||
[fullscreenItem, noControls, realItems]
|
||||
);
|
||||
|
||||
const Grid =
|
||||
@@ -283,16 +292,19 @@ export function InCallView({
|
||||
disableAnimations={prefersReducedMotion || isSafari}
|
||||
layoutStates={layoutStates}
|
||||
>
|
||||
{(props) => (
|
||||
{(props) => props.data.type === "real" ? (
|
||||
<VideoTile
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
showSpeakingIndicator={items.length > 2}
|
||||
showSpeakingIndicator={realItems.length > 2}
|
||||
showConnectionStats={showConnectionStats}
|
||||
{...props}
|
||||
data={props.data}
|
||||
ref={props.ref as Ref<HTMLDivElement>}
|
||||
/>
|
||||
) : (
|
||||
<FakeVideoTile {...props} data={props.data} />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
@@ -381,6 +393,8 @@ export function InCallView({
|
||||
);
|
||||
}
|
||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||
if (videoGridSandboxMode)
|
||||
buttons.push(<SimulateJoinButton key="5" onPress={simulateJoin} />)
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
@@ -480,7 +494,7 @@ function useParticipantTiles(
|
||||
const member = matrixParticipants.get(id);
|
||||
allGhosts &&= member === undefined;
|
||||
|
||||
const userMediaTile = {
|
||||
const userMediaTile: TileDescriptor<ItemData> = {
|
||||
id,
|
||||
focused: false,
|
||||
isPresenter: sfuParticipant.isScreenShareEnabled,
|
||||
@@ -491,6 +505,7 @@ function useParticipantTiles(
|
||||
local: sfuParticipant.isLocal,
|
||||
largeBaseSize: false,
|
||||
data: {
|
||||
type: "real",
|
||||
id,
|
||||
member,
|
||||
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 { useCallback, useLayoutEffect, useRef } from "react";
|
||||
import { useCallback, useLayoutEffect } from "react";
|
||||
|
||||
import { TileDescriptor } from "../video-grid/VideoGrid";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { useEventTarget } from "../useEvents";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
const isFullscreen = () =>
|
||||
Boolean(document.fullscreenElement) ||
|
||||
@@ -69,11 +70,8 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
||||
[items]
|
||||
);
|
||||
|
||||
const latestItems = useRef<TileDescriptor<T>[]>(items);
|
||||
latestItems.current = items;
|
||||
|
||||
const latestFullscreenItem = useRef<TileDescriptor<T> | null>(fullscreenItem);
|
||||
latestFullscreenItem.current = fullscreenItem;
|
||||
const latestItems = useLatest(items)
|
||||
const latestFullscreenItem = useLatest(fullscreenItem)
|
||||
|
||||
const toggleFullscreen = useCallback(
|
||||
(itemId: string) => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
useOptInAnalytics,
|
||||
useDeveloperSettingsTab,
|
||||
useShowConnectionStats,
|
||||
useVideoGridSandboxMode,
|
||||
} from "./useSetting";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
@@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => {
|
||||
useDeveloperSettingsTab();
|
||||
const [showConnectionStats, setShowConnectionStats] =
|
||||
useShowConnectionStats();
|
||||
const [videoGridSandboxMode, setVideoGridSandboxMode] = useVideoGridSandboxMode()
|
||||
|
||||
const downloadDebugLog = useDownloadDebugLog();
|
||||
|
||||
@@ -249,6 +251,18 @@ export const SettingsModal = (props: Props) => {
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
||||
</FieldRow>
|
||||
|
||||
@@ -104,6 +104,8 @@ export const useDeveloperSettingsTab = () =>
|
||||
export const useShowConnectionStats = () =>
|
||||
useSetting("show-connection-stats", false);
|
||||
|
||||
export const useVideoGridSandboxMode = () => useSetting("video-grid-sandbox-mode", false)
|
||||
|
||||
export const useDefaultDevices = () =>
|
||||
useSetting("defaultDevices", {
|
||||
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 { ChildrenProperties } from "./VideoGrid";
|
||||
import styles from "./TileWrapper.module.css"
|
||||
|
||||
interface Props<T> {
|
||||
id: string;
|
||||
@@ -77,6 +78,7 @@ export const TileWrapper = memo(
|
||||
<>
|
||||
{children({
|
||||
ref,
|
||||
className: styles.tile,
|
||||
style: {
|
||||
opacity,
|
||||
scale,
|
||||
|
||||
@@ -802,6 +802,7 @@ interface DragTileData {
|
||||
|
||||
export interface ChildrenProperties<T> {
|
||||
ref: Ref<HTMLElement>;
|
||||
className?: string;
|
||||
style: ComponentProps<typeof animated.div>["style"];
|
||||
/**
|
||||
* The width this tile will have once its animations have settled.
|
||||
|
||||
@@ -15,15 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.videoTile {
|
||||
position: absolute;
|
||||
contain: strict;
|
||||
top: 0;
|
||||
container-name: videoTile;
|
||||
container-type: size;
|
||||
--tileRadius: 8px;
|
||||
border-radius: var(--tileRadius);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
/* 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 */
|
||||
@@ -154,7 +150,7 @@ limitations under the License.
|
||||
.videoMutedOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #21262c;
|
||||
background-color: var(--system);
|
||||
}
|
||||
|
||||
.presenterLabel {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
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 classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -39,6 +39,7 @@ import { useModalTriggerState } from "../Modal";
|
||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||
|
||||
export interface ItemData {
|
||||
type: "real", // To differentiate from FakeItemData
|
||||
id: string;
|
||||
member?: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
@@ -64,7 +65,7 @@ interface Props {
|
||||
showConnectionStats: boolean;
|
||||
}
|
||||
|
||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
export const VideoTile = forwardRef<HTMLElement, Props>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
@@ -159,7 +160,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
[styles.maximised]: maximised,
|
||||
})}
|
||||
style={style}
|
||||
ref={tileRef}
|
||||
ref={tileRef as Ref<HTMLDivElement>}
|
||||
data-testid="videoTile"
|
||||
>
|
||||
{toolbarButtons.length > 0 && (!maximised || fullscreen) && (
|
||||
|
||||
Reference in New Issue
Block a user