bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/webcam/webcam-graphql/component.tsx

396 lines
14 KiB
TypeScript
Raw Normal View History

2024-04-20 04:34:43 +08:00
import React, { useState, useEffect } from 'react';
import Resizable from 're-resizable';
import Draggable, { DraggableEvent } from 'react-draggable';
2024-04-20 04:34:43 +08:00
import { Session } from 'meteor/session';
import { withTracker } from 'meteor/react-meteor-data';
import { useSubscription } from '@apollo/client';
import { useVideoStreams } from '/imports/ui/components/video-provider/video-provider-graphql/hooks';
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch,
} from '/imports/ui/components/layout/context';
import { LAYOUT_TYPE, ACTIONS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
import { CURRENT_PRESENTATION_PAGE_SUBSCRIPTION } from '/imports/ui/components/whiteboard/queries';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import DropAreaContainer from './drop-areas/container';
import VideoProviderContainer from '/imports/ui/components/video-provider/video-provider-graphql/container';
import Storage from '/imports/ui/services/storage/session';
import Styled from './styles';
import { Input, Layout, Output } from '/imports/ui/components/layout/layoutTypes';
import { VideoItem } from '/imports/ui/components/video-provider/video-provider-graphql/types';
import useSettings from '/imports/ui/services/settings/hooks/useSettings';
import { SETTINGS } from '/imports/ui/services/settings/enums';
interface WebcamComponentGraphqlProps {
cameraDock: Output['cameraDock'];
swapLayout: boolean;
focusedId: string;
layoutContextDispatch: (...args: unknown[]) => void;
fullscreen: Layout['fullscreen'];
isPresenter: boolean;
displayPresentation: boolean;
cameraOptimalGridSize: Input['cameraDock']['cameraOptimalGridSize'];
isRTL: boolean;
isGridEnabled: boolean;
}
2024-04-20 04:34:43 +08:00
const WebcamComponentGraphql: React.FC<WebcamComponentGraphqlProps> = ({
2024-04-20 04:34:43 +08:00
cameraDock,
swapLayout,
focusedId,
layoutContextDispatch,
fullscreen,
isPresenter,
displayPresentation,
cameraOptimalGridSize: cameraSize,
isRTL,
isGridEnabled,
}) => {
const [isResizing, setIsResizing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isFullscreen, setIsFullScreen] = useState(false);
const [resizeStart, setResizeStart] = useState({ width: 0, height: 0 });
const [cameraMaxWidth, setCameraMaxWidth] = useState(0);
const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false);
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
const { height: lastHeight } = lastSize as { width: number, height: number };
2024-04-20 04:34:43 +08:00
const isCameraTopOrBottom = cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
const isCameraLeftOrRight = cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT;
const isCameraSidebar = cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM;
useEffect(() => {
const handleVisibility = () => {
if (document.hidden) {
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
}
};
document.addEventListener('visibilitychange', handleVisibility);
return () => {
document.removeEventListener('visibilitychange', handleVisibility);
};
}, []);
useEffect(() => {
setIsFullScreen(fullscreen.group === 'webcams');
}, [fullscreen]);
useEffect(() => {
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth)
? cameraDock.presenterMaxWidth
: cameraDock.maxWidth;
2024-04-20 04:34:43 +08:00
setCameraMaxWidth(newCameraMaxWidth);
if (isCameraLeftOrRight && cameraDock.width > newCameraMaxWidth) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: newCameraMaxWidth,
height: cameraDock.height,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
Storage.setItem('webcamSize', { width: newCameraMaxWidth, height: lastHeight });
}
const cams = document.getElementById('cameraDock');
cams?.setAttribute('data-position', cameraDock.position);
2024-04-20 04:34:43 +08:00
}, [cameraDock.position, cameraDock.maxWidth, isPresenter, displayPresentation]);
const handleVideoFocus = (id: string) => {
2024-04-20 04:34:43 +08:00
layoutContextDispatch({
type: ACTIONS.SET_FOCUSED_CAMERA_ID,
value: focusedId !== id ? id : false,
});
};
2024-04-20 04:34:43 +08:00
const onResizeHandle = (deltaWidth: number, deltaHeight: number) => {
if (cameraDock.resizableEdge?.top || cameraDock.resizableEdge?.bottom) {
2024-04-20 04:34:43 +08:00
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: cameraDock.width,
height: resizeStart.height + deltaHeight,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
}
if (cameraDock.resizableEdge?.left || cameraDock.resizableEdge?.right) {
2024-04-20 04:34:43 +08:00
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: resizeStart.width + deltaWidth,
height: cameraDock.height,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
}
};
const handleWebcamDragStart = () => {
setIsDragging(true);
document.body.style.overflow = 'hidden';
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_DRAGGING,
value: true,
});
};
const handleWebcamDragStop = (e: DraggableEvent) => {
2024-04-20 04:34:43 +08:00
setIsDragging(false);
setDraggedAtLeastOneTime(false);
document.body.style.overflow = 'auto';
const dropAreaId = (e.target as HTMLDivElement).id;
2024-04-20 04:34:43 +08:00
if (Object.values(CAMERADOCK_POSITION).includes(dropAreaId) && draggedAtLeastOneTime) {
2024-04-20 04:34:43 +08:00
const layout = document.getElementById('layout');
layout?.setAttribute('data-cam-position', dropAreaId);
2024-04-20 04:34:43 +08:00
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_POSITION,
value: dropAreaId,
2024-04-20 04:34:43 +08:00
});
}
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_DRAGGING,
value: false,
});
};
const draggableOffset = {
2024-04-20 04:34:43 +08:00
left: isDragging && (isCameraTopOrBottom || isCameraSidebar)
? ((cameraDock.width - (cameraSize?.width ?? 0)) / 2)
2024-04-20 04:34:43 +08:00
: 0,
top: isDragging && isCameraLeftOrRight
? ((cameraDock.height - (cameraSize?.height ?? 0)) / 2)
2024-04-20 04:34:43 +08:00
: 0,
};
if (isRTL) {
draggableOffset.left *= -1;
2024-04-20 04:34:43 +08:00
}
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const mobileWidth = `${isDragging ? cameraSize?.width : cameraDock.width}pt`;
const mobileHeight = `${isDragging ? cameraSize?.height : cameraDock.height}pt`;
const isDesktopWidth = isDragging ? cameraSize?.width : cameraDock.width;
const isDesktopHeight = isDragging ? cameraSize?.height : cameraDock.height;
2024-04-20 04:34:43 +08:00
const camOpacity = isDragging ? 0.5 : undefined;
2024-04-20 04:34:43 +08:00
return (
<>
{isDragging ? <DropAreaContainer /> : null}
<Styled.ResizableWrapper
$horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
2024-04-20 04:34:43 +08:00
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM}
$vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
2024-04-20 04:34:43 +08:00
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT}
>
<Draggable
handle="video"
bounds="html"
onStart={handleWebcamDragStart}
onDrag={() => {
if (!draggedAtLeastOneTime) {
setDraggedAtLeastOneTime(true);
}
}}
onStop={handleWebcamDragStop}
onMouseDown={
cameraDock.isDraggable ? (e) => e.preventDefault() : undefined
}
disabled={!cameraDock.isDraggable || isResizing || isFullscreen}
position={
{
x: cameraDock.left - cameraDock.right + draggableOffset.left,
y: cameraDock.top + draggableOffset.top,
}
}
>
<Resizable
minWidth={isDragging ? cameraSize?.width : cameraDock.minWidth}
minHeight={isDragging ? cameraSize?.height : cameraDock.minHeight}
maxWidth={isDragging ? cameraSize?.width : cameraMaxWidth}
maxHeight={isDragging ? cameraSize?.height : cameraDock.maxHeight}
2024-04-20 04:34:43 +08:00
size={{
width: isDragging ? cameraSize?.width : cameraDock.width,
height: isDragging ? cameraSize?.height : cameraDock.height,
2024-04-20 04:34:43 +08:00
}}
onResizeStart={() => {
setIsResizing(true);
setResizeStart({ width: cameraDock.width, height: cameraDock.height });
onResizeHandle(cameraDock.width, cameraDock.height);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING,
value: true,
});
}}
onResize={(_, __, ___, d) => {
2024-04-20 04:34:43 +08:00
onResizeHandle(d.width, d.height);
}}
onResizeStop={() => {
setResizeStart({ width: 0, height: 0 });
setTimeout(() => setIsResizing(false), 500);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING,
value: false,
});
}}
enable={{
top: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.top,
2024-04-20 04:34:43 +08:00
bottom: !isFullscreen && !isDragging && !swapLayout
&& cameraDock?.resizableEdge?.bottom,
left: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.left,
right: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.right,
2024-04-20 04:34:43 +08:00
topLeft: false,
topRight: false,
bottomLeft: false,
bottomRight: false,
}}
style={{
position: 'absolute',
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock?.zIndex,
2024-04-20 04:34:43 +08:00
}}
>
<Styled.Draggable
$isDraggable={!!cameraDock.isDraggable && !isFullscreen && !isDragging}
$isDragging={isDragging}
2024-04-20 04:34:43 +08:00
id="cameraDock"
role="region"
draggable={cameraDock.isDraggable && !isFullscreen ? 'true' : undefined}
style={{
width: isIphone ? mobileWidth : isDesktopWidth,
height: isIphone ? mobileHeight : isDesktopHeight,
opacity: camOpacity,
background: 'none',
2024-04-20 04:34:43 +08:00
}}
>
<VideoProviderContainer
{...{
swapLayout,
cameraDock,
focusedId,
handleVideoFocus,
isGridEnabled,
}}
/>
</Styled.Draggable>
</Resizable>
</Draggable>
</Styled.ResizableWrapper>
</>
);
};
interface WebcamContainerGraphqlProps {
audioModalIsOpen: boolean;
isLayoutSwapped: boolean;
layoutType: string;
}
const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
audioModalIsOpen,
isLayoutSwapped,
layoutType,
}) => {
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const cameraDockInput = layoutSelectInput((i: Input) => i.cameraDock);
const presentation = layoutSelectOutput((i: Output) => i.presentation);
const cameraDock = layoutSelectOutput((i: Output) => i.cameraDock);
2024-04-20 04:34:43 +08:00
const layoutContextDispatch = layoutDispatch();
const { data: presentationPageData } = useSubscription(CURRENT_PRESENTATION_PAGE_SUBSCRIPTION);
const presentationPage = presentationPageData?.pres_page_curr[0] || {};
const hasPresentation = !!presentationPage?.presentationId;
// @ts-ignore JS code
const { paginationEnabled } = useSettings(SETTINGS.APPLICATION);
// @ts-ignore JS code
const { viewParticipantsWebcams } = useSettings(SETTINGS.DATA_SAVING);
2024-04-20 04:34:43 +08:00
const swapLayout = !hasPresentation || isLayoutSwapped;
let floatingOverlay = false;
let hideOverlay = false;
if (swapLayout) {
floatingOverlay = true;
hideOverlay = true;
}
const { cameraOptimalGridSize } = cameraDockInput;
const { display: displayPresentation } = presentation;
const { data: currentUserData } = useCurrentUser((user) => ({
presenter: user.presenter,
}));
const isGridEnabled = layoutType === LAYOUT_TYPE.VIDEO_FOCUS;
const { streams: videoUsers, gridUsers } = useVideoStreams(isGridEnabled, paginationEnabled, viewParticipantsWebcams);
let usersVideo: VideoItem[];
2024-04-20 04:34:43 +08:00
if (gridUsers.length > 0) {
usersVideo = [
...videoUsers,
...gridUsers,
];
2024-04-20 04:34:43 +08:00
} else {
usersVideo = videoUsers;
}
return !audioModalIsOpen && (usersVideo.length > 0 || isGridEnabled)
? (
<WebcamComponentGraphql
{...{
swapLayout,
usersVideo,
focusedId: cameraDock.focusedId,
cameraDock,
cameraOptimalGridSize,
layoutContextDispatch,
fullscreen,
isPresenter: currentUserData?.presenter ?? false,
2024-04-20 04:34:43 +08:00
displayPresentation,
isRTL,
isGridEnabled,
floatingOverlay,
hideOverlay,
}}
/>
)
: null;
};
type TrackerData = {
audioModalIsOpen: boolean;
};
type TrackerProps = {
isLayoutSwapped: boolean;
layoutType: string;
};
2024-04-20 04:34:43 +08:00
export default withTracker<TrackerData, TrackerProps>(() => {
const audioModalIsOpen = Session.get('audioModalIsOpen');
return {
audioModalIsOpen,
};
2024-04-20 04:34:43 +08:00
})(WebcamContainerGraphql);