- {!hideHeader && (
+ {!hideHeader && maximisedParticipant === null && (
)}
+
{renderContent()}
{footer}
@@ -469,6 +489,7 @@ function useParticipantTiles(
local: sfuParticipant.isLocal,
largeBaseSize: false,
data: {
+ id,
member,
sfuParticipant,
content: TileContent.UserMedia,
@@ -478,14 +499,16 @@ function useParticipantTiles(
// If there is a screen sharing enabled for this participant, create a tile for it as well.
let screenShareTile: TileDescriptor | undefined;
if (sfuParticipant.isScreenShareEnabled) {
+ const screenShareId = `${id}:screen-share`;
screenShareTile = {
...userMediaTile,
- id: `${id}:screen-share`,
+ id: screenShareId,
focused: true,
largeBaseSize: true,
placeNear: id,
data: {
...userMediaTile.data,
+ id: screenShareId,
content: TileContent.ScreenShare,
},
};
diff --git a/src/room/useFullscreen.ts b/src/room/useFullscreen.ts
new file mode 100644
index 00000000..78ec1c87
--- /dev/null
+++ b/src/room/useFullscreen.ts
@@ -0,0 +1,114 @@
+/*
+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 { logger } from "matrix-js-sdk/src/logger";
+import { useCallback, useLayoutEffect, useRef } from "react";
+
+import { TileDescriptor } from "../video-grid/VideoGrid";
+import { useReactiveState } from "../useReactiveState";
+import { useEventTarget } from "../useEvents";
+
+const isFullscreen = () =>
+ Boolean(document.fullscreenElement) ||
+ Boolean(document.webkitFullscreenElement);
+
+function enterFullscreen() {
+ if (document.body.requestFullscreen) {
+ document.body.requestFullscreen();
+ } else if (document.body.webkitRequestFullscreen) {
+ document.body.webkitRequestFullscreen();
+ } else {
+ logger.error("No available fullscreen API!");
+ }
+}
+
+function exitFullscreen() {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ } else {
+ logger.error("No available fullscreen API!");
+ }
+}
+
+function useFullscreenChange(onFullscreenChange: () => void) {
+ useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
+ useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
+}
+
+/**
+ * Provides callbacks for controlling the full-screen view, which can hold one
+ * item at a time.
+ */
+export function useFullscreen(items: TileDescriptor[]): {
+ fullscreenItem: TileDescriptor | null;
+ toggleFullscreen: (itemId: string) => void;
+ exitFullscreen: () => void;
+} {
+ const [fullscreenItem, setFullscreenItem] =
+ useReactiveState | null>(
+ (prevItem) =>
+ prevItem == null
+ ? null
+ : items.find((i) => i.id === prevItem.id) ?? null,
+ [items]
+ );
+
+ const latestItems = useRef[]>(items);
+ latestItems.current = items;
+
+ const latestFullscreenItem = useRef | null>(fullscreenItem);
+ latestFullscreenItem.current = fullscreenItem;
+
+ const toggleFullscreen = useCallback(
+ (itemId: string) => {
+ setFullscreenItem(
+ latestFullscreenItem.current === null
+ ? latestItems.current.find((i) => i.id === itemId) ?? null
+ : null
+ );
+ },
+ [setFullscreenItem]
+ );
+
+ const exitFullscreenCallback = useCallback(
+ () => setFullscreenItem(null),
+ [setFullscreenItem]
+ );
+
+ useLayoutEffect(() => {
+ // Determine whether we need to change the fullscreen state
+ if (isFullscreen() !== (fullscreenItem !== null)) {
+ (fullscreenItem === null ? exitFullscreen : enterFullscreen)();
+ }
+ }, [fullscreenItem]);
+
+ // Detect when the user exits fullscreen through an external mechanism like
+ // browser chrome or the escape key
+ useFullscreenChange(
+ useCallback(() => {
+ if (!isFullscreen()) setFullscreenItem(null);
+ }, [setFullscreenItem])
+ );
+
+ return {
+ fullscreenItem,
+ toggleFullscreen,
+ exitFullscreen: exitFullscreenCallback,
+ };
+}
diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx
index 81a2361d..1cf77d3f 100644
--- a/src/video-grid/VideoTile.tsx
+++ b/src/video-grid/VideoTile.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { useCallback } from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
@@ -34,8 +34,10 @@ import styles from "./VideoTile.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { useReactiveState } from "../useReactiveState";
+import { FullscreenButton } from "../button/Button";
export interface ItemData {
+ id: string;
member?: RoomMember;
sfuParticipant: LocalParticipant | RemoteParticipant;
content: TileContent;
@@ -48,7 +50,9 @@ export enum TileContent {
interface Props {
data: ItemData;
-
+ maximised: boolean;
+ fullscreen: boolean;
+ onToggleFullscreen: (itemId: string) => void;
// TODO: Refactor these props.
targetWidth: number;
targetHeight: number;
@@ -62,6 +66,9 @@ export const VideoTile = React.forwardRef(
(
{
data,
+ maximised,
+ fullscreen,
+ onToggleFullscreen,
className,
style,
targetWidth,
@@ -93,17 +100,33 @@ export const VideoTile = React.forwardRef(
}
}, [member, setDisplayName]);
- const audioEl = React.useRef(null);
const { isMuted: microphoneMuted } = useMediaTrack(
content === TileContent.UserMedia
? Track.Source.Microphone
: Track.Source.ScreenShareAudio,
- sfuParticipant,
- {
- element: audioEl,
- }
+ sfuParticipant
);
+ const onFullscreen = useCallback(() => {
+ onToggleFullscreen(data.id);
+ }, [data, onToggleFullscreen]);
+
+ const toolbarButtons: JSX.Element[] = [];
+ if (!sfuParticipant.isLocal) {
+ // TODO local volume option, which would also go here
+
+ if (content === TileContent.ScreenShare) {
+ toolbarButtons.push(
+
+ );
+ }
+ }
+
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
@@ -117,11 +140,15 @@ export const VideoTile = React.forwardRef(
showSpeakingIndicator,
[styles.muted]: microphoneMuted,
[styles.screenshare]: content === TileContent.ScreenShare,
+ [styles.maximised]: maximised,
})}
style={style}
ref={tileRef}
data-testid="videoTile"
>
+ {toolbarButtons.length > 0 && (!maximised || fullscreen) && (
+ {toolbarButtons}
+ )}
{content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && (
<>
@@ -134,7 +161,7 @@ export const VideoTile = React.forwardRef(
/>
>
)}
- {content == TileContent.ScreenShare ? (
+ {content === TileContent.ScreenShare ? (
{t("{{displayName}} is presenting", { displayName })}
@@ -155,7 +182,6 @@ export const VideoTile = React.forwardRef(
: Track.Source.ScreenShare
}
/>
-
);
}