From 2c8d39c471fc0821a875f51f2b092ac9eead1a50 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Thu, 3 Oct 2024 17:32:43 -0300 Subject: [PATCH 1/6] fix(layout): sidebar content display check Adds a check for unmounting the sidebar content when the display option from layout context is `false`. --- .../imports/ui/components/sidebar-content/container.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/sidebar-content/container.jsx b/bigbluebutton-html5/imports/ui/components/sidebar-content/container.jsx index ba1908d699..3f5d8c1d02 100644 --- a/bigbluebutton-html5/imports/ui/components/sidebar-content/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/sidebar-content/container.jsx @@ -27,6 +27,8 @@ const SidebarContentContainer = () => { const currentSlideId = presentationPage?.pageId; + if (sidebarContentOutput.display === false) return null; + return ( Date: Fri, 4 Oct 2024 14:28:19 -0300 Subject: [PATCH 2/6] fix(layout): focused camera propagation The focused camera id is of type string and was set to boolean `false` when a camera was unfocused, causing it to throw an error on apollo and consequently not propagating the focused camera status. This commit fixes this issue by setting the focused camera id to its initial string value `none` when a camera is unfocused. --- bigbluebutton-html5/imports/ui/components/webcam/component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx index 033bc68330..90ee82a915 100644 --- a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx @@ -21,6 +21,7 @@ import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedS import { useStorageKey } from '/imports/ui/services/storage/hooks'; import useSettings from '../../services/settings/hooks/useSettings'; import { SETTINGS } from '../../services/settings/enums'; +import { INITIAL_INPUT_STATE } from '../layout/initState'; interface WebcamComponentProps { cameraDock: Output['cameraDock']; @@ -107,7 +108,7 @@ const WebcamComponent: React.FC = ({ const handleVideoFocus = (id: string) => { layoutContextDispatch({ type: ACTIONS.SET_FOCUSED_CAMERA_ID, - value: focusedId !== id ? id : false, + value: focusedId !== id ? id : INITIAL_INPUT_STATE.cameraDock.focusedId, }); }; From cf8a9b857c55c042ff9da142f78b6461365ba5aa Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 9 Oct 2024 11:42:34 -0300 Subject: [PATCH 3/6] refactor(layout): propagate and replicate mapping This commit introduces a mapping between each layout type and the structures it should propagate or replicate. This change enhances the clarity of the implemented behavior for each layout type and simplifies modifications to the elements propagated for each layout. Additionally, the push layout engine has been updated to utilize the new mapping structure. --- .../imports/ui/components/layout/enums.js | 13 ++++ .../layout/push-layout/pushLayoutEngine.jsx | 76 +++++++++++++------ .../imports/ui/components/layout/utils.js | 56 +++++++++++++- 3 files changed, 120 insertions(+), 25 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/layout/enums.js b/bigbluebutton-html5/imports/ui/components/layout/enums.js index 58a701eea9..7300b080d4 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/enums.js +++ b/bigbluebutton-html5/imports/ui/components/layout/enums.js @@ -33,6 +33,19 @@ export const HIDDEN_LAYOUTS = [ LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY, ]; +export const LAYOUT_ELEMENTS = { + LAYOUT_TYPE: 'layoutType', + PRESENTATION_STATE: 'presentationState', + FOCUSED_CAMERA: 'focusedCamera', + CAMERA_DOCK_SIZE: 'cameraDockSize', + CAMERA_DOCK_POSITION: 'cameradockPosition', +}; + +export const SYNC = { + PROPAGATE_ELEMENTS: 'propagateElements', + REPLICATE_ELEMENTS: 'replicateElements', +}; + export const ACTIONS = { SET_AUTO_ARRANGE_LAYOUT: 'setAutoArrangeLayout', SET_IS_RTL: 'setIsRTL', diff --git a/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx b/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx index 603f2819d8..a6c0ce1066 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx +++ b/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx @@ -3,8 +3,13 @@ import PropTypes from 'prop-types'; import getFromUserSettings from '/imports/ui/services/users-settings'; import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import MediaService from '/imports/ui/components/media/service'; -import { LAYOUT_TYPE, ACTIONS } from '../enums'; -import { isMobile } from '../utils'; +import { + LAYOUT_TYPE, + ACTIONS, + SYNC, + LAYOUT_ELEMENTS, +} from '../enums'; +import { isMobile, LAYOUTS_SYNC } from '../utils'; import { updateSettings } from '/imports/ui/components/settings/service'; import Session from '/imports/ui/services/storage/in-memory'; import usePreviousValue from '/imports/ui/hooks/usePreviousValue'; @@ -159,6 +164,7 @@ const PushLayoutEngine = (props) => { }, [hasMeetingLayout]); useEffect(() => { + if (!selectedLayout) return () => {}; const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout; const pushLayoutMeetingDidChange = pushLayoutMeeting !== prevProps.pushLayoutMeeting; const enforceLayoutDidChange = enforceLayout !== prevProps.enforceLayout; @@ -166,13 +172,15 @@ const PushLayoutEngine = (props) => { ? meetingLayoutDidChange || enforceLayoutDidChange : ((meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting) || enforceLayoutDidChange; + const layoutReplicateElements = LAYOUTS_SYNC[selectedLayout][SYNC.REPLICATE_ELEMENTS]; + const layoutPropagateElements = LAYOUTS_SYNC[selectedLayout][SYNC.PROPAGATE_ELEMENTS]; const Settings = getSettingsSingletonInstance(); - if (shouldSwitchLayout) { + const replicateLayoutType = () => { let contextLayout = enforceLayout || meetingLayout; if (isMobile()) { - if (contextLayout === 'custom') { - contextLayout = 'smart'; + if (contextLayout === LAYOUT_TYPE.CUSTOM_LAYOUT) { + contextLayout = LAYOUT_TYPE.SMART_LAYOUT; } } @@ -187,18 +195,19 @@ const PushLayoutEngine = (props) => { selectedLayout: contextLayout, }, }, null, setLocalSettings); - } + }; - if (!enforceLayout && pushLayoutMeetingDidChange) { - updateSettings({ - application: { - ...Settings.application, - pushLayout: pushLayoutMeeting, - }, - }, null, setLocalSettings); - } + const replicatePresentationState = () => { + if (meetingPresentationIsOpen !== prevProps.meetingPresentationIsOpen + || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { + layoutContextDispatch({ + type: ACTIONS.SET_PRESENTATION_IS_OPEN, + value: meetingPresentationIsOpen, + }); + } + }; - if (meetingLayout === 'custom' && selectedLayout === 'custom' && !isPresenter) { + const replicateFocusedCamera = () => { if (meetingLayoutFocusedCamera !== prevProps.meetingLayoutFocusedCamera || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { layoutContextDispatch({ @@ -206,7 +215,9 @@ const PushLayoutEngine = (props) => { value: meetingLayoutFocusedCamera, }); } + }; + const replicateCameraDockPosition = () => { if (meetingLayoutCameraPosition !== prevProps.meetingLayoutCameraPosition || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { layoutContextDispatch({ @@ -214,7 +225,9 @@ const PushLayoutEngine = (props) => { value: meetingLayoutCameraPosition, }); } + }; + const replicateCameraDockSize = () => { if (!equalDouble(meetingLayoutVideoRate, prevProps.meetingLayoutVideoRate) || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { let w; let h; @@ -243,16 +256,28 @@ const PushLayoutEngine = (props) => { }, }); } + }; - if (meetingPresentationIsOpen !== prevProps.meetingPresentationIsOpen - || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { - layoutContextDispatch({ - type: ACTIONS.SET_PRESENTATION_IS_OPEN, - value: meetingPresentationIsOpen, - }); + // REPLICATE LAYOUT + if (shouldSwitchLayout && layoutReplicateElements.includes(LAYOUT_ELEMENTS.LAYOUT_TYPE)) { + replicateLayoutType(); + } + if (!isPresenter) { + if (layoutReplicateElements.includes(LAYOUT_ELEMENTS.PRESENTATION_STATE)) { + replicatePresentationState(); + } + if (layoutReplicateElements.includes(LAYOUT_ELEMENTS.FOCUSED_CAMERA)) { + replicateFocusedCamera(); + } + if (layoutReplicateElements.includes(LAYOUT_ELEMENTS.CAMERA_DOCK_POSITION)) { + replicateCameraDockPosition(); + } + if (layoutReplicateElements.includes(LAYOUT_ELEMENTS.CAMERA_DOCK_SIZE)) { + replicateCameraDockSize(); } } + // PROPAGATE LAYOUT const layoutChanged = presentationIsOpen !== prevProps.presentationIsOpen || selectedLayout !== prevProps.selectedLayout || cameraIsResizing !== prevProps.cameraIsResizing @@ -270,8 +295,12 @@ const PushLayoutEngine = (props) => { } // change layout sizes / states - if ((pushLayout && layoutChanged) || pushLayout !== prevProps.pushLayout) { - if (isPresenter) { + if (isPresenter + // since all meeting layout properties are pushed together in a + // single call just check whether there is any element to be propagate + && layoutPropagateElements.length > 0 + ) { + if ((pushLayout && layoutChanged) || pushLayout !== prevProps.pushLayout) { setMeetingLayout(); } } @@ -279,6 +308,7 @@ const PushLayoutEngine = (props) => { if (selectedLayout !== prevProps.selectedLayout) { Session.setItem('isGridEnabled', selectedLayout === LAYOUT_TYPE.VIDEO_FOCUS); } + return () => {}; }); return null; diff --git a/bigbluebutton-html5/imports/ui/components/layout/utils.js b/bigbluebutton-html5/imports/ui/components/layout/utils.js index 12c62b82f2..6f4b9b88e8 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/utils.js +++ b/bigbluebutton-html5/imports/ui/components/layout/utils.js @@ -1,4 +1,9 @@ -import { DEVICE_TYPE, LAYOUT_TYPE } from './enums'; +import { + DEVICE_TYPE, + LAYOUT_ELEMENTS, + LAYOUT_TYPE, + SYNC, +} from './enums'; const phoneUpperBoundary = 600; const tabletPortraitUpperBoundary = 900; @@ -66,4 +71,51 @@ const suportedLayouts = [ ], }, ]; -export { suportedLayouts }; + +const COMMON_ELEMENTS = { + DEFAULT: [ + LAYOUT_ELEMENTS.LAYOUT_TYPE, + LAYOUT_ELEMENTS.PRESENTATION_STATE, + LAYOUT_ELEMENTS.FOCUSED_CAMERA, + ], + DOCK: [ + LAYOUT_ELEMENTS.CAMERA_DOCK_POSITION, + LAYOUT_ELEMENTS.CAMERA_DOCK_SIZE, + ], +}; + +const LAYOUTS_SYNC = { + [LAYOUT_TYPE.CUSTOM_LAYOUT]: { + [SYNC.PROPAGATE_ELEMENTS]: [...COMMON_ELEMENTS.DEFAULT, ...COMMON_ELEMENTS.DOCK], + [SYNC.REPLICATE_ELEMENTS]: [...COMMON_ELEMENTS.DEFAULT, ...COMMON_ELEMENTS.DOCK], + }, + [LAYOUT_TYPE.SMART_LAYOUT]: { + [SYNC.PROPAGATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + [SYNC.REPLICATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + }, + [LAYOUT_TYPE.PRESENTATION_FOCUS]: { + [SYNC.PROPAGATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + [SYNC.REPLICATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + }, + [LAYOUT_TYPE.VIDEO_FOCUS]: { + [SYNC.PROPAGATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + [SYNC.REPLICATE_ELEMENTS]: COMMON_ELEMENTS.DEFAULT, + }, + // Hidden layouts neither propagate nor replicate the layout type pushed. + // These layouts are not available for selection in the UI and are set only via join parameters. + // Propagating or replicating the layout type would be inappropriate, as + // they are intended to maintain a specific view unaffected by layout type changes. + [LAYOUT_TYPE.CAMERAS_ONLY]: { + [SYNC.PROPAGATE_ELEMENTS]: [], + [SYNC.REPLICATE_ELEMENTS]: [LAYOUT_ELEMENTS.FOCUSED_CAMERA], + }, + [LAYOUT_TYPE.PRESENTATION_ONLY]: { + [SYNC.PROPAGATE_ELEMENTS]: [], + [SYNC.REPLICATE_ELEMENTS]: [], + }, + [LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY]: { + [SYNC.PROPAGATE_ELEMENTS]: [], + [SYNC.REPLICATE_ELEMENTS]: [], + }, +}; +export { suportedLayouts, LAYOUTS_SYNC }; From a0f103972145e812e3f822796e729f1ef3ae931d Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 9 Oct 2024 13:57:17 -0300 Subject: [PATCH 4/6] feat(layout): add optional parameter to `calculateMediaAreaBounds` Adds an optional parameter to the `calculateMediaAreaBounds` function, allowing a margin to be specified when determining the media area bounds. Any usage of the function without the optional parameter will default to the previous behavior(no margin). --- .../components/layout/layout-manager/layoutEngine.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx index 304fdf2431..7fe1229da3 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx +++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx @@ -293,7 +293,7 @@ const LayoutEngine = () => { }; }; - const calculatesMediaAreaBounds = (sidebarNavWidth, sidebarContentWidth) => { + const calculatesMediaAreaBounds = (sidebarNavWidth, sidebarContentWidth, margin = 0) => { const { height: actionBarHeight } = calculatesActionbarHeight(); const navBarHeight = calculatesNavbarHeight(); @@ -307,10 +307,10 @@ const LayoutEngine = () => { } return { - width, - height: windowHeight() - (navBarHeight + actionBarHeight + bannerAreaHeight()), - top: navBarHeight + bannerAreaHeight(), - left, + width: width - (2 * margin), + height: windowHeight() - (navBarHeight + actionBarHeight + bannerAreaHeight() + (2 * margin)), + top: navBarHeight + bannerAreaHeight() + margin, + left: left + margin, }; }; From 28eb37a4986357009293d014f43bb0017ac40960 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 9 Oct 2024 15:05:56 -0300 Subject: [PATCH 5/6] fix(layout): correct top and height calculation of camera dock bounds Fixes an issue with the camera dock bounds calculation, where the banner height was being accounted for twice, leading to incorrect dock height and excessive spacing. The height is now correctly based on the media are height, which already factors in the banner height. Additionally, the top position of the camera dock is now calculated using the media area's top position, correctly factoring in the margin of the media area. --- .../ui/components/layout/layout-manager/layoutEngine.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx index 7fe1229da3..eeb2dcaed7 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx +++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx @@ -69,7 +69,6 @@ const LayoutEngine = () => { return cameraDockBounds; } - const navBarHeight = calculatesNavbarHeight(); const hasPresentation = isPresentationEnabled && slidesLength !== 0; const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare @@ -78,9 +77,9 @@ const LayoutEngine = () => { if (!isOpen || isGeneralMediaOff) { cameraDockBounds.width = mediaAreaBounds.width; cameraDockBounds.maxWidth = mediaAreaBounds.width; - cameraDockBounds.height = mediaAreaBounds.height - bannerAreaHeight(); + cameraDockBounds.height = mediaAreaBounds.height; cameraDockBounds.maxHeight = mediaAreaBounds.height; - cameraDockBounds.top = navBarHeight + bannerAreaHeight(); + cameraDockBounds.top = mediaAreaBounds.top; cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0; cameraDockBounds.right = isRTL ? sidebarSize : null; } From 5f74d750e54635392a31e379a97ac3ed7df7c7ab Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 9 Oct 2024 16:15:54 -0300 Subject: [PATCH 6/6] feat(layout): add new hidden layout `MEDIA_ONLY` Introduces a new layout type called `MEDIA_ONLY`, which shows only media elements(presentation area and cameras). This layout follows the same heuristics used by the smart layout to arrange presentation and cameras in the screen. --- .../imports/ui/components/layout/enums.js | 4 + .../layout/layout-manager/layoutEngine.jsx | 4 + .../layout/layout-manager/mediaOnlyLayout.jsx | 525 ++++++++++++++++++ .../imports/ui/components/layout/utils.js | 7 + .../ui/components/nav-bar/component.jsx | 3 +- 5 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 bigbluebutton-html5/imports/ui/components/layout/layout-manager/mediaOnlyLayout.jsx diff --git a/bigbluebutton-html5/imports/ui/components/layout/enums.js b/bigbluebutton-html5/imports/ui/components/layout/enums.js index 7300b080d4..5ce0e95a4f 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/enums.js +++ b/bigbluebutton-html5/imports/ui/components/layout/enums.js @@ -6,6 +6,7 @@ export const LAYOUT_TYPE = { CAMERAS_ONLY: 'camerasOnly', PRESENTATION_ONLY: 'presentationOnly', PARTICIPANTS_AND_CHAT_ONLY: 'participantsAndChatOnly', + MEDIA_ONLY: 'mediaOnly', }; export const DEVICE_TYPE = { @@ -18,6 +19,8 @@ export const DEVICE_TYPE = { export const SMALL_VIEWPORT_BREAKPOINT = 640; +export const MEDIA_ONLY_LAYOUT_MARGIN = 10; + export const CAMERADOCK_POSITION = { CONTENT_TOP: 'contentTop', CONTENT_RIGHT: 'contentRight', @@ -31,6 +34,7 @@ export const HIDDEN_LAYOUTS = [ LAYOUT_TYPE.CAMERAS_ONLY, LAYOUT_TYPE.PRESENTATION_ONLY, LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY, + LAYOUT_TYPE.MEDIA_ONLY, ]; export const LAYOUT_ELEMENTS = { diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx index eeb2dcaed7..9c240b8d39 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx +++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/layoutEngine.jsx @@ -14,6 +14,7 @@ import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import useSettings from '/imports/ui/services/settings/hooks/useSettings'; import { SETTINGS } from '/imports/ui/services/settings/enums'; import { useIsPresentationEnabled } from '/imports/ui/services/features'; +import MediaOnlyLayout from './mediaOnlyLayout'; const LayoutEngine = () => { const bannerBarInput = layoutSelectInput((i) => i.bannerBar); @@ -354,6 +355,9 @@ const LayoutEngine = () => { case LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY: layout?.setAttribute('data-layout', LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY); return ; + case LAYOUT_TYPE.MEDIA_ONLY: + layout?.setAttribute('data-layout', LAYOUT_TYPE.MEDIA_ONLY); + return ; default: layout?.setAttribute('data-layout', LAYOUT_TYPE.CUSTOM_LAYOUT); return ; diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/mediaOnlyLayout.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/mediaOnlyLayout.jsx new file mode 100644 index 0000000000..a5d198e566 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/mediaOnlyLayout.jsx @@ -0,0 +1,525 @@ +import { useEffect, useRef } from 'react'; +import { throttle } from '/imports/utils/throttle'; +import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context'; +import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues'; +import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState'; +import { ACTIONS, CAMERADOCK_POSITION, MEDIA_ONLY_LAYOUT_MARGIN } from '/imports/ui/components/layout/enums'; +import { defaultsDeep } from '/imports/utils/array-utils'; +import Session from '/imports/ui/services/storage/in-memory'; +import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; + +const windowWidth = () => window.document.documentElement.clientWidth; +const windowHeight = () => window.document.documentElement.clientHeight; + +const MediaOnlyLayout = (props) => { + const { isMobile } = props; + + function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; + } + + const input = layoutSelect((i) => i.input); + const deviceType = layoutSelect((i) => i.deviceType); + const Settings = getSettingsSingletonInstance(); + const { isRTL } = Settings.application; + const fullscreen = layoutSelect((i) => i.fullscreen); + const fontSize = layoutSelect((i) => i.fontSize); + const currentPanelType = layoutSelect((i) => i.currentPanelType); + + const navbarInput = layoutSelectInput((i) => i.navBar); + const presentationInput = layoutSelectInput((i) => i.presentation); + const cameraDockInput = layoutSelectInput((i) => i.cameraDock); + const actionbarInput = layoutSelectInput((i) => i.actionBar); + const externalVideoInput = layoutSelectInput((i) => i.externalVideo); + const genericMainContentInput = layoutSelectInput((i) => i.genericMainContent); + const screenShareInput = layoutSelectInput((i) => i.screenShare); + const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes); + const layoutContextDispatch = layoutDispatch(); + + const prevDeviceType = usePrevious(deviceType); + const { isPresentationEnabled } = props; + + useEffect(() => { + window.addEventListener('resize', () => { + layoutContextDispatch({ + type: ACTIONS.SET_BROWSER_SIZE, + value: { + width: window.document.documentElement.clientWidth, + height: window.document.documentElement.clientHeight, + }, + }); + }); + }, []); + + const calculatesSlideSize = (mediaAreaBounds) => { + const { currentSlide } = presentationInput; + + if (currentSlide.size.width === 0 && currentSlide.size.height === 0) { + return { + width: 0, + height: 0, + }; + } + + let slideWidth; + let slideHeight; + + slideWidth = (currentSlide.size.width * mediaAreaBounds.height) / currentSlide.size.height; + slideHeight = mediaAreaBounds.height; + + if (slideWidth > mediaAreaBounds.width) { + slideWidth = mediaAreaBounds.width; + slideHeight = (currentSlide.size.height * mediaAreaBounds.width) / currentSlide.size.width; + } + + return { + width: slideWidth, + height: slideHeight, + }; + }; + + const calculatesScreenShareSize = (mediaAreaBounds) => { + const { width = 0, height = 0 } = screenShareInput; + + if (width === 0 && height === 0) return { width, height }; + + let screeShareWidth; + let screeShareHeight; + + screeShareWidth = (width * mediaAreaBounds.height) / height; + screeShareHeight = mediaAreaBounds.height; + + if (screeShareWidth > mediaAreaBounds.width) { + screeShareWidth = mediaAreaBounds.width; + screeShareHeight = (height * mediaAreaBounds.width) / width; + } + + return { + width: screeShareWidth, + height: screeShareHeight, + }; + }; + + const calculatesCameraDockBounds = (mediaAreaBounds, mediaBounds, sidebarSize) => { + const { baseCameraDockBounds } = props; + const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize); + + if (Object.keys(baseBounds).length > 0) { + baseBounds.isCameraHorizontal = false; + return baseBounds; + } + + const { presentationToolbarMinWidth } = DEFAULT_VALUES; + + const cameraDockBounds = {}; + + cameraDockBounds.isCameraHorizontal = false; + + const mediaBoundsWidth = mediaBounds.width > presentationToolbarMinWidth + && !isMobile + ? mediaBounds.width + : presentationToolbarMinWidth; + cameraDockBounds.top = mediaAreaBounds.top; + cameraDockBounds.left = mediaAreaBounds.left; + cameraDockBounds.right = isRTL ? sidebarSize : null; + cameraDockBounds.zIndex = 1; + + if (mediaBounds.width < mediaAreaBounds.width) { + cameraDockBounds.width = mediaAreaBounds.width + - mediaBoundsWidth - MEDIA_ONLY_LAYOUT_MARGIN; + cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8; + cameraDockBounds.height = mediaAreaBounds.height; + cameraDockBounds.maxHeight = mediaAreaBounds.height; + cameraDockBounds.isCameraHorizontal = true; + cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_LEFT; + } else { + cameraDockBounds.width = mediaAreaBounds.width; + cameraDockBounds.maxWidth = mediaAreaBounds.width; + cameraDockBounds.height = mediaAreaBounds.height + - mediaBounds.height - MEDIA_ONLY_LAYOUT_MARGIN; + cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8; + cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_TOP; + } + + cameraDockBounds.minWidth = cameraDockBounds.width; + cameraDockBounds.minHeight = cameraDockBounds.height; + + return cameraDockBounds; + }; + + const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => { + const { isOpen, slidesLength } = presentationInput; + const { hasExternalVideo } = externalVideoInput; + const { genericContentId } = genericMainContentInput; + const { hasScreenShare } = screenShareInput; + const { isPinned: isSharedNotesPinned } = sharedNotesInput; + + const hasPresentation = isPresentationEnabled && slidesLength !== 0; + const isGeneralMediaOff = !hasPresentation && !hasExternalVideo + && !hasScreenShare && !isSharedNotesPinned && !genericContentId; + + const mediaBounds = {}; + const { element: fullscreenElement } = fullscreen; + + if (!isOpen || isGeneralMediaOff) { + mediaBounds.width = 0; + mediaBounds.height = 0; + mediaBounds.top = 0; + mediaBounds.left = !isRTL ? 0 : null; + mediaBounds.right = isRTL ? 0 : null; + mediaBounds.zIndex = 0; + return mediaBounds; + } + + if ( + fullscreenElement === 'Presentation' + || fullscreenElement === 'Screenshare' + || fullscreenElement === 'ExternalVideo' + || fullscreenElement === 'GenericContent' + ) { + mediaBounds.width = windowWidth(); + mediaBounds.height = windowHeight(); + mediaBounds.top = 0; + mediaBounds.left = !isRTL ? 0 : null; + mediaBounds.right = isRTL ? 0 : null; + mediaBounds.zIndex = 99; + return mediaBounds; + } + + const mediaContentSize = hasScreenShare ? screenShareSize : slideSize; + + if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) { + if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0 + && !hasExternalVideo && !genericContentId) { + if (mediaContentSize.width < mediaAreaBounds.width && !isMobile) { + if (mediaContentSize.width < mediaAreaBounds.width * 0.8) { + mediaBounds.width = mediaContentSize.width; + } else { + mediaBounds.width = mediaAreaBounds.width * 0.8; + } + mediaBounds.height = mediaAreaBounds.height; + mediaBounds.top = mediaAreaBounds.top; + const sizeValue = mediaAreaBounds.left + (mediaAreaBounds.width - mediaBounds.width); + mediaBounds.left = !isRTL ? sizeValue : null; + mediaBounds.right = isRTL ? sidebarSize : null; + } else { + if (mediaContentSize.height < mediaAreaBounds.height * 0.8) { + mediaBounds.height = mediaContentSize.height; + } else { + mediaBounds.height = mediaAreaBounds.height * 0.8; + } + mediaBounds.width = mediaAreaBounds.width; + mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height); + const sizeValue = mediaAreaBounds.left; + mediaBounds.left = !isRTL ? sizeValue : null; + mediaBounds.right = isRTL ? sidebarSize : null; + } + } else { + mediaBounds.width = mediaAreaBounds.width; + mediaBounds.height = mediaAreaBounds.height * 0.8; + mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height); + const sizeValue = mediaAreaBounds.left; + mediaBounds.left = !isRTL ? sizeValue : null; + mediaBounds.right = isRTL ? sidebarSize : null; + } + } else { + mediaBounds.width = mediaAreaBounds.width; + mediaBounds.height = mediaAreaBounds.height; + mediaBounds.top = mediaAreaBounds.top; + const sizeValue = mediaAreaBounds.left; + mediaBounds.left = !isRTL ? sizeValue : null; + mediaBounds.right = isRTL ? sidebarSize : null; + } + mediaBounds.zIndex = 1; + + return mediaBounds; + }; + + const calculatesLayout = () => { + const { + calculatesNavbarBounds, + calculatesActionbarBounds, + calculatesSidebarNavBounds, + calculatesSidebarContentBounds, + calculatesMediaAreaBounds, + } = props; + const { camerasMargin } = DEFAULT_VALUES; + + const sidebarNavBounds = calculatesSidebarNavBounds(); + const sidebarContentBounds = calculatesSidebarContentBounds(0); + const mediaAreaBounds = calculatesMediaAreaBounds(0, 0, MEDIA_ONLY_LAYOUT_MARGIN); + const navbarBounds = calculatesNavbarBounds(mediaAreaBounds); + const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds); + const slideSize = calculatesSlideSize(mediaAreaBounds); + const screenShareSize = calculatesScreenShareSize(mediaAreaBounds); + const sidebarSize = 0; + const mediaBounds = calculatesMediaBounds( + mediaAreaBounds, + slideSize, + sidebarSize, + screenShareSize, + ); + const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize); + const horizontalCameraDiff = cameraDockBounds.isCameraHorizontal + ? cameraDockBounds.width + camerasMargin * 2 + : 0; + + layoutContextDispatch({ + type: ACTIONS.SET_NAVBAR_OUTPUT, + value: { + display: navbarInput.hasNavBar, + width: navbarBounds.width, + height: navbarBounds.height, + top: navbarBounds.top, + left: navbarBounds.left, + tabOrder: DEFAULT_VALUES.navBarTabOrder, + zIndex: navbarBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_ACTIONBAR_OUTPUT, + value: { + display: actionbarInput.hasActionBar, + width: actionbarBounds.width, + height: actionbarBounds.height, + innerHeight: actionbarBounds.innerHeight, + top: actionbarBounds.top, + left: actionbarBounds.left, + padding: actionbarBounds.padding, + tabOrder: DEFAULT_VALUES.actionBarTabOrder, + zIndex: actionbarBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT, + value: { + display: false, + minWidth: 0, + width: 0, + maxWidth: 0, + height: 0, + top: 0, + left: 0, + right: 0, + tabOrder: DEFAULT_VALUES.sidebarNavTabOrder, + isResizable: false, + zIndex: sidebarNavBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE, + value: { + top: false, + right: false, + bottom: false, + left: false, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT, + value: { + display: false, + minWidth: 0, + width: 0, + maxWidth: 0, + height: 0, + top: 0, + left: 0, + right: 0, + currentPanelType, + tabOrder: DEFAULT_VALUES.sidebarContentTabOrder, + isResizable: false, + zIndex: sidebarContentBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE, + value: { + top: false, + right: false, + bottom: false, + left: false, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_MEDIA_AREA_SIZE, + value: { + width: mediaAreaBounds.width, + height: mediaAreaBounds.height, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_OUTPUT, + value: { + display: cameraDockInput.numCameras > 0, + position: cameraDockBounds.position, + minWidth: cameraDockBounds.minWidth, + width: cameraDockBounds.width, + maxWidth: cameraDockBounds.maxWidth, + minHeight: cameraDockBounds.minHeight, + height: cameraDockBounds.height, + maxHeight: cameraDockBounds.maxHeight, + top: cameraDockBounds.top, + left: cameraDockBounds.left, + right: cameraDockBounds.right, + tabOrder: 4, + isDraggable: false, + resizableEdge: { + top: false, + right: false, + bottom: false, + left: false, + }, + zIndex: cameraDockBounds.zIndex, + focusedId: input.cameraDock.focusedId, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_PRESENTATION_OUTPUT, + value: { + display: presentationInput.isOpen, + width: mediaBounds.width, + height: mediaBounds.height, + top: mediaBounds.top, + left: mediaBounds.left, + right: isRTL ? mediaBounds.right + horizontalCameraDiff : null, + tabOrder: DEFAULT_VALUES.presentationTabOrder, + isResizable: false, + zIndex: mediaBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SCREEN_SHARE_OUTPUT, + value: { + width: mediaBounds.width, + height: mediaBounds.height, + top: mediaBounds.top, + left: mediaBounds.left, + right: isRTL ? mediaBounds.right + horizontalCameraDiff : null, + zIndex: mediaBounds.zIndex, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT, + value: { + width: mediaBounds.width, + height: mediaBounds.height, + top: mediaBounds.top, + left: mediaBounds.left, + right: isRTL ? mediaBounds.right + horizontalCameraDiff : null, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_GENERIC_CONTENT_OUTPUT, + value: { + width: mediaBounds.width, + height: mediaBounds.height, + top: mediaBounds.top, + left: mediaBounds.left, + right: isRTL ? mediaBounds.right + horizontalCameraDiff : null, + }, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_SHARED_NOTES_OUTPUT, + value: { + width: mediaBounds.width, + height: mediaBounds.height, + top: mediaBounds.top, + left: mediaBounds.left, + right: isRTL ? mediaBounds.right + horizontalCameraDiff : null, + }, + }); + }; + + const throttledCalculatesLayout = throttle(() => calculatesLayout(), 50, { + trailing: true, + leading: true, + }); + + const init = () => { + layoutContextDispatch({ + type: ACTIONS.SET_LAYOUT_INPUT, + value: (prevInput) => { + const { + sidebarContent, presentation, cameraDock, + externalVideo, genericMainContent, screenShare, sharedNotes, + } = prevInput; + const { sidebarContentPanel } = sidebarContent; + return defaultsDeep( + { + sidebarNavigation: { + isOpen: false, + }, + sidebarContent: { + isOpen: false, + sidebarContentPanel, + }, + SidebarContentHorizontalResizer: { + isOpen: false, + }, + presentation: { + isOpen: presentation.isOpen, + slidesLength: presentation.slidesLength, + currentSlide: { + ...presentation.currentSlide, + }, + }, + cameraDock: { + numCameras: cameraDock.numCameras, + }, + externalVideo: { + hasExternalVideo: externalVideo.hasExternalVideo, + }, + genericMainContent: { + genericContentId: genericMainContent.genericContentId, + }, + screenShare: { + hasScreenShare: screenShare.hasScreenShare, + width: screenShare.width, + height: screenShare.height, + }, + sharedNotes: { + isPinned: sharedNotes.isPinned, + }, + }, + INITIAL_INPUT_STATE, + ); + }, + }); + Session.setItem('layoutReady', true); + throttledCalculatesLayout(); + }; + + useEffect(() => { + if (deviceType === null) return () => null; + + if (deviceType !== prevDeviceType) { + // reset layout if deviceType changed + // not all options is supported in all devices + init(); + } else { + throttledCalculatesLayout(); + } + return () => {}; + }, [input, deviceType, isRTL, fontSize, fullscreen, isPresentationEnabled]); + + return null; +}; + +export default MediaOnlyLayout; diff --git a/bigbluebutton-html5/imports/ui/components/layout/utils.js b/bigbluebutton-html5/imports/ui/components/layout/utils.js index 6f4b9b88e8..97f948ca70 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/utils.js +++ b/bigbluebutton-html5/imports/ui/components/layout/utils.js @@ -117,5 +117,12 @@ const LAYOUTS_SYNC = { [SYNC.PROPAGATE_ELEMENTS]: [], [SYNC.REPLICATE_ELEMENTS]: [], }, + [LAYOUT_TYPE.MEDIA_ONLY]: { + [SYNC.PROPAGATE_ELEMENTS]: [], + [SYNC.REPLICATE_ELEMENTS]: [ + LAYOUT_ELEMENTS.FOCUSED_CAMERA, + LAYOUT_ELEMENTS.PRESENTATION_STATE, + ], + }, }; export { suportedLayouts, LAYOUTS_SYNC }; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 2b4ff891df..5a35bfdc97 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -288,7 +288,8 @@ class NavBar extends Component { const { selectedLayout } = Settings.application; const shouldShowNavBarToggleButton = selectedLayout !== LAYOUT_TYPE.CAMERAS_ONLY && selectedLayout !== LAYOUT_TYPE.PRESENTATION_ONLY - && selectedLayout !== LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY; + && selectedLayout !== LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY + && selectedLayout !== LAYOUT_TYPE.MEDIA_ONLY; const APP_CONFIG = window.meetingClientSettings?.public?.app; const enableTalkingIndicator = APP_CONFIG?.enableTalkingIndicator;