diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index 1e5859907752..ee5b39509011 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -690,12 +690,9 @@ class Presentation extends PureComponent { intl, fullscreenElementId, layoutContextDispatch, - userIsPresenter, } = this.props; const { tldrawAPI, isToolbarVisible } = this.state; - if (userIsPresenter && isToolbarVisible) return null; - return ( { } return ( - + @@ -353,7 +353,7 @@ const PresentationMenu = (props) => { }} actions={options} /> - + ); }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-menu/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-menu/styles.js index 98a3f3738702..709a903d3f48 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-menu/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-menu/styles.js @@ -33,11 +33,10 @@ const DropdownButton = styled.button` } `; -const Right = styled.div` +const Left = styled.div` cursor: pointer; position: absolute; - left: auto; - right: 0px; + left: 0px; z-index: 999; box-shadow: 0 4px 2px -2px rgba(0, 0, 0, 0.05); border-bottom: 1px solid ${colorWhite}; @@ -56,8 +55,8 @@ const Right = styled.div` } [dir="rtl"] & { - right: auto; - left : ${borderSize}; + right: 0px; + left: auto; } `; @@ -141,7 +140,7 @@ const ButtonIcon = styled(Icon)` export default { DropdownButton, - Right, + Left, ToastText, StatusIcon, ToastIcon, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 1a30a59523c2..697a9bd64407 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import _ from 'lodash'; import { TldrawApp, Tldraw } from '@tldraw/tldraw'; @@ -20,7 +19,6 @@ import PanToolInjector from './pan-tool-injector/component'; import { findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious, } from './utils'; -import PresentationOpsContainer from './presentation-ops-injector/container'; const SMALL_HEIGHT = 435; const SMALLEST_DOCK_HEIGHT = 475; @@ -78,8 +76,6 @@ export default function Whiteboard(props) { animations, isToolbarVisible, fullscreenRef, - fullscreen, - setIsToolbarVisible, fullscreenElementId, layoutContextDispatch, } = props; @@ -110,37 +106,6 @@ export default function Whiteboard(props) { const isMountedRef = React.useRef(true); const [isToolLocked, setIsToolLocked] = React.useState(tldrawAPI?.appState?.isToolLocked); const [bgShape, setBgShape] = React.useState(null); - const [labels, setLabels] = React.useState({ - closeLabel: '', - activeLable: '', - optionsLabel: '', - fullscreenLabel: '', - exitFullscreenLabel: '', - hideToolsDesc: '', - showToolsDesc: '', - downloading: '', - downloaded: '', - downloadFailed: '', - snapshotLabel: '', - whiteboardLabel: '', - }); - - React.useEffect(() => { - setLabels({ - closeLabel: intl.formatMessage({id: 'app.dropdown.close', description: 'Close button label'}), - activeLable: intl.formatMessage({id: 'app.dropdown.list.item.activeLabel', description: 'active item label'}), - optionsLabel: intl.formatMessage({id: 'app.navBar.settingsDropdown.optionsLabel', description: 'Options button label'}), - fullscreenLabel: intl.formatMessage({id: 'app.presentation.options.fullscreen', description: 'Fullscreen label'}), - exitFullscreenLabel: intl.formatMessage({id: 'app.presentation.options.exitFullscreen', description: 'Exit fullscreen label'}), - hideToolsDesc: intl.formatMessage({id: 'app.presentation.presentationToolbar.hideToolsDesc', description: 'Hide toolbar label'}), - showToolsDesc: intl.formatMessage({id: 'app.presentation.presentationToolbar.showToolsDesc', description: 'Show toolbar label'}), - downloading: intl.formatMessage({id: 'app.presentation.options.downloading', description: 'Downloading label'}), - downloaded: intl.formatMessage({id: 'app.presentation.options.downloaded', description: 'Downloaded label'}), - downloadFailed: intl.formatMessage({id: 'app.presentation.options.downloadFailed', description: 'Downloaded failed label'}), - snapshotLabel: intl.formatMessage({id: 'app.presentation.options.snapshot', description: 'Snapshot of current slide label'}), - whiteboardLabel: intl.formatMessage({id: 'app.shortcut-help.whiteboard', description: 'used for aria whiteboard options button label'}), - }); - }, [intl?.locale]); // eslint-disable-next-line arrow-body-style React.useEffect(() => { @@ -643,15 +608,9 @@ export default function Whiteboard(props) { } if (menu) { - const MENU_OFFSET = '48px'; menu.style.position = 'relative'; menu.style.height = presentationMenuHeight; menu.setAttribute('id', 'TD-Styles-Parent'); - if (isRTL) { - menu.style.left = MENU_OFFSET; - } else { - menu.style.right = MENU_OFFSET; - } [...menu.children] .sort((a, b) => (a?.id > b?.id ? -1 : 1)) @@ -706,43 +665,6 @@ export default function Whiteboard(props) { const onPatch = (e, t, reason) => { if (!e?.pageState || !reason) return; - // Append Presentation Options to Tldraw - const tdStylesParent = document.getElementById('TD-Styles-Parent'); - if (tdStylesParent) { - tdStylesParent.style.right = '0px'; - tdStylesParent.style.width = '17.75rem'; - let presentationMenuNode = document.getElementById('PresentationMenuId'); - if (!presentationMenuNode) { - presentationMenuNode = document.createElement('div'); - presentationMenuNode.setAttribute('id', 'PresentationMenuId'); - tdStylesParent.appendChild(presentationMenuNode); - } - - ReactDOM.render( - , presentationMenuNode - ); - } if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) { e.patchState( @@ -1113,7 +1035,7 @@ export default function Whiteboard(props) { const menuOffset = menuOffsetValues[isRTL][isIphone]; return ( -
+
{ const isModerator = currentUser.role === ROLE_MODERATOR; const { maxStickyNoteLength, maxNumberOfAnnotations } = WHITEBOARD_CONFIG; const fontFamily = WHITEBOARD_CONFIG.styles.text.family; - const fullscreen = layoutSelect((i) => i.fullscreen); const handleToggleFullScreen = (ref) => FullscreenService.toggleFullScreen(ref); const layoutContextDispatch = layoutDispatch(); @@ -84,7 +83,6 @@ const WhiteboardContainer = (props) => { maxStickyNoteLength, maxNumberOfAnnotations, fontFamily, - fullscreen, hasShapeAccess, handleToggleFullScreen, sidebarNavigationWidth, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/component.jsx deleted file mode 100644 index afbe440b1090..000000000000 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/component.jsx +++ /dev/null @@ -1,317 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { toPng } from 'html-to-image'; -import { toast } from 'react-toastify'; -import logger from '/imports/startup/client/logger'; -import Styled from '/imports/ui/components/presentation/presentation-menu/styles'; -import BBBMenu from './menu/component'; -import TooltipContainer from '/imports/ui/components/common/tooltip/container'; -import { ACTIONS } from '/imports/ui/components/layout/enums'; -import browserInfo from '/imports/utils/browserInfo'; -import AppService from '/imports/ui/components/app/service'; - -const propTypes = { - handleToggleFullscreen: PropTypes.func.isRequired, - isFullscreen: PropTypes.bool, - elementName: PropTypes.string, - fullscreenRef: PropTypes.instanceOf(Element), - meetingName: PropTypes.string, - isIphone: PropTypes.bool, - elementId: PropTypes.string, - elementGroup: PropTypes.string, - currentElement: PropTypes.string, - currentGroup: PropTypes.string, - layoutContextDispatch: PropTypes.func.isRequired, - isRTL: PropTypes.bool, - tldrawAPI: PropTypes.shape({ - copySvg: PropTypes.func.isRequired, - getShapes: PropTypes.func.isRequired, - currentPageId: PropTypes.string.isRequired, - }), -}; - -const defaultProps = { - isIphone: false, - isFullscreen: false, - isRTL: false, - elementName: '', - meetingName: '', - fullscreenRef: null, - elementId: '', - elementGroup: '', - currentElement: '', - currentGroup: '', - tldrawAPI: null, -}; - -const PresentationOps = (props) => { - const { - isFullscreen, - elementId, - elementName, - elementGroup, - currentElement, - currentGroup, - fullscreenRef, - tldrawAPI, - handleToggleFullscreen, - layoutContextDispatch, - meetingName, - isIphone, - isRTL, - isToolbarVisible, - setIsToolbarVisible, - exitFullscreenLabel, - fullscreenLabel, - hideToolsDesc, - showToolsDesc, - downloading, - downloaded, - downloadFailed, - snapshotLabel, - hasWBAccess, - amIPresenter, - optionsLabel, - whiteboardLabel, - } = props; - - const [state, setState] = useState({ - hasError: false, - loading: false, - }); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const toastId = useRef(null); - const dropdownRef = useRef(null); - - const formattedLabel = (fullscreen) => (fullscreen - ? exitFullscreenLabel - : fullscreenLabel - ); - - const formattedVisibilityLabel = (visible) => (visible - ? hideToolsDesc - : showToolsDesc - ); - - function renderToastContent() { - const { loading, hasError } = state; - - let icon = loading ? 'blank' : 'check'; - if (hasError) icon = 'circle_close'; - - return ( - - - - {loading && !hasError && downloading} - {!loading && !hasError && downloaded} - {!loading && hasError && downloadFailed} - - - - - - - ); - } - - function getAvailableOptions() { - const menuItems = []; - - if (!isIphone) { - menuItems.push( - { - key: 'list-item-fullscreen', - dataTest: 'presentationFullscreen', - label: formattedLabel(isFullscreen), - icon: isFullscreen ? 'exit_fullscreen' : 'fullscreen', - onClick: () => { - handleToggleFullscreen(fullscreenRef); - const newElement = (elementId === currentElement) ? '' : elementId; - const newGroup = (elementGroup === currentGroup) ? '' : elementGroup; - - layoutContextDispatch({ - type: ACTIONS.SET_FULLSCREEN_ELEMENT, - value: { - element: newElement, - group: newGroup, - }, - }); - }, - }, - ); - } - - const { isSafari } = browserInfo; - - if (!isSafari) { - menuItems.push( - { - key: 'list-item-screenshot', - label: snapshotLabel, - dataTest: 'presentationSnapshot', - icon: 'video', - onClick: async () => { - setState({ - loading: true, - hasError: false, - }); - - toastId.current = toast.info(renderToastContent(), { - hideProgressBar: true, - autoClose: false, - newestOnTop: true, - closeOnClick: true, - onClose: () => { - toastId.current = null; - }, - }); - - // This is a workaround to a conflict of the - // dark mode's styles and the html-to-image lib. - // Issue: - // https://github.com/bubkoo/html-to-image/issues/370 - const darkThemeState = AppService.isDarkThemeEnabled(); - AppService.setDarkTheme(false); - - try { - const { copySvg, getShapes, currentPageId } = tldrawAPI; - const svgString = await copySvg(getShapes(currentPageId).map((shape) => shape.id)); - const container = document.createElement('div'); - container.innerHTML = svgString; - const svgElem = container.firstChild; - const width = svgElem?.width?.baseVal?.value ?? window.screen.width; - const height = svgElem?.height?.baseVal?.value ?? window.screen.height; - - const data = await toPng(svgElem, { width, height, backgroundColor: '#FFF' }); - - const anchor = document.createElement('a'); - anchor.href = data; - anchor.setAttribute( - 'download', - `${elementName}_${meetingName}_${new Date().toISOString()}.png`, - ); - anchor.click(); - - setState({ - loading: false, - hasError: false, - }); - } catch (e) { - setState({ - loading: false, - hasError: true, - }); - - logger.warn({ - logCode: 'presentation_snapshot_error', - extraInfo: e, - }); - } finally { - // Workaround - AppService.setDarkTheme(darkThemeState); - } - }, - }, - ); - } - - const tools = document.querySelector('#TD-Tools'); - if (tools && (hasWBAccess || amIPresenter)){ - menuItems.push( - { - key: 'list-item-toolvisibility', - dataTest: 'toolVisibility', - label: formattedVisibilityLabel(isToolbarVisible), - icon: isToolbarVisible ? 'close' : 'pen_tool', - onClick: () => { - setIsToolbarVisible(!isToolbarVisible); - }, - }, - ); - } - - return menuItems; - } - - useEffect(() => { - if (toastId.current) { - toast.update(toastId.current, { - render: renderToastContent(), - hideProgressBar: state.loading, - autoClose: state.loading ? false : 3000, - newestOnTop: true, - closeOnClick: true, - onClose: () => { - toastId.current = null; - }, - }); - } - - if (dropdownRef.current) { - document.activeElement.blur(); - dropdownRef.current.focus(); - } - }); - - const options = getAvailableOptions(); - - if (options.length === 0) { - const undoCtrls = document.getElementById('TD-Styles')?.nextSibling; - if (undoCtrls?.style) { - undoCtrls.style = 'padding:0px'; - } - const styleTool = document.getElementById('TD-Styles')?.parentNode; - if (styleTool?.style) { - styleTool.style = 'right:0px'; - } - return null; - } - - return ( - - - { - setIsDropdownOpen((isOpen) => !isOpen); - }} - > - - - - )} - opts={{ - id: 'presentation-dropdown-menu', - keepMounted: true, - transitionDuration: 0, - elevation: 3, - getContentAnchorEl: null, - fullwidth: 'true', - anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, - transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, - container: fullscreenRef, - }} - actions={options} - /> - - ); -}; - -PresentationOps.propTypes = propTypes; -PresentationOps.defaultProps = defaultProps; - -export default PresentationOps; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/container.jsx deleted file mode 100644 index a54adfe070a8..000000000000 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/container.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withTracker } from 'meteor/react-meteor-data'; -import PresentationOps from './component'; -import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; -import Auth from '/imports/ui/services/auth'; -import Meetings from '/imports/api/meetings'; -import WhiteboardService from '/imports/ui/components/whiteboard/service'; -import UserService from '/imports/ui/components/user-list/service'; - -const PresentationOpsContainer = (props) => { - const { elementId, isRTL, layoutContextDispatch, fullscreen } = props; - const { element: currentElement, group: currentGroup } = fullscreen; - const isFullscreen = currentElement === elementId; - - return ( - - ); -}; - -export default withTracker((props) => { - const handleToggleFullscreen = (ref) => FullscreenService.toggleFullScreen(ref); - const isIphone = !!(navigator.userAgent.match(/iPhone/i)); - const meetingId = Auth.meetingID; - const meetingObject = Meetings.findOne({ meetingId }, { fields: { 'meetingProp.name': 1 } }); - const hasWBAccess = WhiteboardService.hasMultiUserAccess( - WhiteboardService.getCurrentWhiteboardId(), - Auth.userID - ); - const amIPresenter = UserService.isUserPresenter(Auth.userID); - return { - ...props, - handleToggleFullscreen, - isIphone, - isDropdownOpen: Session.get('dropdownOpen'), - meetingName: meetingObject.meetingProp.name, - hasWBAccess, - amIPresenter, - }; -})(PresentationOpsContainer); - -PresentationOpsContainer.propTypes = { - elementId: PropTypes.string.isRequired, -}; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/menu/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/menu/component.jsx deleted file mode 100644 index 5ec052c61217..000000000000 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/presentation-ops-injector/menu/component.jsx +++ /dev/null @@ -1,253 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -import Menu from "@material-ui/core/Menu"; -import { Divider } from "@material-ui/core"; -import Icon from "/imports/ui/components/common/icon/component"; -import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums'; -import KEY_CODES from '/imports/utils/keyCodes'; - -import { ENTER } from "/imports/utils/keyCodes"; - -import Styled from '/imports/ui/components/common/menu/styles'; - -class BBBMenu extends React.Component { - constructor(props) { - super(props); - this.state = { - anchorEl: null, - }; - - this.optsToMerge = {}; - this.autoFocus = false; - - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleClick = this.handleClick.bind(this); - this.handleClose = this.handleClose.bind(this); - } - - componentDidUpdate() { - const { anchorEl } = this.state; - const { open } = this.props; - if (open === false && anchorEl) { - this.setState({ anchorEl: null }); - } else if (open === true && !anchorEl) { - this.setState({ anchorEl: this.anchorElRef }); - } - } - - handleKeyDown(event) { - const { anchorEl } = this.state; - const isMenuOpen = Boolean(anchorEl); - - - if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.which)) { - this.handleClose(); - return; - } - - if (isMenuOpen && [KEY_CODES.ARROW_UP, KEY_CODES.ARROW_DOWN].includes(event.which)) { - event.preventDefault(); - event.stopPropagation(); - const menuItems = Array.from(document.querySelectorAll('[data-key^="menuItem-"]')); - if (menuItems.length === 0) return; - - const focusedIndex = menuItems.findIndex(item => item === document.activeElement); - const nextIndex = event.which === KEY_CODES.ARROW_UP ? focusedIndex - 1 : focusedIndex + 1; - let indexToFocus = 0; - if (nextIndex < 0) { - indexToFocus = menuItems.length - 1; - } else if (nextIndex >= menuItems.length) { - indexToFocus = 0; - } else { - indexToFocus = nextIndex; - } - - menuItems[indexToFocus].focus(); - } - }; - - handleClick(event) { - this.setState({ anchorEl: event.currentTarget }); - }; - - handleClose(event) { - const { onCloseCallback } = this.props; - this.setState({ anchorEl: null }, onCloseCallback()); - - if (event) { - event.persist(); - - if (event.type === 'click') { - setTimeout(() => { - document.activeElement.blur(); - }, 0); - } - } - }; - - makeMenuItems() { - const { actions, selectedEmoji, activeLabel } = this.props; - - return actions?.map(a => { - const { dataTest, label, onClick, key, disabled, description, selected } = a; - const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase()); - - let customStyles = { - paddingLeft: '16px', - paddingRight: '16px', - paddingTop: '12px', - paddingBottom: '12px', - marginLeft: '0px', - marginRight: '0px', - }; - - if (a.customStyles) { - customStyles = { ...customStyles, ...a.customStyles }; - } - - return [ - a.dividerTop && , - { - onClick(); - const close = !key?.includes('setstatus') && !key?.includes('back'); - // prevent menu close for sub menu actions - if (close) this.handleClose(event); - event.stopPropagation(); - }}> - - {a.icon ? : null} - {label} - {description &&
{`${description}${selected ? ` - ${activeLabel}` : ''}`}
} - {a.iconRight ? : null} -
-
, - a.divider && - ]; - }); - } - - render() { - const { anchorEl } = this.state; - const { trigger, customStyles, dataTest, opts, accessKey, closeLabel } = this.props; - const actionsItems = this.makeMenuItems(); - - let menuStyles = { zIndex: 9999 }; - - if (customStyles) { - menuStyles = { ...menuStyles, ...customStyles }; - } - - return ( - <> -
{ - e.persist(); - // 1 = mouse, 5 = touch (firefox only) - const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); - const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType)); - this.optsToMerge.autoFocus = firefoxInputSource && chromeInputSource; - this.handleClick(e); - }} - onKeyPress={(e) => { - e.persist(); - if (e.which !== ENTER) return null; - return this.handleClick(e); - }} - accessKey={accessKey} - ref={(ref) => { this.anchorElRef = ref; return null; }} - role="button" - tabIndex={-1} - > - {trigger} -
- - - {actionsItems} - {anchorEl && window.innerWidth < SMALL_VIEWPORT_BREAKPOINT && - - } - - - ); - } -} - -export default BBBMenu; - -BBBMenu.defaultProps = { - opts: { - id: "default-dropdown-menu", - autoFocus: false, - keepMounted: true, - transitionDuration: 0, - elevation: 3, - getContentAnchorEl: null, - fullwidth: "true", - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - transformorigin: { vertical: 'top', horizontal: 'right' }, - }, - dataTest: "", - onCloseCallback: () => { }, -}; - -BBBMenu.propTypes = { - trigger: PropTypes.element.isRequired, - - actions: PropTypes.arrayOf(PropTypes.shape({ - key: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - onClick: PropTypes.func, - icon: PropTypes.string, - iconRight: PropTypes.string, - disabled: PropTypes.bool, - divider: PropTypes.bool, - dividerTop: PropTypes.bool, - accessKey: PropTypes.string, - dataTest: PropTypes.string, - })).isRequired, - - opts: PropTypes.shape({ - id: PropTypes.string, - autoFocus: PropTypes.bool, - keepMounted: PropTypes.bool, - transitionDuration: PropTypes.number, - elevation: PropTypes.number, - getContentAnchorEl: PropTypes.element, - fullwidth: PropTypes.string, - anchorOrigin: PropTypes.shape({ - vertical: PropTypes.string, - horizontal: PropTypes.string - }), - transformorigin: PropTypes.shape({ - vertical: PropTypes.string, - horizontal: PropTypes.string - }), - }), - - onCloseCallback: PropTypes.func, - dataTest: PropTypes.string, -}; \ No newline at end of file