diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index f67e33d967..c4c7c4260c 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -30,12 +30,11 @@ import PresentationAreaContainer from '../presentation/presentation-area/contain import ScreenshareContainer from '../screenshare/container'; import ExternalVideoContainer from '../external-video-player/container'; import Styled from './styles'; -import { LAYOUT_TYPE, DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT } from '../layout/enums'; +import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT } from '../layout/enums'; import { isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop, } from '../layout/utils'; import LayoutEngine from '../layout/layout-manager/layoutEngine'; -import getFromUserSettings from '/imports/ui/services/users-settings'; import NavBarContainer from '../nav-bar/container'; import SidebarNavigationContainer from '../sidebar-navigation/container'; import SidebarContentContainer from '../sidebar-content/container'; @@ -46,24 +45,16 @@ import Settings from '/imports/ui/services/settings'; import { registerTitleView } from '/imports/utils/dom-utils'; import Notifications from '../notifications/container'; import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles'; -import MediaService from '/imports/ui/components/media/service'; import ActionsBarContainer from '../actions-bar/container'; -import { updateSettings } from '/imports/ui/components/settings/service'; +import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine'; const MOBILE_MEDIA = 'only screen and (max-width: 40em)'; const APP_CONFIG = Meteor.settings.public.app; const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize; const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize; const OVERRIDE_LOCALE = APP_CONFIG.defaultSettings.application.overrideLocale; -const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentation; const LAYOUT_CONFIG = Meteor.settings.public.layout; -const equalDouble = (n1, n2) => { - const precision = 0.01; - - return Math.abs(n1 - n2) <= precision; -}; - const intlMessages = defineMessages({ userListLabel: { id: 'app.userList.label', @@ -159,18 +150,7 @@ class App extends Component { intl, validIOSVersion, layoutContextDispatch, - meetingLayout, - settingsLayout, - cameraWidth, - cameraHeight, - layoutPresOpen, - layoutCamPosition, - layoutFocusedCam, - layoutRate, - horizontalPosition, isRTL, - shouldShowScreenshare, - shouldShowExternalVideo, } = this.props; const { browserName } = browserInfo; const { osName } = deviceInfo; @@ -193,57 +173,6 @@ class App extends Component { value: parseInt(fontSize.slice(0, -2), 10), }); - const userLayout = LAYOUT_TYPE[getFromUserSettings('bbb_change_layout', false)]; - Settings.application.selectedLayout = settingsLayout - || userLayout - || meetingLayout; - - let selectedLayout = Settings.application.selectedLayout; - if (isMobile()) { - selectedLayout = selectedLayout === 'custom' ? 'smart' : selectedLayout; - Settings.application.selectedLayout = selectedLayout; - } - Settings.save(); - - const initialPresentation = !getFromUserSettings('bbb_hide_presentation', HIDE_PRESENTATION || !layoutPresOpen) || shouldShowScreenshare || shouldShowExternalVideo; - MediaService.setPresentationIsOpen(layoutContextDispatch, initialPresentation); - - if (selectedLayout === 'custom') { - setTimeout(() => { - - layoutContextDispatch({ - type: ACTIONS.SET_FOCUSED_CAMERA_ID, - value: layoutFocusedCam, - }); - - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_POSITION, - value: layoutCamPosition, - }); - - if (!equalDouble(layoutRate, 0)) { - let w, h; - if (horizontalPosition) { - w = window.innerWidth * layoutRate; - h = cameraHeight; - } else { - w = cameraWidth; - h = window.innerHeight * layoutRate; - } - - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_SIZE, - value: { - width: w, - height: h, - browserWidth: window.innerWidth, - browserHeight: window.innerHeight, - } - }); - } - }, 0); - } - const body = document.getElementsByTagName('body')[0]; if (browserName) { @@ -286,146 +215,11 @@ class App extends Component { intl, mountModal, deviceType, - meetingLayout, - meetingLayoutUpdatedAt, - presentationIsOpen, - focusedCamera, - cameraPosition, - presentationVideoRate, - cameraWidth, - cameraHeight, - cameraIsResizing, - isPresenter, - isModerator, - layoutPresOpen, - layoutIsResizing, - layoutCamPosition, - layoutFocusedCam, - layoutRate, - horizontalPosition, - selectedLayout, // layout name - pushLayout, // is layout pushed - pushLayoutMeeting, - layoutContextDispatch, mountRandomUserModal, - setPushLayout, - setMeetingLayout, } = this.props; this.renderDarkMode(); - const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout; - const pushLayoutMeetingDidChange = pushLayoutMeeting !== prevProps.pushLayoutMeeting; - const shouldSwitchLayout = isPresenter - ? meetingLayoutDidChange - : (meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting; - - if (shouldSwitchLayout) { - - let contextLayout = meetingLayout; - if (isMobile()) { - contextLayout = meetingLayout === 'custom' ? 'smart' : meetingLayout; - } - - layoutContextDispatch({ - type: ACTIONS.SET_LAYOUT_TYPE, - value: contextLayout, - }); - - updateSettings({ - application: { - ...Settings.application, - selectedLayout: contextLayout, - }, - }); - } - - if (pushLayoutMeetingDidChange) { - updateSettings({ - application: { - ...Settings.application, - pushLayout: pushLayoutMeeting, - }, - }); - } - - if (meetingLayout === "custom" && !isPresenter) { - - if (layoutFocusedCam !== prevProps.layoutFocusedCam - || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { - - layoutContextDispatch({ - type: ACTIONS.SET_FOCUSED_CAMERA_ID, - value: layoutFocusedCam, - }); - } - - if (layoutCamPosition !== prevProps.layoutCamPosition - || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { - - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_POSITION, - value: layoutCamPosition, - }); - } - - if (!equalDouble(layoutRate, prevProps.layoutRate) - || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { - - let w, h; - if (horizontalPosition) { - w = window.innerWidth * layoutRate; - h = cameraHeight; - } else { - w = cameraWidth; - h = window.innerHeight * layoutRate; - } - - if (layoutIsResizing !== prevProps.layoutIsResizing) { - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING, - value: layoutIsResizing, - }); - } - - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_SIZE, - value: { - width: w, - height: h, - browserWidth: window.innerWidth, - browserHeight: window.innerHeight, - } - }); - } - - if (layoutPresOpen !== prevProps.layoutPresOpen - || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { - - layoutContextDispatch({ - type: ACTIONS.SET_PRESENTATION_IS_OPEN, - value: layoutPresOpen, - }); - } - } - - const layoutChanged = presentationIsOpen !== prevProps.presentationIsOpen - || selectedLayout !== prevProps.selectedLayout - || cameraIsResizing !== prevProps.cameraIsResizing - || cameraPosition !== prevProps.cameraPosition - || focusedCamera !== prevProps.focusedCamera - || !equalDouble(presentationVideoRate, prevProps.presentationVideoRate); - - if ((pushLayout && layoutChanged) // change layout sizes / states - || (pushLayout !== prevProps.pushLayout) // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers - ) { - if (isPresenter) { - setMeetingLayout(); - } else if (isModerator) { - setPushLayout(); - } - } - if (mountRandomUserModal) mountModal(); if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) { @@ -623,6 +417,68 @@ class App extends Component { : DarkReader.disable(); } + mountPushLayoutEngine() { + const { + cameraWidth, + cameraHeight, + cameraIsResizing, + cameraPosition, + focusedCamera, + horizontalPosition, + isLayoutMeetingResizing, + isPresenter, + isModerator, + layoutContextDispatch, + meetingLayout, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, + meetingPresentationIsOpen, + meetingLayoutUpdatedAt, + presentationIsOpen, + presentationVideoRate, + pushLayout, + pushLayoutMeeting, + selectedLayout, + setPushLayout, + setMeetingLayout, + shouldShowScreenshare, + shouldShowExternalVideo, + } = this.props; + + return ( + + ); + } + render() { const { customStyle, @@ -641,6 +497,7 @@ class App extends Component { <> + {this.mountPushLayoutEngine()} { const { actionsbar, - meetingLayout, - meetingLayoutUpdatedAt, selectedLayout, pushLayout, pushLayoutMeeting, @@ -72,11 +70,13 @@ const AppContainer = (props) => { isPresenter, randomlySelectedUser, isModalOpen, - presentationIsOpen: layoutPresOpen, - isResizing: layoutIsResizing, - cameraPosition: layoutCamPosition, - focusedCamera: layoutFocusedCam, - presentationVideoRate: layoutRate, + meetingLayout, + meetingLayoutUpdatedAt, + meetingPresentationIsOpen, + isLayoutMeetingResizing, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, ...otherProps } = props; @@ -87,7 +87,6 @@ const AppContainer = (props) => { const cameraDock = layoutSelectOutput((i) => i.cameraDock); const cameraDockInput = layoutSelectInput((i) => i.cameraDock); const presentation = layoutSelectInput((i) => i.presentation); - const layoutType = layoutSelect((i) => i.layoutType); const deviceType = layoutSelect((i) => i.deviceType); const layoutContextDispatch = layoutDispatch(); @@ -154,11 +153,11 @@ const AppContainer = (props) => { cameraWidth: cameraDock.width, cameraHeight: cameraDock.height, cameraIsResizing: cameraDockInput.isResizing, - layoutPresOpen, - layoutIsResizing, - layoutCamPosition, - layoutFocusedCam, - layoutRate, + meetingPresentationIsOpen, + isLayoutMeetingResizing, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, horizontalPosition, deviceType, layoutContextDispatch, @@ -222,8 +221,17 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) randomlySelectedUser, } = currentMeeting; - const meetingLayout = LayoutMeetings.findOne({ meetingId: Auth.meetingID }) || {}; - const { layout, pushLayout: pushLayoutMeeting, layoutUpdatedAt, presentationIsOpen, isResizing, cameraPosition, focusedCamera, presentationVideoRate } = meetingLayout; + const meetingLayoutObj = LayoutMeetings.findOne({ meetingId: Auth.meetingID }) || {}; + const { + layout: meetingLayout, + pushLayout: pushLayoutMeeting, + layoutUpdatedAt: meetingLayoutUpdatedAt, + presentationIsOpen: meetingPresentationIsOpen, + isResizing: isMeetingLayoutResizing, + cameraPosition: meetingLayoutCameraPosition, + focusedCamera: meetingLayoutFocusedCamera, + presentationVideoRate: meetingLayoutVideoRate, + } = meetingLayoutObj; if (currentUser && !currentUser.approved) { baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage)); @@ -267,13 +275,13 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) currentUserId: currentUser?.userId, isPresenter, isModerator: currentUser?.role === ROLE_MODERATOR, - meetingLayout: layout, - meetingLayoutUpdatedAt: layoutUpdatedAt, - presentationIsOpen, - isResizing, - cameraPosition, - focusedCamera, - presentationVideoRate, + meetingLayout, + meetingLayoutUpdatedAt, + meetingPresentationIsOpen, + isMeetingLayoutResizing, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, selectedLayout, pushLayout, pushLayoutMeeting, diff --git a/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx b/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx new file mode 100644 index 0000000000..ae95d1fb3c --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx @@ -0,0 +1,263 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Meteor } from 'meteor/meteor'; +import getFromUserSettings from '/imports/ui/services/users-settings'; +import Settings from '/imports/ui/services/settings'; +import MediaService from '/imports/ui/components/media/service'; +import { LAYOUT_TYPE, ACTIONS } from '../enums'; +import { isMobile } from '../utils'; +import { updateSettings } from '/imports/ui/components/settings/service'; + +const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentation; + +const equalDouble = (n1, n2) => { + const precision = 0.01; + + return Math.abs(n1 - n2) <= precision; +}; + +const propTypes = { + cameraWidth: PropTypes.number, + cameraHeight: PropTypes.number, + cameraIsResizing: PropTypes.bool, + cameraPosition: PropTypes.string, + focusedCamera: PropTypes.string, + horizontalPosition: PropTypes.string, + isLayoutMeetingResizing: PropTypes.bool, + isPresenter: PropTypes.bool, + isModerator: PropTypes.bool, + layoutContextDispatch: PropTypes.func, + meetingLayout: PropTypes.string, + meetingLayoutCameraPosition: PropTypes.string, + meetingLayoutFocusedCamera: PropTypes.string, + meetingLayoutVideoRate: PropTypes.number, + meetingPresentationIsOpen: PropTypes.bool, + meetingLayoutUpdatedAt: PropTypes.number, + presentationIsOpen: PropTypes.bool, + presentationVideoRate: PropTypes.number, + pushLayout: PropTypes.bool, + pushLayoutMeeting: PropTypes.bool, + selectedLayout: PropTypes.string, + setPushLayout: PropTypes.func, + setMeetingLayout: PropTypes.func, + shouldShowScreenshare: PropTypes.bool, + shouldShowExternalVideo: PropTypes.bool, +}; + +class PushLayoutEngine extends React.Component { + constructor(props) { + super(props); + } + + componentDidMount() { + const { + cameraWidth, + cameraHeight, + horizontalPosition, + layoutContextDispatch, + meetingLayout, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, + meetingPresentationIsOpen, + shouldShowScreenshare, + shouldShowExternalVideo, + } = this.props; + + const userLayout = LAYOUT_TYPE[getFromUserSettings('bbb_change_layout', false)]; + Settings.application.selectedLayout = userLayout || meetingLayout; + + let selectedLayout = Settings.application.selectedLayout; + if (isMobile()) { + selectedLayout = selectedLayout === 'custom' ? 'smart' : selectedLayout; + Settings.application.selectedLayout = selectedLayout; + } + Settings.save(); + + const initialPresentation = !getFromUserSettings('bbb_hide_presentation', HIDE_PRESENTATION || !meetingPresentationIsOpen) || shouldShowScreenshare || shouldShowExternalVideo; + MediaService.setPresentationIsOpen(layoutContextDispatch, initialPresentation); + + if (selectedLayout === 'custom') { + setTimeout(() => { + layoutContextDispatch({ + type: ACTIONS.SET_FOCUSED_CAMERA_ID, + value: meetingLayoutFocusedCamera, + }); + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_POSITION, + value: meetingLayoutCameraPosition, + }); + + if (!equalDouble(meetingLayoutVideoRate, 0)) { + let w, h; + if (horizontalPosition) { + w = window.innerWidth * meetingLayoutVideoRate; + h = cameraHeight; + } else { + w = cameraWidth; + h = window.innerHeight * meetingLayoutVideoRate; + } + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_SIZE, + value: { + width: w, + height: h, + browserWidth: window.innerWidth, + browserHeight: window.innerHeight, + } + }); + } + }, 0); + } + } + + componentDidUpdate(prevProps) { + const { + cameraWidth, + cameraHeight, + cameraIsResizing, + cameraPosition, + focusedCamera, + horizontalPosition, + isLayoutMeetingResizing, + isModerator, + isPresenter, + layoutContextDispatch, + meetingLayout, + meetingLayoutUpdatedAt, + meetingPresentationIsOpen, + meetingLayoutCameraPosition, + meetingLayoutFocusedCamera, + meetingLayoutVideoRate, + presentationIsOpen, + presentationVideoRate, + pushLayout, + pushLayoutMeeting, + selectedLayout, + setMeetingLayout, + setPushLayout, + } = this.props; + + const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout; + const pushLayoutMeetingDidChange = pushLayoutMeeting !== prevProps.pushLayoutMeeting; + const shouldSwitchLayout = isPresenter + ? meetingLayoutDidChange + : (meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting; + + if (shouldSwitchLayout) { + + let contextLayout = meetingLayout; + if (isMobile()) { + contextLayout = meetingLayout === 'custom' ? 'smart' : meetingLayout; + } + + layoutContextDispatch({ + type: ACTIONS.SET_LAYOUT_TYPE, + value: contextLayout, + }); + + updateSettings({ + application: { + ...Settings.application, + selectedLayout: contextLayout, + }, + }); + } + + if (pushLayoutMeetingDidChange) { + updateSettings({ + application: { + ...Settings.application, + pushLayout: pushLayoutMeeting, + }, + }); + } + + if (meetingLayout === "custom" && !isPresenter) { + + if (meetingLayoutFocusedCamera !== prevProps.meetingLayoutFocusedCamera + || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { + + layoutContextDispatch({ + type: ACTIONS.SET_FOCUSED_CAMERA_ID, + value: meetingLayoutFocusedCamera, + }); + } + + if (meetingLayoutCameraPosition !== prevProps.meetingLayoutCameraPosition + || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_POSITION, + value: meetingLayoutCameraPosition, + }); + } + + if (!equalDouble(meetingLayoutVideoRate, prevProps.meetingLayoutVideoRate) + || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { + + let w, h; + if (horizontalPosition) { + w = window.innerWidth * meetingLayoutVideoRate; + h = cameraHeight; + } else { + w = cameraWidth; + h = window.innerHeight * meetingLayoutVideoRate; + } + + if (isLayoutMeetingResizing !== prevProps.isLayoutMeetingResizing) { + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING, + value: isLayoutMeetingResizing, + }); + } + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_SIZE, + value: { + width: w, + height: h, + browserWidth: window.innerWidth, + browserHeight: window.innerHeight, + } + }); + } + + if (meetingPresentationIsOpen !== prevProps.meetingPresentationIsOpen + || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) { + + layoutContextDispatch({ + type: ACTIONS.SET_PRESENTATION_IS_OPEN, + value: meetingPresentationIsOpen, + }); + } + } + + const layoutChanged = presentationIsOpen !== prevProps.presentationIsOpen + || selectedLayout !== prevProps.selectedLayout + || cameraIsResizing !== prevProps.cameraIsResizing + || cameraPosition !== prevProps.cameraPosition + || focusedCamera !== prevProps.focusedCamera + || !equalDouble(presentationVideoRate, prevProps.presentationVideoRate); + + if ((pushLayout && layoutChanged) // change layout sizes / states + || (pushLayout !== prevProps.pushLayout) // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers + ) { + if (isPresenter) { + setMeetingLayout(); + } else if (isModerator) { + setPushLayout(); + } + } + } + + render() { + return null; + } +}; + +PushLayoutEngine.propTypes = propTypes; + +export default PushLayoutEngine;