From c344a17adbd2e317235192cc54931ea6e9ceea68 Mon Sep 17 00:00:00 2001 From: Chad Pilkey <capilkey@gmail.com> Date: Wed, 17 Jul 2019 17:30:28 -0700 Subject: [PATCH] pan/zoom and cursor refactor for better performance --- .../imports/ui/components/cursor/service.js | 13 +- .../ui/components/panel-manager/component.jsx | 31 +- .../ui/components/presentation/component.jsx | 483 ++++++------ .../ui/components/presentation/container.jsx | 2 + .../presentation/cursor/component.jsx | 50 +- .../presentation-overlay/component.jsx | 717 ++++++++++-------- .../presentation-overlay/container.jsx | 23 +- .../presentation-overlay/service.js | 10 - .../presentation-toolbar/component.jsx | 59 +- .../presentation-toolbar/container.jsx | 75 +- .../presentation-toolbar/service.js | 26 +- .../ui/components/presentation/service.js | 7 +- .../whiteboard-overlay/component.jsx | 125 ++- .../whiteboard-overlay/container.jsx | 4 +- .../cursor-listener/component.jsx | 216 ++++++ .../pan-zoom-draw-listener/component.jsx | 183 ----- .../pencil-draw-listener/component.jsx | 81 +- .../whiteboard/whiteboard-overlay/service.js | 6 + .../shape-draw-listener/component.jsx | 90 ++- .../text-draw-listener/component.jsx | 198 +++-- .../imports/utils/slideCalcUtils.js | 16 +- .../private/config/settings.yml | 1 + 22 files changed, 1394 insertions(+), 1022 deletions(-) mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/container.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx delete mode 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/service.js mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/presentation/service.js create mode 100755 bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx mode change 100644 => 100755 bigbluebutton-html5/imports/utils/slideCalcUtils.js diff --git a/bigbluebutton-html5/imports/ui/components/cursor/service.js b/bigbluebutton-html5/imports/ui/components/cursor/service.js index db01b2a104..50db0ae5e5 100755 --- a/bigbluebutton-html5/imports/ui/components/cursor/service.js +++ b/bigbluebutton-html5/imports/ui/components/cursor/service.js @@ -1,13 +1,11 @@ -import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; import { CursorStreamer } from '/imports/api/cursor'; import { throttle } from 'lodash'; const Cursor = new Mongo.Collection(null); -function updateCursor(meetingId, userId, payload) { +function updateCursor(userId, payload) { const selector = { - meetingId, userId, whiteboardId: payload.whiteboardId, }; @@ -15,7 +13,6 @@ function updateCursor(meetingId, userId, payload) { const modifier = { $set: { userId, - meetingId, ...payload, }, }; @@ -23,10 +20,10 @@ function updateCursor(meetingId, userId, payload) { return Cursor.upsert(selector, modifier); } -CursorStreamer.on('message', ({ meetingId, cursors }) => { +CursorStreamer.on('message', ({ meetingID, cursors }) => { Object.keys(cursors).forEach((userId) => { - if (Auth.meetingID === meetingId && Auth.userID === userId) return; - updateCursor(meetingId, userId, cursors[userId]); + if (Auth.userID === userId) return; + updateCursor(userId, cursors[userId]); }); }); @@ -38,7 +35,7 @@ export function publishCursorUpdate(payload) { payload, }); - return updateCursor(Auth.meetingID, Auth.userID, payload); + return updateCursor(Auth.userID, payload); } export default Cursor; diff --git a/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx b/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx old mode 100644 new mode 100755 index 3202a04043..18a674d122 --- a/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx @@ -401,16 +401,19 @@ class PanelManager extends Component { render() { const { enableResize, openPanel } = this.props; if (openPanel === '') return null; - - const panels = [this.renderUserList()]; - const resizablePanels = [ - this.renderUserListResizable(), - <div className={styles.userlistPad} key={this.padKey} />, - ]; + const panels = []; + if (enableResize) { + panels.push( + this.renderUserListResizable(), + <div className={styles.userlistPad} key={this.padKey} />, + ); + } else { + panels.push(this.renderUserList()); + } if (openPanel === 'chat') { if (enableResize) { - resizablePanels.push(this.renderChatResizable()); + panels.push(this.renderChatResizable()); } else { panels.push(this.renderChat()); } @@ -418,7 +421,7 @@ class PanelManager extends Component { if (openPanel === 'note') { if (enableResize) { - resizablePanels.push(this.renderNoteResizable()); + panels.push(this.renderNoteResizable()); } else { panels.push(this.renderNote()); } @@ -426,7 +429,7 @@ class PanelManager extends Component { if (openPanel === 'captions') { if (enableResize) { - resizablePanels.push(this.renderCaptionsResizable()); + panels.push(this.renderCaptionsResizable()); } else { panels.push(this.renderCaptions()); } @@ -434,7 +437,7 @@ class PanelManager extends Component { if (openPanel === 'poll') { if (enableResize) { - resizablePanels.push(this.renderPollResizable()); + panels.push(this.renderPollResizable()); } else { panels.push(this.renderPoll()); } @@ -442,7 +445,7 @@ class PanelManager extends Component { if (openPanel === 'breakoutroom') { if (enableResize) { - resizablePanels.push(this.renderBreakoutRoom()); + panels.push(this.renderBreakoutRoom()); } else { panels.push(this.renderBreakoutRoom()); } @@ -450,15 +453,13 @@ class PanelManager extends Component { if (openPanel === 'waitingUsersPanel') { if (enableResize) { - resizablePanels.push(this.renderWaitingUsersPanelResizable()); + panels.push(this.renderWaitingUsersPanelResizable()); } else { panels.push(this.renderWaitingUsersPanel()); } } - return enableResize - ? resizablePanels - : panels; + return panels; } } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx old mode 100644 new mode 100755 index 6175ece85d..8baf208870 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { TransitionGroup, CSSTransition } from 'react-transition-group'; import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container'; import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container'; import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils'; @@ -32,45 +31,38 @@ class PresentationArea extends Component { super(); this.state = { - presentationWidth: 0, - presentationHeight: 0, + presentationAreaWidth: 0, + presentationAreaHeight: 0, showSlide: false, zoom: 100, - touchZoom: false, - delta: { - x: 0, - y: 0, - }, fitToWidth: false, }; this.getSvgRef = this.getSvgRef.bind(this); this.zoomChanger = this.zoomChanger.bind(this); - this.touchUpdate = this.touchUpdate.bind(this); - this.pointUpdate = this.pointUpdate.bind(this); + this.updateLocalPosition = this.updateLocalPosition.bind(this); + this.panAndZoomChanger = this.panAndZoomChanger.bind(this); this.fitToWidthHandler = this.fitToWidthHandler.bind(this); } - componentDidUpdate(prevProps, prevState) { - const { currentPresentation, notify, intl } = this.props; + static getDerivedStateFromProps(props, state) { + const { prevProps } = state; + const stateChange = { prevProps: props }; - if (prevProps.currentPresentation.name !== currentPresentation.name) { - notify( - `${intl.formatMessage(intlMessages.changeNotification)} ${currentPresentation.name}`, - 'info', - 'presentation', - ); + if (props.userIsPresenter && (!prevProps || !prevProps.userIsPresenter) && props.currentSlide) { + const potentialZoom = 100 * 100 / props.currentSlide.widthRatio; + stateChange.zoom = potentialZoom; } - if (prevState.fitToWidth) { - // When presenter is changed or slide changed we reset fitToWidth - if ((prevProps.userIsPresenter && !this.props.userIsPresenter) - || (prevProps.currentSlide.id !== this.props.currentSlide.id)) { - this.setState({ - fitToWidth: false, - }); - } + if (!prevProps) return stateChange; + + // When presenter is changed or slide changed we reset localPosition + if (prevProps.currentSlide.id !== props.currentSlide.id + || prevProps.userIsPresenter !== props.userIsPresenter) { + stateChange.localPosition = undefined; } + + return stateChange; } componentDidMount() { @@ -82,6 +74,18 @@ class PresentationArea extends Component { this.getInitialPresentationSizes(); } + componentDidUpdate(prevProps) { + const { currentPresentation, notify, intl } = this.props; + + if (prevProps.currentPresentation.name !== currentPresentation.name) { + notify( + `${intl.formatMessage(intlMessages.changeNotification)} ${currentPresentation.name}`, + 'info', + 'presentation', + ); + } + } + componentWillUnmount() { window.removeEventListener('resize', () => { setTimeout(this.handleResize.bind(this), 0); @@ -121,23 +125,23 @@ class PresentationArea extends Component { ({ clientWidth, clientHeight } = refWhiteboardArea); } - presentationSizes.presentationHeight = clientHeight - this.getToolbarHeight(); - presentationSizes.presentationWidth = clientWidth; + presentationSizes.presentationAreaHeight = clientHeight - this.getToolbarHeight(); + presentationSizes.presentationAreaWidth = clientWidth; } return presentationSizes; } getInitialPresentationSizes() { - // determining the presentationWidth and presentationHeight (available space for the svg) - // on the initial load + // determining the presentationAreaWidth and presentationAreaHeight (available + // space for the svg) on the initial load const presentationSizes = this.getPresentationSizesAvailable(); if (Object.keys(presentationSizes).length > 0) { // setting the state of the available space for the svg // and set the showSlide to true to start rendering the slide this.setState({ - presentationHeight: presentationSizes.presentationHeight, - presentationWidth: presentationSizes.presentationWidth, + presentationAreaHeight: presentationSizes.presentationAreaHeight, + presentationAreaWidth: presentationSizes.presentationAreaWidth, showSlide: true, }); } @@ -148,112 +152,88 @@ class PresentationArea extends Component { if (Object.keys(presentationSizes).length > 0) { // updating the size of the space available for the slide this.setState({ - presentationHeight: presentationSizes.presentationHeight, - presentationWidth: presentationSizes.presentationWidth, + presentationAreaHeight: presentationSizes.presentationAreaHeight, + presentationAreaWidth: presentationSizes.presentationAreaWidth, }); } } - calculateSize() { - const { presentationHeight, presentationWidth, fitToWidth } = this.state; - const { currentSlide } = this.props; - const slideSizes = currentSlide - && currentSlide.calculatedData - ? currentSlide.calculatedData : {}; - const originalWidth = slideSizes.width; - const originalHeight = slideSizes.height; - - let adjustedWidth; - let adjustedHeight; - - if (!originalHeight || !originalWidth) { - return { - width: 0, - height: 0, - }; + calculateSize(viewBoxDimensions) { + const { + presentationAreaHeight, + presentationAreaWidth, + fitToWidth, + } = this.state; + + const { + userIsPresenter, + currentSlide, + } = this.props; + + if (!currentSlide || !currentSlide.calculatedData) { + return { width: 0, height: 0 }; } - if (!fitToWidth) { - // Slide has a portrait orientation - if (originalWidth <= originalHeight) { - adjustedWidth = (presentationHeight * originalWidth) / originalHeight; - if (presentationWidth < adjustedWidth) { - adjustedHeight = (presentationHeight * presentationWidth) / adjustedWidth; - adjustedWidth = presentationWidth; - } else { - adjustedHeight = presentationHeight; - } - // Slide has a landscape orientation + const originalWidth = currentSlide.calculatedData.width; + const originalHeight = currentSlide.calculatedData.height; + const viewBoxWidth = viewBoxDimensions.width; + const viewBoxHeight = viewBoxDimensions.height; + + let svgWidth; + let svgHeight; + + if (!userIsPresenter) { + svgWidth = (presentationAreaHeight * viewBoxWidth) / viewBoxHeight; + if (presentationAreaWidth < svgWidth) { + svgHeight = (presentationAreaHeight * presentationAreaWidth) / svgWidth; + svgWidth = presentationAreaWidth; + } else { + svgHeight = presentationAreaHeight; + } + } else if (!fitToWidth) { + svgWidth = (presentationAreaHeight * originalWidth) / originalHeight; + if (presentationAreaWidth < svgWidth) { + svgHeight = (presentationAreaHeight * presentationAreaWidth) / svgWidth; + svgWidth = presentationAreaWidth; } else { - adjustedHeight = (presentationWidth * originalHeight) / originalWidth; - if (presentationHeight < adjustedHeight) { - adjustedWidth = (presentationWidth * presentationHeight) / adjustedHeight; - adjustedHeight = presentationHeight; - } else { - adjustedWidth = presentationWidth; - } + svgHeight = presentationAreaHeight; } } else { - adjustedWidth = presentationWidth; - adjustedHeight = (adjustedWidth * originalHeight) / originalWidth; - if (adjustedHeight > presentationHeight) adjustedHeight = presentationHeight; + svgWidth = presentationAreaWidth; + svgHeight = (svgWidth * originalHeight) / originalWidth; + if (svgHeight > presentationAreaHeight) svgHeight = presentationAreaHeight; } + return { - width: adjustedWidth, - height: adjustedHeight, + width: svgWidth, + height: svgHeight, }; } - // TODO: This could be replaced if we synchronize the fit-to-width state between users - checkFitToWidth() { - const { userIsPresenter, currentSlide } = this.props; - const { fitToWidth } = this.state; - if (userIsPresenter) { - return fitToWidth; - } + zoomChanger(incomingZoom) { const { - width, height, viewBoxWidth, viewBoxHeight, - } = currentSlide.calculatedData; - const slideSizeRatio = width / height; - const viewBoxSizeRatio = viewBoxWidth / viewBoxHeight; - if (slideSizeRatio !== viewBoxSizeRatio) { - return true; - } - return false; - } + zoom, + } = this.state; - zoomChanger(incomingZoom) { - const { zoom } = this.state; let newZoom = incomingZoom; - const isDifferent = newZoom !== zoom; if (newZoom <= HUNDRED_PERCENT) { newZoom = HUNDRED_PERCENT; } else if (incomingZoom >= MAX_PERCENT) { newZoom = MAX_PERCENT; } - if (isDifferent) this.setState({ zoom: newZoom }); - } - pointUpdate(pointX, pointY) { - this.setState({ - delta: { - x: pointX, - y: pointY, - }, - }); - } - - touchUpdate(bool) { - this.setState({ - touchZoom: bool, - }); + if (newZoom !== zoom) this.setState({ zoom: newZoom }); } fitToWidthHandler() { - const { fitToWidth } = this.state; + const { + fitToWidth, + } = this.state; + this.setState({ fitToWidth: !fitToWidth, + zoom: HUNDRED_PERCENT, }); } @@ -263,6 +243,25 @@ class PresentationArea extends Component { return currentSlide && currentSlide.calculatedData; } + updateLocalPosition(x, y, width, height, zoom) { + this.setState({ + localPosition: { + x, y, width, height, + }, + zoom, + }); + } + + panAndZoomChanger(w, h, x, y) { + const { + currentSlide, + podId, + zoomSlide, + } = this.props; + + zoomSlide(currentSlide.num, podId, w, h, x, y); + } + renderPresentationClose() { const { isFullscreen } = this.props; if (!shouldEnableSwapLayout() || isFullscreen) { @@ -271,7 +270,7 @@ class PresentationArea extends Component { return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />; } - renderOverlays(slideObj, adjustedSizes) { + renderOverlays(slideObj, svgDimensions, viewBoxPosition, viewBoxDimensions, physicalDimensions) { const { userIsPresenter, multiUser, @@ -280,9 +279,7 @@ class PresentationArea extends Component { } = this.props; const { - delta, zoom, - touchZoom, fitToWidth, } = this.state; @@ -292,160 +289,160 @@ class PresentationArea extends Component { // retrieving the pre-calculated data from the slide object const { - x, - y, width, height, - viewBoxWidth, - viewBoxHeight, } = slideObj.calculatedData; return ( <PresentationOverlayContainer podId={podId} + userIsPresenter={userIsPresenter} currentSlideNum={currentSlide.num} slide={slideObj} - whiteboardId={slideObj.id} slideWidth={width} slideHeight={height} - delta={delta} - viewBoxWidth={viewBoxWidth} - viewBoxHeight={viewBoxHeight} + viewBoxX={viewBoxPosition.x} + viewBoxY={viewBoxPosition.y} + viewBoxWidth={viewBoxDimensions.width} + viewBoxHeight={viewBoxDimensions.height} + physicalSlideWidth={physicalDimensions.width} + physicalSlideHeight={physicalDimensions.height} + svgWidth={svgDimensions.width} + svgHeight={svgDimensions.height} zoom={zoom} zoomChanger={this.zoomChanger} - adjustedSizes={adjustedSizes} + updateLocalPosition={this.updateLocalPosition} + panAndZoomChanger={this.panAndZoomChanger} getSvgRef={this.getSvgRef} - presentationSize={this.getPresentationSizesAvailable()} - touchZoom={touchZoom} fitToWidth={fitToWidth} > <WhiteboardOverlayContainer getSvgRef={this.getSvgRef} + userIsPresenter={userIsPresenter} whiteboardId={slideObj.id} + slide={slideObj} slideWidth={width} slideHeight={height} - viewBoxX={x} - viewBoxY={y} - pointChanger={this.pointUpdate} - viewBoxWidth={viewBoxWidth} - viewBoxHeight={viewBoxHeight} - physicalSlideWidth={(adjustedSizes.width / slideObj.widthRatio) * 100} - physicalSlideHeight={(adjustedSizes.height / slideObj.heightRatio) * 100} + viewBoxX={viewBoxPosition.x} + viewBoxY={viewBoxPosition.y} + viewBoxWidth={viewBoxDimensions.width} + viewBoxHeight={viewBoxDimensions.height} + physicalSlideWidth={physicalDimensions.width} + physicalSlideHeight={physicalDimensions.height} zoom={zoom} zoomChanger={this.zoomChanger} - touchUpdate={this.touchUpdate} /> </PresentationOverlayContainer> ); } // renders the whole presentation area - renderPresentationArea() { - const { presentationWidth } = this.state; - const { podId, currentSlide, isFullscreen } = this.props; - if (!this.isPresentationAccessible()) return null; + renderPresentationArea(svgDimensions, viewBoxDimensions) { + const { + podId, + currentSlide, + isFullscreen, + userIsPresenter, + } = this.props; + const { + localPosition, + } = this.state; - // to control the size of the svg wrapper manually - // and adjust cursor's thickness, so that svg didn't scale it automatically - const adjustedSizes = this.calculateSize(); - // a reference to the slide object - const slideObj = currentSlide; + if (!this.isPresentationAccessible()) { + return null; + } // retrieving the pre-calculated data from the slide object const { - x, - y, width, height, - viewBoxWidth, - viewBoxHeight, imageUri, - } = slideObj.calculatedData; + } = currentSlide.calculatedData; - const svgAreaDimensions = this.checkFitToWidth() - ? { - position: 'absolute', - width: 'inherit', - height: adjustedSizes.height, - } - : { - position: 'absolute', - width: adjustedSizes.width, - height: adjustedSizes.height, - textAlign: 'center', + let viewBoxPosition; + + if (userIsPresenter && localPosition) { + viewBoxPosition = { + x: localPosition.x, + y: localPosition.y, + }; + } else { + viewBoxPosition = { + x: currentSlide.calculatedData.x, + y: currentSlide.calculatedData.y, }; + } + + const widthRatio = viewBoxDimensions.width / width; + const heightRatio = viewBoxDimensions.height / height; + + const physicalDimensions = { + width: (svgDimensions.width / widthRatio), + height: (svgDimensions.height / heightRatio), + }; + + const svgViewBox = `${viewBoxPosition.x} ${viewBoxPosition.y} ` + + `${viewBoxDimensions.width} ${viewBoxDimensions.height}`; return ( <div - style={svgAreaDimensions} + style={{ + position: 'absolute', + width: svgDimensions.width, + height: svgDimensions.height, + textAlign: 'center', + }} > {this.renderPresentationClose()} {this.renderPresentationDownload()} {isFullscreen ? null : this.renderPresentationFullscreen()} - <TransitionGroup> - <CSSTransition - key={slideObj.id} - classNames={{ - enter: styles.enter, - enterActive: styles.enterActive, - appear: styles.appear, - appearActive: styles.appearActive, - }} - appear - enter - exit={false} - timeout={{ enter: 400 }} - > - <svg - data-test="whiteboard" + <svg + key={currentSlide.id} + data-test="whiteboard" + width={svgDimensions.width} + height={svgDimensions.height} + ref={(ref) => { if (ref != null) { this.svggroup = ref; } }} + viewBox={svgViewBox} + version="1.1" + xmlns="http://www.w3.org/2000/svg" + className={styles.svgStyles} + > + <defs> + <clipPath id="viewBox"> + <rect x={viewBoxPosition.x} y={viewBoxPosition.y} width="100%" height="100%" fill="none" /> + </clipPath> + </defs> + <g clipPath="url(#viewBox)"> + <Slide + imageUri={imageUri} + svgWidth={width} + svgHeight={height} + /> + <AnnotationGroupContainer {...{ width, height, }} - ref={(ref) => { if (ref != null) { this.svggroup = ref; } }} - viewBox={`${x} ${y} ${viewBoxWidth} ${viewBoxHeight}`} - version="1.1" - xmlns="http://www.w3.org/2000/svg" - className={styles.svgStyles} - style={this.checkFitToWidth() - ? { - position: 'absolute', - } - : null - } - > - <defs> - <clipPath id="viewBox"> - <rect x={x} y={y} width="100%" height="100%" fill="none" /> - </clipPath> - </defs> - <g clipPath="url(#viewBox)"> - <Slide - imageUri={imageUri} - svgWidth={width} - svgHeight={height} - /> - <AnnotationGroupContainer - {...{ - width, - height, - }} - whiteboardId={slideObj.id} - /> - <CursorWrapperContainer - podId={podId} - whiteboardId={slideObj.id} - widthRatio={slideObj.widthRatio} - physicalWidthRatio={this.checkFitToWidth() ? (presentationWidth / width) : (adjustedSizes.width / width)} - slideWidth={width} - slideHeight={height} - /> - </g> - {this.renderOverlays(slideObj, adjustedSizes)} - </svg> - </CSSTransition> - </TransitionGroup> + whiteboardId={currentSlide.id} + /> + <CursorWrapperContainer + podId={podId} + whiteboardId={currentSlide.id} + widthRatio={widthRatio} + physicalWidthRatio={svgDimensions.width / width} + slideWidth={width} + slideHeight={height} + /> + </g> + {this.renderOverlays( + currentSlide, + svgDimensions, + viewBoxPosition, + viewBoxDimensions, + physicalDimensions, + )} + </svg> </div> ); } @@ -480,15 +477,14 @@ class PresentationArea extends Component { ); } - renderWhiteboardToolbar() { + renderWhiteboardToolbar(svgDimensions) { const { currentSlide } = this.props; if (!this.isPresentationAccessible()) return null; - const adjustedSizes = this.calculateSize(); return ( <WhiteboardToolbarContainer whiteboardId={currentSlide.id} - height={adjustedSizes.height} + height={svgDimensions.height} /> ); } @@ -530,32 +526,47 @@ class PresentationArea extends Component { const { userIsPresenter, multiUser, + currentSlide, } = this.props; + const { showSlide, fitToWidth, + presentationAreaWidth, + localPosition, } = this.state; - const adjustedSizes = this.calculateSize(); - const adjustedHeight = adjustedSizes.height; - const adjustedWidth = adjustedSizes.width; + let viewBoxDimensions; + + if (userIsPresenter && localPosition) { + viewBoxDimensions = { + width: localPosition.width, + height: localPosition.height, + }; + } else { + viewBoxDimensions = { + width: currentSlide.calculatedData.viewBoxWidth, + height: currentSlide.calculatedData.viewBoxHeight, + }; + } + + const svgDimensions = this.calculateSize(viewBoxDimensions); + const svgHeight = svgDimensions.height; + const svgWidth = svgDimensions.width; const toolbarHeight = this.getToolbarHeight(); let toolbarWidth = 0; if (this.refWhiteboardArea) { - const { clientWidth: areaWidth } = this.refWhiteboardArea; - if (adjustedWidth <= 400 - && adjustedWidth !== areaWidth - && areaWidth > 400 - && fitToWidth === false) { - toolbarWidth = '400px'; - } else if (adjustedWidth === areaWidth - || areaWidth <= 400 + if (svgWidth === presentationAreaWidth + || presentationAreaWidth <= 400 || fitToWidth === true) { toolbarWidth = '100%'; + } else if (svgWidth <= 400 + && presentationAreaWidth > 400) { + toolbarWidth = '400px'; } else { - toolbarWidth = adjustedWidth; + toolbarWidth = svgWidth; } } @@ -575,16 +586,16 @@ class PresentationArea extends Component { <div className={styles.svgContainer} style={{ - height: adjustedHeight + toolbarHeight, + height: svgHeight + toolbarHeight, }} > {showSlide - ? this.renderPresentationArea() + ? this.renderPresentationArea(svgDimensions, viewBoxDimensions) : null} - {userIsPresenter || multiUser - ? this.renderWhiteboardToolbar() + {showSlide && (userIsPresenter || multiUser) + ? this.renderWhiteboardToolbar(svgDimensions) : null} - {userIsPresenter || multiUser + {showSlide && userIsPresenter ? ( <div className={styles.presentationToolbar} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx old mode 100644 new mode 100755 index 96cf5601cc..60fd249298 --- a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx @@ -4,6 +4,7 @@ import { getSwapLayout } from '/imports/ui/components/media/service'; import { notify } from '/imports/ui/services/notification'; import PresentationAreaService from './service'; import PresentationArea from './component'; +import PresentationToolbarService from './presentation-toolbar/service'; const PresentationAreaContainer = ({ presentationPodIds, mountPresentationArea, ...props }) => ( mountPresentationArea && <PresentationArea {...props} /> @@ -24,5 +25,6 @@ export default withTracker(({ podId }) => { mountPresentationArea: !!currentSlide, currentPresentation: PresentationAreaService.getCurrentPresentation(podId), notify, + zoomSlide: PresentationToolbarService.zoomSlide, }; })(PresentationAreaContainer); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx old mode 100644 new mode 100755 index 8810560122..640aedc089 --- a/bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx @@ -3,11 +3,11 @@ import PropTypes from 'prop-types'; export default class Cursor extends Component { static scale(attribute, widthRatio, physicalWidthRatio) { - return ((attribute * widthRatio) / 100) / physicalWidthRatio; + return (attribute * widthRatio) / physicalWidthRatio; } static invertScale(attribute, widthRatio, physicalWidthRatio) { - return ((attribute * physicalWidthRatio) * 100) / widthRatio; + return (attribute * physicalWidthRatio) / widthRatio; } static getCursorCoordinates(cursorX, cursorY, slideWidth, slideHeight) { @@ -41,7 +41,7 @@ export default class Cursor extends Component { static getScaledSizes(props) { // TODO: This might need to change for the use case of fit-to-width portrait // slides in non-presenter view. Some elements are still shrinking. - const scaleFactor = props.widthRatio / 100 / props.physicalWidthRatio; + const scaleFactor = props.widthRatio / props.physicalWidthRatio; return { // Adjust the radius of the cursor according to zoom @@ -67,7 +67,12 @@ export default class Cursor extends Component { componentWillMount() { const { - cursorX, cursorY, slideWidth, slideHeight, presenter, isMultiUser, + cursorX, + cursorY, + slideWidth, + slideHeight, + presenter, + isMultiUser, } = this.props; // setting the initial cursor info @@ -130,6 +135,11 @@ export default class Cursor extends Component { // this function retrieves the text node, measures its BBox and sets the size for the outer box calculateCursorLabelBoxDimensions() { + const { + cursorLabelBox, + setLabelBoxDimensions, + } = this.props; + let labelBoxWidth = 0; let labelBoxHeight = 0; if (this.cursorLabelRef) { @@ -139,25 +149,37 @@ export default class Cursor extends Component { labelBoxHeight = Cursor.invertScale(height, widthRatio, physicalWidthRatio); // if the width of the text node is bigger than the maxSize - set the width to maxWidth - if (labelBoxWidth > this.props.cursorLabelBox.maxWidth) { - labelBoxWidth = this.props.cursorLabelBox.maxWidth; + if (labelBoxWidth > cursorLabelBox.maxWidth) { + labelBoxWidth = cursorLabelBox.maxWidth; } } // updating labelBoxWidth and labelBoxHeight in the container, which then passes it down here - this.props.setLabelBoxDimensions(labelBoxWidth, labelBoxHeight); + setLabelBoxDimensions(labelBoxWidth, labelBoxHeight); } render() { + const { + cursorId, + userName, + } = this.props; + const { cursorCoordinate, fill, } = this; - const { cursorLabelBox, cursorLabelText, finalRadius } = this.scaledSizes; + const { + cursorLabelBox, + cursorLabelText, + finalRadius, + } = this.scaledSizes; + + const { + x, + y, + } = cursorCoordinate; - const x = cursorCoordinate.x; - const y = cursorCoordinate.y; const boxX = x + cursorLabelBox.xOffset; const boxY = y + cursorLabelBox.yOffset; @@ -198,11 +220,11 @@ export default class Cursor extends Component { fill={fill} fillOpacity="0.8" fontSize={cursorLabelText.fontSize} - clipPath={`url(#${this.props.cursorId})`} + clipPath={`url(#${cursorId})`} > - {this.props.userName} + {userName} </text> - <clipPath id={this.props.cursorId}> + <clipPath id={cursorId}> <rect x={boxX} y={boxY} @@ -255,7 +277,7 @@ Cursor.propTypes = { * Defines the cursor radius (not scaled) * @defaultValue 5 */ - radius: PropTypes.number.isRequired, + radius: PropTypes.number, cursorLabelBox: PropTypes.shape({ labelBoxStrokeWidth: PropTypes.number.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx index ba29204767..8cf206565c 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx @@ -1,31 +1,46 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { throttle } from 'lodash'; import SlideCalcUtil, { - HUNDRED_PERCENT, MAX_PERCENT, MYSTERY_NUM, STEP, + HUNDRED_PERCENT, MAX_PERCENT, STEP, } from '/imports/utils/slideCalcUtils'; -import WhiteboardToolbarService from '../../whiteboard/whiteboard-toolbar/service'; // After lots of trial and error on why synching doesn't work properly, I found I had to // multiply the coordinates by 2. There's something I don't understand probably on the // canvas coordinate system. (ralam feb 22, 2012) -const CURSOR_INTERVAL = 16; +// maximum value of z-index to prevent other things from overlapping +const MAX_Z_INDEX = (2 ** 31) - 1; +const HAND_TOOL = 'hand'; +const MOUSE_INTERVAL = 32; export default class PresentationOverlay extends Component { + static calculateDistance(touches) { + return Math.sqrt(((touches[0].clientX - touches[1].clientX) ** 2) + + ((touches[0].clientY - touches[1].clientY) ** 2)); + } + + static touchCenterPoint(touches) { + let totalX = 0; let + totalY = 0; + + for (let i = 0; i < touches.length; i += 1) { + totalX += touches[i].clientX; + totalY += touches[i].clientY; + } + + return { x: totalX / touches.length, y: totalY / touches.length }; + } + constructor(props) { super(props); - // last sent coordinates - this.lastSentClientX = 0; - this.lastSentClientY = 0; + this.currentMouseX = 0; + this.currentMouseY = 0; - // last updated coordinates - this.currentClientX = 0; - this.currentClientY = 0; + this.prevZoom = props.zoom; - // id of the setInterval() - this.intervalId = 0; this.state = { - zoom: props.zoom, + pressed: false, }; // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent @@ -36,156 +51,77 @@ export default class PresentationOverlay extends Component { this.touchStarted = false; this.handleTouchStart = this.handleTouchStart.bind(this); - this.handleTouchMove = this.handleTouchMove.bind(this); + this.handleTouchMove = throttle(this.handleTouchMove.bind(this), MOUSE_INTERVAL); this.handleTouchEnd = this.handleTouchEnd.bind(this); this.handleTouchCancel = this.handleTouchCancel.bind(this); - this.mouseMoveHandler = this.mouseMoveHandler.bind(this); - this.checkCursor = this.checkCursor.bind(this); - this.mouseEnterHandler = this.mouseEnterHandler.bind(this); - this.mouseOutHandler = this.mouseOutHandler.bind(this); - this.getTransformedSvgPoint = this.getTransformedSvgPoint.bind(this); - this.svgCoordinateToPercentages = this.svgCoordinateToPercentages.bind(this); + this.mouseDownHandler = this.mouseDownHandler.bind(this); + this.mouseMoveHandler = throttle(this.mouseMoveHandler.bind(this), MOUSE_INTERVAL); + this.mouseUpHandler = this.mouseUpHandler.bind(this); this.mouseZoomHandler = this.mouseZoomHandler.bind(this); - this.zoomCalculation = this.zoomCalculation.bind(this); - this.doZoomCall = this.doZoomCall.bind(this); - this.tapHandler = this.tapHandler.bind(this); - this.zoomCall = this.zoomCall.bind(this); - - this.panZoom = this.panZoom.bind(this); - this.pinchZoom = this.pinchZoom.bind(this); - this.toolbarZoom = this.toolbarZoom.bind(this); - - this.isPortraitDoc = this.isPortraitDoc.bind(this); - this.doWidthBoundsDetection = this.doWidthBoundsDetection.bind(this); - this.doHeightBoundsDetection = this.doHeightBoundsDetection.bind(this); - this.calcViewedRegion = this.calcViewedRegion.bind(this); - this.onZoom = this.onZoom.bind(this); - - const { - viewBoxWidth, - viewBoxHeight, - slideWidth, - slideHeight, - slide, - presentationSize, - } = props; - - this.viewportW = slideWidth; - this.viewportH = slideHeight; - - this.viewedRegionX = slide.xOffset; - this.viewedRegionY = slide.yOffset; - this.viewedRegionW = (viewBoxWidth / slideWidth) * 100; - this.viewedRegionH = (viewBoxHeight / slideHeight) * 100; - - this.pageOrigW = slideWidth; - this.pageOrigH = slideHeight; - - this.parentW = presentationSize.presentationWidth; - this.parentH = presentationSize.presentationHeight; - - this.calcPageW = this.pageOrigW / (this.viewedRegionW / HUNDRED_PERCENT); - this.calcPageH = this.pageOrigH / (this.viewedRegionH / HUNDRED_PERCENT); - this.calcPageX = (this.viewedRegionX / HUNDRED_PERCENT) * this.calcPageW; - this.calcPageY = (this.viewedRegionY / HUNDRED_PERCENT) * this.calcPageH; this.tapedTwice = false; - this.touches = []; } componentDidMount() { const { - viewBoxWidth, + zoom, slideWidth, - zoomChanger, + svgWidth, + svgHeight, + userIsPresenter, } = this.props; - const realZoom = (viewBoxWidth / slideWidth) * 100; + if (userIsPresenter) { + this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT; + this.viewBoxH = svgHeight / svgWidth * this.viewBoxW; - const zoomPercentage = (Math.round((100 / realZoom) * 100)); - const roundedUpToFive = Math.round(zoomPercentage / 5) * 5; - zoomChanger(roundedUpToFive); - this.doZoomCall(HUNDRED_PERCENT, 0, 0); + this.doWidthBoundsDetection(); + this.doHeightBoundsDetection(); + + this.pushSlideUpdate(); + } } componentDidUpdate(prevProps) { const { zoom, - delta, - touchZoom, - presentationSize, - slideHeight, - slideWidth, fitToWidth, + svgWidth, + svgHeight, + slideWidth, + userIsPresenter, + slide, } = this.props; - const isDifferent = zoom !== this.state.zoom && !touchZoom; - const moveSLide = ((delta.x !== prevProps.delta.x) - || (delta.y !== prevProps.delta.y)) && !isDifferent; - const isTouchZoom = zoom !== this.state.zoom && touchZoom; - if (moveSLide) { - this.panZoom(); - } - if (isTouchZoom) { - this.pinchZoom(); - } + if (!userIsPresenter) return; - if (isDifferent) { + if (zoom !== this.prevZoom) { this.toolbarZoom(); } - if (fitToWidth) { - if (!prevProps.fitToWidth || this.checkResize(prevProps.presentationSize)) { - this.parentH = presentationSize.presentationHeight; - this.parentW = presentationSize.presentationWidth; - this.viewportH = this.parentH; - this.viewportW = this.parentW; - this.doZoomCall(HUNDRED_PERCENT, 0, 0); - } - } else if (prevProps.fitToWidth) { - this.viewportH = slideHeight; - this.viewportW = slideWidth; - this.doZoomCall(HUNDRED_PERCENT, 0, 0); - } - } - - onZoom(zoomValue, mouseX, mouseY) { - let absXcoordInPage = (Math.abs(this.calcPageX) * MYSTERY_NUM) + mouseX; - let absYcoordInPage = (Math.abs(this.calcPageY) * MYSTERY_NUM) + mouseY; - - const relXcoordInPage = absXcoordInPage / this.calcPageW; - const relYcoordInPage = absYcoordInPage / this.calcPageH; - - if (this.isPortraitDoc()) { - if (this.props.fitToWidth) { - this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT; - this.calcPageH = (this.calcPageW / this.pageOrigW) * this.pageOrigH; - } else { - this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT; - this.calcPageW = (this.pageOrigW / this.pageOrigH) * this.calcPageH; - } - } else if (this.props.fitToWidth) { - this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT; - this.calcPageH = (this.calcPageW / this.pageOrigW) * this.pageOrigH; - } else { - this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT; - this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT; - } - - absXcoordInPage = relXcoordInPage * this.calcPageW; - absYcoordInPage = relYcoordInPage * this.calcPageH; + if (fitToWidth !== prevProps.fitToWidth + || this.checkResize(prevProps.svgWidth, prevProps.svgHeight) + || slide.id !== prevProps.slide.id) { + this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT; + this.viewBoxH = svgHeight / svgWidth * this.viewBoxW; - this.calcPageX = -((absXcoordInPage - mouseX) / MYSTERY_NUM); - this.calcPageY = -((absYcoordInPage - mouseY) / MYSTERY_NUM); + this.doWidthBoundsDetection(); + this.doHeightBoundsDetection(); - this.doWidthBoundsDetection(); - this.doHeightBoundsDetection(); + this.pushSlideUpdate(); + } + } - this.calcViewedRegion(); + componentWillUnmount() { + window.removeEventListener('mousemove', this.mouseMoveHandler); + window.removeEventListener('mouseup', this.mouseUpHandler); } getTransformedSvgPoint(clientX, clientY) { - const svgObject = this.props.getSvgRef(); + const { + getSvgRef, + } = this.props; + const svgObject = getSvgRef(); // If svgObject is not ready, return origin if (!svgObject) return { x: 0, y: 0 }; const screenPoint = svgObject.createSVGPoint(); @@ -197,160 +133,154 @@ export default class PresentationOverlay extends Component { return screenPoint.matrixTransform(CTM.inverse()); } - checkResize(prevPresentationSize) { - const { presentationSize } = this.props; - const heightChanged = prevPresentationSize.presentationHeight !== presentationSize.presentationHeight; - const widthChanged = prevPresentationSize.presentationWidth !== presentationSize.presentationWidth; - return heightChanged || widthChanged; + pushSlideUpdate() { + const { + updateLocalPosition, + panAndZoomChanger, + } = this.props; + + if (this.didPositionChange()) { + this.calcViewedRegion(); + updateLocalPosition( + this.viewBoxX, this.viewBoxY, + this.viewBoxW, this.viewBoxH, + this.prevZoom, + ); + panAndZoomChanger( + this.viewedRegionW, this.viewedRegionH, + this.viewedRegionX, this.viewedRegionY, + ); + } } - panZoom() { + checkResize(prevWidth, prevHeight) { const { - delta, + svgWidth, + svgHeight, } = this.props; - this.deltaX = delta.x; - this.deltaY = delta.y; - this.calcPageX += delta.x * -1; - this.calcPageY += delta.y * -1; - this.doHeightBoundsDetection(); - this.doWidthBoundsDetection(); - this.calcViewedRegion(); - this.zoomCall( - this.state.zoom, - this.viewedRegionW, - this.viewedRegionH, - this.viewedRegionX, - this.viewedRegionY, - ); + + const heightChanged = svgWidth !== prevWidth; + const widthChanged = svgHeight !== prevHeight; + return heightChanged || widthChanged; } - pinchZoom() { + didPositionChange() { const { - zoom, + viewBoxX, + viewBoxY, + viewBoxWidth, + viewBoxHeight, } = this.props; - const posX = this.touches[0].clientX; - const posY = this.touches[0].clientY; - this.doZoomCall(zoom, posX, posY); + + return this.viewBoxX !== viewBoxX || this.viewBoxY !== viewBoxY + || this.viewBoxW !== viewBoxWidth || this.viewBoxH !== viewBoxHeight; } - toolbarZoom() { + panSlide(deltaX, deltaY) { const { - getSvgRef, zoom, } = this.props; - const svgRect = getSvgRef().getBoundingClientRect(); - const svgCenterX = svgRect.left + (svgRect.width / 2); - const svgCenterY = svgRect.top + (svgRect.height / 2); - this.doZoomCall(zoom, svgCenterX, svgCenterY); + this.viewBoxX += deltaX; + this.viewBoxY += deltaY; + this.doHeightBoundsDetection(); + this.doWidthBoundsDetection(); + + this.prevZoom = zoom; + this.pushSlideUpdate(); } - isPortraitDoc() { - return this.pageOrigH > this.pageOrigW; + toolbarZoom() { + const { zoom } = this.props; + + const viewPortCenterX = this.viewBoxW / 2 + this.viewBoxX; + const viewPortCenterY = this.viewBoxH / 2 + this.viewBoxY; + this.doZoomCall(zoom, viewPortCenterX, viewPortCenterY); } doWidthBoundsDetection() { - const verifyPositionToBound = (this.calcPageW + (this.calcPageX * MYSTERY_NUM)); - if (this.calcPageX >= 0) { - this.calcPageX = 0; - } else if (verifyPositionToBound < this.viewportW) { - this.calcPageX = (this.viewportW - this.calcPageW) / MYSTERY_NUM; + const { + slideWidth, + } = this.props; + + const verifyPositionToBound = (this.viewBoxW + this.viewBoxX); + if (this.viewBoxX <= 0) { + this.viewBoxX = 0; + } else if (verifyPositionToBound > slideWidth) { + this.viewBoxX = (slideWidth - this.viewBoxW); } } doHeightBoundsDetection() { - const verifyPositionToBound = (this.calcPageH + (this.calcPageY * MYSTERY_NUM)); - if (this.calcPageY >= 0) { - this.calcPageY = 0; - } else if (verifyPositionToBound < this.viewportH) { - this.calcPageY = (this.viewportH - this.calcPageH) / MYSTERY_NUM; + const { + slideHeight, + } = this.props; + + const verifyPositionToBound = (this.viewBoxH + this.viewBoxY); + if (this.viewBoxY < 0) { + this.viewBoxY = 0; + } else if (verifyPositionToBound > slideHeight) { + this.viewBoxY = (slideHeight - this.viewBoxH); } } calcViewedRegion() { - this.viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(this.viewportW, this.calcPageW); - this.viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(this.viewportH, this.calcPageH); - this.viewedRegionX = SlideCalcUtil.calcViewedRegionX(this.calcPageX, this.calcPageW); - this.viewedRegionY = SlideCalcUtil.calcViewedRegionY(this.calcPageY, this.calcPageH); - } + const { + slideWidth, + slideHeight, + } = this.props; - checkCursor() { - // check if the cursor hasn't moved since last check - if (this.lastSentClientX !== this.currentClientX - || this.lastSentClientY !== this.currentClientY) { - const { currentClientX, currentClientY } = this; - // retrieving a transformed coordinate - let transformedSvgPoint = this.getTransformedSvgPoint(currentClientX, currentClientY); - - // determining the cursor's coordinates as percentages from the slide's width/height - transformedSvgPoint = this.svgCoordinateToPercentages(transformedSvgPoint); - // updating last sent raw coordinates - this.lastSentClientX = currentClientX; - this.lastSentClientY = currentClientY; - - // sending the update to the server - this.props.updateCursor({ - xPercent: transformedSvgPoint.x, - yPercent: transformedSvgPoint.y, - whiteboardId: this.props.whiteboardId, - }); - } + this.viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(this.viewBoxW, slideWidth); + this.viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(this.viewBoxH, slideHeight); + this.viewedRegionX = SlideCalcUtil.calcViewedRegionX(this.viewBoxX, slideWidth); + this.viewedRegionY = SlideCalcUtil.calcViewedRegionY(this.viewBoxY, slideHeight); } // receives an svg coordinate and changes the values to percentages of the slide's width/height svgCoordinateToPercentages(svgPoint) { + const { + slideWidth, + slideHeight, + } = this.props; + const point = { - x: (svgPoint.x / this.props.slideWidth) * 100, - y: (svgPoint.y / this.props.slideHeight) * 100, + x: (svgPoint.x / slideWidth) * 100, + y: (svgPoint.y / slideHeight) * 100, }; return point; } - zoomCalculation(zoom, mouseX, mouseY) { - const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY); - this.onZoom(zoom, svgPosition.x, svgPosition.y); - - return { - viewedRegionW: this.viewedRegionW, - viewedRegionH: this.viewedRegionH, - viewedRegionX: this.viewedRegionX, - viewedRegionY: this.viewedRegionY, - }; - } - - zoomCall(zoom, w, h, x, y) { + doZoomCall(zoom, mouseX, mouseY) { const { - zoomSlide, - podId, - currentSlideNum, - isPresenter, + svgWidth, + svgHeight, + slideWidth, } = this.props; - if (!isPresenter) return; - zoomSlide(currentSlideNum, podId, w, h, x, y); - this.setState({ zoom }); - this.props.zoomChanger(zoom); - } - doZoomCall(zoom, mouseX, mouseY) { - const zoomData = this.zoomCalculation(zoom, mouseX, mouseY); + const relXcoordInViewport = (mouseX - this.viewBoxX) / this.viewBoxW; + const relYcoordInViewport = (mouseY - this.viewBoxY) / this.viewBoxH; - const { - viewedRegionW, - viewedRegionH, - viewedRegionX, - viewedRegionY, - } = zoomData; + this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT; + this.viewBoxH = svgHeight / svgWidth * this.viewBoxW; + + this.viewBoxX = mouseX - (relXcoordInViewport * this.viewBoxW); + this.viewBoxY = mouseY - (relYcoordInViewport * this.viewBoxH); + + this.doWidthBoundsDetection(); + this.doHeightBoundsDetection(); - this.zoomCall(zoom, viewedRegionW, viewedRegionH, viewedRegionX, viewedRegionY); + this.prevZoom = zoom; + this.pushSlideUpdate(); } mouseZoomHandler(e) { const { zoom, - whiteboardId, - updateCursor, + userIsPresenter, } = this.props; + if (!userIsPresenter) return; + let newZoom = zoom; if (e.deltaY < 0) { newZoom += STEP; @@ -364,40 +294,92 @@ export default class PresentationOverlay extends Component { newZoom = MAX_PERCENT; } - const mouseX = e.clientX; - const mouseY = e.clientY; - const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY); - const svgPercentage = this.svgCoordinateToPercentages(svgPosition); - - this.doZoomCall(newZoom, mouseX, mouseY); - updateCursor({ - xPercent: svgPercentage.x, - yPercent: svgPercentage.y, - whiteboardId, - }); + if (newZoom === zoom) return; + + const svgPosition = this.getTransformedSvgPoint(e.clientX, e.clientY); + this.doZoomCall(newZoom, svgPosition.x, svgPosition.y); + } + + pinchStartHandler(event) { + if (!this.pinchGesture) return; + + this.prevDiff = PresentationOverlay.calculateDistance(event.touches); + } + + pinchMoveHandler(event) { + const { + zoom, + } = this.props; + + if (!this.pinchGesture) return; + if (event.touches.length < 2) return; + + const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches); + const currDiff = PresentationOverlay.calculateDistance(event.touches); + + if (currDiff > 0) { + const newZoom = zoom + (currDiff - this.prevDiff); + + const svgPosition = this.getTransformedSvgPoint(touchCenterPoint.x, touchCenterPoint.y); + this.doZoomCall(newZoom, svgPosition.x, svgPosition.y); + } + this.prevDiff = currDiff; + } + + panStartHandler(event) { + if (this.pinchGesture) return; + + const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches); + this.currentMouseX = touchCenterPoint.x; + this.currentMouseY = touchCenterPoint.y; + } + + panMoveHandler(event) { + const { + slideHeight, + physicalSlideHeight, + } = this.props; + + if (this.pinchGesture) return; + + const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches); + + const physicalRatio = slideHeight / physicalSlideHeight; + const mouseDeltaX = physicalRatio * (this.currentMouseX - touchCenterPoint.x); + const mouseDeltaY = physicalRatio * (this.currentMouseY - touchCenterPoint.y); + this.currentMouseX = touchCenterPoint.x; + this.currentMouseY = touchCenterPoint.y; + this.panSlide(mouseDeltaX, mouseDeltaY); } tapHandler(event) { - const AnnotationTool = WhiteboardToolbarService - .getCurrentDrawSettings().whiteboardAnnotationTool; + const { annotationTool } = this.props; if (event.touches.length === 2) return; if (!this.tapedTwice) { this.tapedTwice = true; - setTimeout(() => this.tapedTwice = false, 300); + setTimeout(() => (this.tapedTwice = false), 300); return; } event.preventDefault(); - const sizeDefault = this.state.zoom === HUNDRED_PERCENT; + const sizeDefault = this.prevZoom === HUNDRED_PERCENT; - if (sizeDefault && AnnotationTool === 'hand') { - this.doZoomCall(200, this.currentClientX, this.currentClientY); + if (sizeDefault && annotationTool === HAND_TOOL) { + const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches); + this.currentMouseX = touchCenterPoint.x; + this.currentMouseY = touchCenterPoint.y; + this.doZoomCall(200, touchCenterPoint.x, touchCenterPoint.y); return; } this.doZoomCall(HUNDRED_PERCENT, 0, 0); } handleTouchStart(event) { + const { + annotationTool, + } = this.props; + + if (annotationTool !== HAND_TOOL) return; // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box window.addEventListener('touchend', this.handleTouchEnd, { passive: false }); window.addEventListener('touchmove', this.handleTouchMove, { passive: false }); @@ -405,42 +387,42 @@ export default class PresentationOverlay extends Component { this.touchStarted = true; - const { clientX, clientY } = event.changedTouches[0]; - this.currentClientX = clientX; - this.currentClientY = clientY; - if (event.touches.length === 2) { - this.touches = [...event.touches]; + const numberTouches = event.touches.length; + if (numberTouches === 2) { + this.pinchGesture = true; + this.pinchStartHandler(event); + } else if (numberTouches === 1) { + this.pinchGesture = false; + this.panStartHandler(event); } - const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL); - this.intervalId = intervalId; + // / TODO Figure out what to do with this later this.tapHandler(event); } handleTouchMove(event) { - event.preventDefault(); + const { + annotationTool, + userIsPresenter, + } = this.props; + + if (annotationTool !== HAND_TOOL || !userIsPresenter) return; - const { clientX, clientY } = event.changedTouches[0]; + event.preventDefault(); - this.currentClientX = clientX; - this.currentClientY = clientY; + if (this.pinchGesture) { + this.pinchMoveHandler(event); + } else { + this.panMoveHandler(event); + } } handleTouchEnd(event) { event.preventDefault(); - // touch ended, removing the interval - clearInterval(this.intervalId); - this.intervalId = 0; - // resetting the touchStarted flag this.touchStarted = false; - // setting the coords to negative values and send the last message (the cursor will disappear) - this.currentClientX = -1; - this.currentClientY = -1; - this.checkCursor(); - window.removeEventListener('touchend', this.handleTouchEnd, { passive: false }); window.removeEventListener('touchmove', this.handleTouchMove, { passive: false }); window.removeEventListener('touchcancel', this.handleTouchCancel, true); @@ -449,72 +431,131 @@ export default class PresentationOverlay extends Component { handleTouchCancel(event) { event.preventDefault(); - // touch was cancelled, removing the interval - clearInterval(this.intervalId); - this.intervalId = 0; - - // resetting the touchStarted flag - this.touchStarted = false; - - // setting the coords to negative values and send the last message (the cursor will disappear) - this.currentClientX = -1; - this.currentClientY = -1; - this.checkCursor(); - window.removeEventListener('touchend', this.handleTouchEnd, { passive: false }); window.removeEventListener('touchmove', this.handleTouchMove, { passive: false }); window.removeEventListener('touchcancel', this.handleTouchCancel, true); } - mouseMoveHandler(event) { - if (this.touchStarted) { - return; + mouseDownHandler(event) { + const { + annotationTool, + userIsPresenter, + } = this.props; + + if (annotationTool !== HAND_TOOL || !userIsPresenter) return; + + const isLeftClick = event.button === 0; + if (isLeftClick) { + this.currentMouseX = event.clientX; + this.currentMouseY = event.clientY; + + this.setState({ + pressed: true, + }); + + window.addEventListener('mousemove', this.mouseMoveHandler, { passive: false }); + window.addEventListener('mouseup', this.mouseUpHandler, { passive: false }); } - this.currentClientX = event.clientX; - this.currentClientY = event.clientY; - this.checkCursor(); } - mouseEnterHandler(event) { - if (this.touchStarted) { - return; + mouseMoveHandler(event) { + const { + slideHeight, + annotationTool, + physicalSlideHeight, + } = this.props; + + const { + pressed, + } = this.state; + + if (annotationTool !== HAND_TOOL) return; + + if (pressed) { + const mouseDeltaX = slideHeight / physicalSlideHeight * (this.currentMouseX - event.clientX); + const mouseDeltaY = slideHeight / physicalSlideHeight * (this.currentMouseY - event.clientY); + + this.currentMouseX = event.clientX; + this.currentMouseY = event.clientY; + this.panSlide(mouseDeltaX, mouseDeltaY); } - this.currentClientX = event.clientX; - this.currentClientY = event.clientY; - this.checkCursor(); } - mouseOutHandler() { - // mouse left the whiteboard, removing the interval - clearInterval(this.intervalId); - this.intervalId = 0; - // setting the coords to negative values and send the last message (the cursor will disappear) - this.currentClientX = -1; - this.currentClientY = -1; - this.checkCursor(); + mouseUpHandler(event) { + const { + pressed, + } = this.state; + + const isLeftClick = event.button === 0; + + if (isLeftClick && pressed) { + this.setState({ + pressed: false, + }); + + window.removeEventListener('mousemove', this.mouseMoveHandler); + window.removeEventListener('mouseup', this.mouseUpHandler); + } } render() { + const { + viewBoxX, + viewBoxY, + viewBoxWidth, + viewBoxHeight, + slideWidth, + slideHeight, + children, + userIsPresenter, + } = this.props; + + const { + pressed, + } = this.state; + + this.viewBoxW = viewBoxWidth; + this.viewBoxH = viewBoxHeight; + this.viewBoxX = viewBoxX; + this.viewBoxY = viewBoxY; + + const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename; + + let cursor; + if (!userIsPresenter) { + cursor = undefined; + } else if (pressed) { + cursor = `url('${baseName}/resources/images/whiteboard-cursor/pan-closed.png') 4 8 , default`; + } else { + cursor = `url('${baseName}/resources/images/whiteboard-cursor/pan.png') 4 8, default`; + } + + const overlayStyle = { + width: '100%', + height: '100%', + touchAction: 'none', + zIndex: MAX_Z_INDEX, + cursor, + }; + return ( <foreignObject clipPath="url(#viewBox)" x="0" y="0" - width={this.props.slideWidth} - height={this.props.slideHeight} - // maximun value of z-index to prevent other things from overlapping - style={{ zIndex: 2 ** 31 - 1 }} + width={slideWidth} + height={slideHeight} + style={{ zIndex: MAX_Z_INDEX }} > <div + role="presentation" onTouchStart={this.handleTouchStart} - onMouseOut={this.mouseOutHandler} - onMouseEnter={this.mouseEnterHandler} - onMouseMove={this.mouseMoveHandler} + onMouseDown={this.mouseDownHandler} onWheel={this.mouseZoomHandler} onBlur={() => {}} - style={{ width: '100%', height: '100%', touchAction: 'none' }} + style={overlayStyle} > - {this.props.children} + {children} </div> </foreignObject> ); @@ -522,19 +563,59 @@ export default class PresentationOverlay extends Component { } PresentationOverlay.propTypes = { - whiteboardId: PropTypes.string.isRequired, - // Defines a function which returns a reference to the main svg object getSvgRef: PropTypes.func.isRequired, + // Defines the current zoom level (100 -> 400) + zoom: PropTypes.number.isRequired, + + // Defines the width of the parent SVG. Used with svgHeight for aspect ratio + svgWidth: PropTypes.number.isRequired, + + // Defines the height of the parent SVG. Used with svgWidth for aspect ratio + svgHeight: PropTypes.number.isRequired, + // Defines the calculated slide width (in svg coordinate system) slideWidth: PropTypes.number.isRequired, // Defines the calculated slide height (in svg coordinate system) slideHeight: PropTypes.number.isRequired, - // A function to send a cursor update - updateCursor: PropTypes.func.isRequired, + // Defines the local X value for the viewbox. Needed for pan/zoom + viewBoxX: PropTypes.number.isRequired, + + // Defines the local Y value for the viewbox. Needed for pan/zoom + viewBoxY: PropTypes.number.isRequired, + + // Defines the local width of the view box + viewBoxWidth: PropTypes.number.isRequired, + + // Defines the local height of the view box + viewBoxHeight: PropTypes.number.isRequired, + + // Defines the height of the slide in page coordinates for mouse movement + physicalSlideHeight: PropTypes.number.isRequired, + + // Defines whether the local user has rights to change the slide position/dimensions + userIsPresenter: PropTypes.bool.isRequired, + + // Defines whether the presentation area is in fitToWidth mode or not + fitToWidth: PropTypes.bool.isRequired, + + // Defines the slide data. There's more in there, but we don't need it here + slide: PropTypes.shape({ + // Defines the slide id. Used to tell if we changed slides + id: PropTypes.string.isRequired, + }).isRequired, + + // Defines a function to send the new viewbox position and size for presenter rendering + updateLocalPosition: PropTypes.func.isRequired, + + // Defines a function to send the new percent based position and size to other users + panAndZoomChanger: PropTypes.func.isRequired, + + // Defines the currently selected annotation tool. When "hand" we can pan + annotationTool: PropTypes.string.isRequired, // As a child we expect only a WhiteboardOverlay at this point children: PropTypes.element.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx old mode 100644 new mode 100755 index 13ddaf8b5c..3992c338d4 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx @@ -1,10 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; -import PresentationOverlayService from './service'; -import PresentationToolbarService from '../presentation-toolbar/service'; -import PresentationService from '../service'; import PresentationOverlay from './component'; +import WhiteboardToolbarService from '../../whiteboard/whiteboard-toolbar/service'; const PresentationOverlayContainer = ({ children, ...rest }) => ( <PresentationOverlay {...rest}> @@ -12,14 +10,17 @@ const PresentationOverlayContainer = ({ children, ...rest }) => ( </PresentationOverlay> ); -export default withTracker(({ podId, currentSlideNum, slide }) => ({ - slide, - podId, - currentSlideNum, - updateCursor: PresentationOverlayService.updateCursor, - zoomSlide: PresentationToolbarService.zoomSlide, - isPresenter: PresentationService.isPresenter(podId), -}))(PresentationOverlayContainer); +export default withTracker(({ podId, currentSlideNum, slide }) => { + const drawSettings = WhiteboardToolbarService.getCurrentDrawSettings(); + const tool = drawSettings ? drawSettings.whiteboardAnnotationTool : ''; + + return { + slide, + podId, + currentSlideNum, + annotationTool: tool, + }; +})(PresentationOverlayContainer); PresentationOverlayContainer.propTypes = { children: PropTypes.node, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/service.js deleted file mode 100755 index 19d2b27e68..0000000000 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/service.js +++ /dev/null @@ -1,10 +0,0 @@ -import { makeCall } from '/imports/ui/services/api'; -import { publishCursorUpdate } from '/imports/ui/components/cursor/service'; - -const updateCursor = (payload) => { - publishCursorUpdate(payload); -}; - -export default { - updateCursor, -}; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx old mode 100644 new mode 100755 index 38fe89f646..32ef0ebaab --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -79,15 +79,12 @@ class PresentationToolbar extends Component { constructor(props) { super(props); - this.state = { - sliderValue: 100, - }; - this.handleValuesChange = this.handleValuesChange.bind(this); this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this); this.change = this.change.bind(this); this.renderAriaDescs = this.renderAriaDescs.bind(this); this.switchSlide = this.switchSlide.bind(this); - this.setInt = 0; + this.nextSlideHandler = this.nextSlideHandler.bind(this); + this.previousSlideHandler = this.previousSlideHandler.bind(this); } componentDidMount() { @@ -101,30 +98,45 @@ class PresentationToolbar extends Component { switchSlide(event) { const { target, which } = event; const isBody = target.nodeName === 'BODY'; - const { actions } = this.props; if (isBody) { if ([KEY_CODES.ARROW_LEFT].includes(which)) { - actions.previousSlideHandler(); + this.previousSlideHandler(); } if ([KEY_CODES.ARROW_RIGHT].includes(which)) { - actions.nextSlideHandler(); + this.nextSlideHandler(); } } } handleSkipToSlideChange(event) { - const { actions } = this.props; + const { + skipToSlide, + podId, + } = this.props; const requestedSlideNum = Number.parseInt(event.target.value, 10); - actions.skipToSlideHandler(requestedSlideNum); + skipToSlide(requestedSlideNum, podId); } - handleValuesChange(event) { - const { sliderValue } = this.state; - this.setState( - { sliderValue: event.target.value }, - () => this.handleZoom(sliderValue), - ); + nextSlideHandler() { + const { + nextSlide, + currentSlideNum, + numberOfSlides, + podId, + } = this.props; + + nextSlide(currentSlideNum, numberOfSlides, podId); + } + + previousSlideHandler() { + const { + previousSlide, + currentSlideNum, + podId, + } = this.props; + + previousSlide(currentSlideNum, podId); } change(value) { @@ -187,7 +199,6 @@ class PresentationToolbar extends Component { numberOfSlides, fitToWidthHandler, fitToWidth, - actions, intl, zoom, isFullscreen, @@ -225,7 +236,7 @@ class PresentationToolbar extends Component { color="default" icon="left_arrow" size="md" - onClick={actions.previousSlideHandler} + onClick={this.previousSlideHandler} label={intl.formatMessage(intlMessages.previousSlideLabel)} hideLabel className={cx(styles.prevSlide, styles.presentationBtn)} @@ -258,7 +269,7 @@ class PresentationToolbar extends Component { color="default" icon="right_arrow" size="md" - onClick={actions.nextSlideHandler} + onClick={this.nextSlideHandler} label={intl.formatMessage(intlMessages.nextSlideLabel)} hideLabel className={cx(styles.skipSlide, styles.presentationBtn)} @@ -322,16 +333,16 @@ class PresentationToolbar extends Component { } PresentationToolbar.propTypes = { + // The Id for the current pod. Should always be default pod + podId: PropTypes.string.isRequired, // Number of current slide being displayed currentSlideNum: PropTypes.number.isRequired, // Total number of slides in this presentation numberOfSlides: PropTypes.number.isRequired, // Actions required for the presenter toolbar - actions: PropTypes.shape({ - nextSlideHandler: PropTypes.func.isRequired, - previousSlideHandler: PropTypes.func.isRequired, - skipToSlideHandler: PropTypes.func.isRequired, - }).isRequired, + nextSlide: PropTypes.func.isRequired, + previousSlide: PropTypes.func.isRequired, + skipToSlide: PropTypes.func.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx old mode 100644 new mode 100755 index 6baededdd4..e751556a31 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -8,17 +8,8 @@ import PresentationToolbarService from './service'; const PresentationToolbarContainer = (props) => { const { - currentSlideNum, userIsPresenter, - numberOfSlides, - actions, - zoom, - zoomChanger, - fitToWidthHandler, getSwapLayout, - isFullscreen, - fullscreenRef, - fitToWidth, } = props; if (userIsPresenter && !getSwapLayout) { @@ -26,17 +17,7 @@ const PresentationToolbarContainer = (props) => { return ( <PresentationToolbar - {...{ - isFullscreen, - fullscreenRef, - currentSlideNum, - numberOfSlides, - actions, - zoom, - zoomChanger, - fitToWidthHandler, - fitToWidth, - }} + {...props} /> ); } @@ -45,43 +26,29 @@ const PresentationToolbarContainer = (props) => { export default withTracker((params) => { const { - podId, presentationId, fitToWidth, fullscreenRef, + podId, + presentationId, + fitToWidth, + fullscreenRef, + zoom, + zoomChanger, + currentSlideNum, + fitToWidthHandler, } = params; - const data = PresentationToolbarService.getSlideData(podId, presentationId); - - const { - numberOfSlides, - } = data; return { getSwapLayout: MediaService.getSwapLayout(), - fitToWidthHandler: params.fitToWidthHandler, + fitToWidthHandler, fitToWidth, fullscreenRef, userIsPresenter: PresentationService.isPresenter(podId), - numberOfSlides, - zoom: params.zoom, - zoomChanger: params.zoomChanger, - actions: { - nextSlideHandler: () => PresentationToolbarService.nextSlide( - params.currentSlideNum, - numberOfSlides, - podId, - ), - previousSlideHandler: () => PresentationToolbarService.previousSlide( - params.currentSlideNum, - podId, - ), - skipToSlideHandler: requestedSlideNum => PresentationToolbarService.skipToSlide( - requestedSlideNum, - podId, - ), - zoomSlideHandler: value => PresentationToolbarService.zoomSlide( - params.currentSlideNum, - podId, - value, - ), - }, + numberOfSlides: PresentationToolbarService.getNumberOfSlides(podId, presentationId), + zoom, + zoomChanger, + currentSlideNum, + nextSlide: PresentationToolbarService.nextSlide, + previousSlide: PresentationToolbarService.previousSlide, + skipToSlide: PresentationToolbarService.skipToSlide, }; })(PresentationToolbarContainer); @@ -98,9 +65,7 @@ PresentationToolbarContainer.propTypes = { numberOfSlides: PropTypes.number.isRequired, // Actions required for the presenter toolbar - actions: PropTypes.shape({ - nextSlideHandler: PropTypes.func.isRequired, - previousSlideHandler: PropTypes.func.isRequired, - skipToSlideHandler: PropTypes.func.isRequired, - }).isRequired, + nextSlide: PropTypes.func.isRequired, + previousSlide: PropTypes.func.isRequired, + skipToSlide: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js old mode 100644 new mode 100755 index 4eccf1a01f..465580e3c6 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -1,21 +1,20 @@ import Auth from '/imports/ui/services/auth'; -import Slides from '/imports/api/slides'; +import Presentations from '/imports/api/presentations'; import { makeCall } from '/imports/ui/services/api'; +import { throttle } from 'lodash'; -const getSlideData = (podId, presentationId) => { - // Get meetingId and userId +const PAN_ZOOM_INTERVAL = Meteor.settings.public.presentation.panZoomInterval || 200; + +const getNumberOfSlides = (podId, presentationId) => { const meetingId = Auth.meetingID; - // Get total number of slides in this presentation - const numberOfSlides = Slides.find({ + const presentation = Presentations.findOne({ meetingId, podId, - presentationId, - }).fetch().length; + id: presentationId, + }); - return { - numberOfSlides, - }; + return presentation ? presentation.pages.length : 0; }; const previousSlide = (currentSlideNum, podId) => { @@ -30,17 +29,16 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { } }; -const zoomSlide = (currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { +const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset); -}; - +}, PAN_ZOOM_INTERVAL); const skipToSlide = (requestedSlideNum, podId) => { makeCall('switchSlide', requestedSlideNum, podId); }; export default { - getSlideData, + getNumberOfSlides, nextSlide, previousSlide, skipToSlide, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js old mode 100644 new mode 100755 index a8b7a1f209..83828386da --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -143,9 +143,14 @@ const parseCurrentSlideContent = (yesValue, noValue, trueValue, falseValue) => { const isPresenter = (podId) => { // a main presenter in the meeting always owns a default pod if (podId === 'DEFAULT_PRESENTATION_POD') { + const options = { + filter: { + presenter: 1, + }, + }; const currentUser = Users.findOne({ userId: Auth.userID, - }); + }, options); return currentUser ? currentUser.presenter : false; } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx index 53ca80b900..7469bfdc64 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ShapeDrawListener from './shape-draw-listener/component'; import TextDrawListener from './text-draw-listener/component'; import PencilDrawListener from './pencil-draw-listener/component'; -import PanZoomDrawListener from './pan-zoom-draw-listener/component'; +import CursorListener from './cursor-listener/component'; export default class WhiteboardOverlay extends Component { // a function to transform a screen point to svg point @@ -48,7 +48,11 @@ export default class WhiteboardOverlay extends Component { // this function receives an event from the mouse event attached to the window // it transforms the coordinate to the main svg coordinate system getTransformedSvgPoint(clientX, clientY) { - const svgObject = this.props.getSvgRef(); + const { + getSvgRef, + } = this.props; + + const svgObject = getSvgRef(); const svgPoint = svgObject.createSVGPoint(); svgPoint.x = clientX; svgPoint.y = clientY; @@ -59,32 +63,52 @@ export default class WhiteboardOverlay extends Component { // receives an svg coordinate and changes the values to percentages of the slide's width/height svgCoordinateToPercentages(svgPoint) { + const { + slideWidth, + slideHeight, + } = this.props; + const point = { - x: (svgPoint.x / this.props.slideWidth) * 100, - y: (svgPoint.y / this.props.slideHeight) * 100, + x: (svgPoint.x / slideWidth) * 100, + y: (svgPoint.y / slideHeight) * 100, }; return point; } normalizeThickness(thickness) { - return (thickness * 100) / this.props.physicalSlideWidth; + const { + physicalSlideWidth, + } = this.props; + + return (thickness * 100) / physicalSlideWidth; } normalizeFont(fontSize) { - return (fontSize * 100) / this.props.physicalSlideHeight; + const { + physicalSlideHeight, + } = this.props; + + return (fontSize * 100) / physicalSlideHeight; } generateNewShapeId() { + const { + userId, + } = this.props; + this.count = this.count + 1; - this.currentShapeId = `${this.props.userId}-${this.count}-${new Date().getTime()}`; + this.currentShapeId = `${userId}-${this.count}-${new Date().getTime()}`; return this.currentShapeId; } // this function receives a transformed svg coordinate and checks if it's not out of bounds checkIfOutOfBounds(point) { const { - viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight, + viewBoxX, + viewBoxY, + viewBoxWidth, + viewBoxHeight, } = this.props; let { x, y } = point; @@ -119,34 +143,18 @@ export default class WhiteboardOverlay extends Component { }; } - render() { + renderDrawListener(actions) { const { drawSettings, userId, whiteboardId, - sendAnnotation, - resetTextShapeSession, - setTextShapeActiveId, - contextMenuHandler, - addAnnotationToDiscardedList, - undoAnnotation, + physicalSlideWidth, + physicalSlideHeight, + slideWidth, + slideHeight, } = this.props; + const { tool } = drawSettings; - const actions = { - getTransformedSvgPoint: this.getTransformedSvgPoint, - checkIfOutOfBounds: this.checkIfOutOfBounds, - svgCoordinateToPercentages: this.svgCoordinateToPercentages, - getCurrentShapeId: this.getCurrentShapeId, - generateNewShapeId: this.generateNewShapeId, - normalizeThickness: this.normalizeThickness, - normalizeFont: this.normalizeFont, - sendAnnotation, - resetTextShapeSession, - setTextShapeActiveId, - contextMenuHandler, - addAnnotationToDiscardedList, - undoAnnotation, - }; if (tool === 'triangle' || tool === 'rectangle' || tool === 'ellipse' || tool === 'line') { return ( @@ -157,37 +165,72 @@ export default class WhiteboardOverlay extends Component { whiteboardId={whiteboardId} /> ); - } else if (tool === 'pencil') { + } if (tool === 'pencil') { return ( <PencilDrawListener userId={userId} whiteboardId={whiteboardId} drawSettings={drawSettings} actions={actions} - physicalSlideWidth={this.props.physicalSlideWidth} - physicalSlideHeight={this.props.physicalSlideHeight} + physicalSlideWidth={physicalSlideWidth} + physicalSlideHeight={physicalSlideHeight} /> ); - } else if (tool === 'text') { + } if (tool === 'text') { return ( <TextDrawListener userId={userId} whiteboardId={whiteboardId} drawSettings={drawSettings} actions={actions} - slideWidth={this.props.slideWidth} - slideHeight={this.props.slideHeight} + slideWidth={slideWidth} + slideHeight={slideHeight} /> ); - } else if (tool === 'hand') { - return ( - <PanZoomDrawListener {...this.props} /> - ); } return ( <span /> ); } + + render() { + const { + whiteboardId, + sendAnnotation, + resetTextShapeSession, + setTextShapeActiveId, + contextMenuHandler, + addAnnotationToDiscardedList, + undoAnnotation, + updateCursor, + } = this.props; + + const actions = { + getTransformedSvgPoint: this.getTransformedSvgPoint, + checkIfOutOfBounds: this.checkIfOutOfBounds, + svgCoordinateToPercentages: this.svgCoordinateToPercentages, + getCurrentShapeId: this.getCurrentShapeId, + generateNewShapeId: this.generateNewShapeId, + normalizeThickness: this.normalizeThickness, + normalizeFont: this.normalizeFont, + sendAnnotation, + resetTextShapeSession, + setTextShapeActiveId, + contextMenuHandler, + addAnnotationToDiscardedList, + undoAnnotation, + }; + + return ( + <CursorListener + whiteboardId={whiteboardId} + actions={actions} + updateCursor={updateCursor} + > + {this.renderDrawListener(actions)} + </CursorListener> + ); + } } WhiteboardOverlay.propTypes = { @@ -232,4 +275,6 @@ WhiteboardOverlay.propTypes = { resetTextShapeSession: PropTypes.func.isRequired, // Defines a function that sets a session value for the current active text shape setTextShapeActiveId: PropTypes.func.isRequired, + // Defines a handler to publish cursor position to the server + updateCursor: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx index eee2d97002..86b1e08959 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx @@ -6,7 +6,8 @@ import WhiteboardToolbarService from '../whiteboard-toolbar/service'; import WhiteboardOverlay from './component'; const WhiteboardOverlayContainer = (props) => { - if (Object.keys(props.drawSettings).length > 0) { + const { drawSettings } = props; + if (Object.keys(drawSettings).length > 0) { return ( <WhiteboardOverlay {...props} /> ); @@ -23,6 +24,7 @@ export default withTracker(() => ({ resetTextShapeSession: WhiteboardOverlayService.resetTextShapeSession, drawSettings: WhiteboardOverlayService.getWhiteboardToolbarValues(), userId: WhiteboardOverlayService.getCurrentUserId(), + updateCursor: WhiteboardOverlayService.updateCursor, }))(WhiteboardOverlayContainer); diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx new file mode 100755 index 0000000000..37c4228f9d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx @@ -0,0 +1,216 @@ +import React, { Component } from 'react'; +import { throttle } from 'lodash'; +import PropTypes from 'prop-types'; + +// maximum value of z-index to prevent other things from overlapping +const MAX_Z_INDEX = (2 ** 31) - 1; +const CURSOR_INTERVAL = 32; + +export default class CursorListener extends Component { + static touchCenterPoint(touches) { + let totalX = 0; let + totalY = 0; + + for (let i = 0; i < touches.length; i += 1) { + totalX += touches[i].clientX; + totalY += touches[i].clientY; + } + + return { x: totalX / touches.length, y: totalY / touches.length }; + } + + constructor(props) { + super(props); + + // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent + // onmousedown from triggering right after. Thus we have to track it manually. + // In case if it's fixed one day - there is another issue, React one. + // https://github.com/facebook/react/issues/9809 + // Check it to figure if you can add onTouchStart in render(), or should use raw DOM api + this.touchStarted = false; + + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseLeave = this.handleMouseLeave.bind(this); + this.handleTouchStart = this.handleTouchStart.bind(this); + this.handleTouchMove = this.handleTouchMove.bind(this); + this.handleTouchEnd = this.handleTouchEnd.bind(this); + this.handleTouchCancel = this.handleTouchCancel.bind(this); + + this.checkCursor = throttle(this.checkCursor, CURSOR_INTERVAL); + } + + componentWillUnmount() { + this.removeTouchListeners(); + this.checkCursor(-1, -1); + } + + checkCursor(mouseX, mouseY) { + const { + actions, + updateCursor, + whiteboardId, + } = this.props; + + // check if the cursor hasn't moved since last check + if (this.lastSentClientX !== mouseX + || this.lastSentClientY !== mouseY) { + // retrieving a transformed coordinate + let transformedSvgPoint = actions.getTransformedSvgPoint(mouseX, mouseY); + + // determining the cursor's coordinates as percentages from the slide's width/height + transformedSvgPoint = actions.svgCoordinateToPercentages(transformedSvgPoint); + // updating last sent raw coordinates + this.lastSentClientX = mouseX; + this.lastSentClientY = mouseY; + this.lastSentXPercent = transformedSvgPoint.x; + this.lastSentYPercent = transformedSvgPoint.y; + + // sending the update to the server + updateCursor({ + xPercent: transformedSvgPoint.x, + yPercent: transformedSvgPoint.y, + whiteboardId, + }); + } + } + + checkLastCursor() { + const { + actions, + updateCursor, + whiteboardId, + } = this.props; + + if (this.lastSentClientX && this.lastSentClientY) { + let transformedSvgPoint = actions.getTransformedSvgPoint( + this.lastSentClientX, this.lastSentClientY, + ); + + // determining the cursor's coordinates as percentages from the slide's width/height + transformedSvgPoint = actions.svgCoordinateToPercentages(transformedSvgPoint); + + if (this.lastSentXPercent !== transformedSvgPoint.x + && this.lastSentYPercent !== transformedSvgPoint) { + // sending the update to the server + updateCursor({ + xPercent: transformedSvgPoint.x, + yPercent: transformedSvgPoint.y, + whiteboardId, + }); + } + } + } + + removeTouchListeners() { + window.removeEventListener('touchmove', this.handleTouchMove, { passive: false }); + window.removeEventListener('touchend', this.handleTouchEnd, { passive: false }); + window.removeEventListener('touchcancel', this.handleTouchCancel, true); + } + + handleMouseEnter(event) { + if (this.touchStarted) { + return; + } + + this.checkCursor(event.clientX, event.clientY); + } + + handleMouseMove(event) { + if (this.touchStarted) { + return; + } + + this.checkCursor(event.clientX, event.clientY); + } + + handleMouseLeave() { + this.checkCursor(-1, -1); + } + + handleTouchStart(event) { + event.preventDefault(); + + window.addEventListener('touchmove', this.handleTouchMove, { passive: false }); + window.addEventListener('touchend', this.handleTouchEnd, { passive: false }); + window.addEventListener('touchcancel', this.handleTouchCancel, true); + + this.touchStarted = true; + + const midPoint = CursorListener.touchCenterPoint(event.touches); + + this.checkCursor(midPoint.x, midPoint.y); + } + + handleTouchMove(event) { + event.preventDefault(); + + const midPoint = CursorListener.touchCenterPoint(event.touches); + + this.checkCursor(midPoint.x, midPoint.y); + } + + handleTouchEnd(event) { + event.preventDefault(); + + if (event.touches.length === 1) { + this.removeTouchListeners(); + this.touchStarted = false; + this.checkCursor(-1, -1); + } + } + + handleTouchCancel(event) { + event.preventDefault(); + + if (event.touches.length === 0) { + this.removeTouchListeners(); + this.touchStarted = false; + this.checkCursor(-1, -1); + } + } + + render() { + const { + children, + } = this.props; + + this.checkLastCursor(); + + const cursorStyle = { + width: '100%', + height: '100%', + touchAction: 'none', + zIndex: MAX_Z_INDEX, + }; + + return ( + <div + style={cursorStyle} + onMouseEnter={this.handleMouseEnter} + onMouseMove={this.handleMouseMove} + onMouseLeave={this.handleMouseLeave} + onTouchStart={this.handleTouchStart} + > + {children} + </div> + ); + } +} + +CursorListener.propTypes = { + // Defines a whiteboard id, which needed to publish an annotation message + whiteboardId: PropTypes.string.isRequired, + // Defines a function to send the new cursor location + updateCursor: PropTypes.func.isRequired, + // Defines an object containing all available actions + actions: PropTypes.shape({ + // Defines a function which transforms a coordinate from the window to svg coordinate system + getTransformedSvgPoint: PropTypes.func.isRequired, + // Defines a function which receives an svg point and transforms it into + // percentage-based coordinates + svgCoordinateToPercentages: PropTypes.func.isRequired, + }).isRequired, + // Expected to be any required draw listeners + children: PropTypes.element.isRequired, +}; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx deleted file mode 100644 index 9d37c00824..0000000000 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx +++ /dev/null @@ -1,183 +0,0 @@ -import React from 'react'; -// import PropTypes from 'prop-types'; - -export default class PanZoomDrawListener extends React.Component { - static calculateDistance({ x: x1, y: y1 }, { x: x2, y: y2 }) { - return Math.sqrt(((x1 - x2) ** 2) + ((y1 - y2) ** 2)); - } - - constructor(props) { - super(props); - this.mouseDownHandler = this.mouseDownHandler.bind(this); - this.mouseMoveHandler = this.mouseMoveHandler.bind(this); - this.mouseUpHandler = this.mouseUpHandler.bind(this); - - this.touchMoveHandler = this.touchMoveHandler.bind(this); - this.touchStartHandler = this.touchStartHandler.bind(this); - this.touchEndHandler = this.touchEndHandler.bind(this); - - this.pinchStartHandler = this.pinchStartHandler.bind(this); - this.pinchMoveHandler = this.pinchMoveHandler.bind(this); - this.pinchEndHandler = this.pinchEndHandler.bind(this); - - this.panMoveHandler = this.panMoveHandler.bind(this); - this.updateTouchPoint = this.updateTouchPoint.bind(this); - this.currentMouseX = 0; - this.currentMouseY = 0; - this.mouseDeltaX = 0; - this.mouseDeltaY = 0; - this.initialTouches = []; - this.state = { - pressed: false, - }; - this.pinchGesture = false; - this.prevDiff = -1; - } - - - mouseDownHandler(event) { - const isLeftClick = event.button === 0; - if (isLeftClick) { - this.setState({ - pressed: true, - }); - } - } - - updateTouchPoint(event) { - if (event.touches.length <= 0) return; - const { clientX, clientY } = event.touches[0]; - this.currentMouseX = clientX; - this.currentMouseY = clientY; - } - - mouseMoveHandler(event) { - if (this.state.pressed) { - this.mouseDeltaX = this.currentMouseX - event.clientX; - this.mouseDeltaY = this.currentMouseY - event.clientY; - this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY); - } - this.currentMouseX = event.clientX; - this.currentMouseY = event.clientY; - } - - touchStartHandler(event) { - this.updateTouchPoint(event); - const numberTouches = event.touches.length; - if (numberTouches === 2) { - this.pinchGesture = true; - this.pinchStartHandler(event); - } - if (numberTouches === 1) { - this.pinchGesture = false; - this.updateTouchPoint(event); - } - } - - touchMoveHandler(event) { - if (this.pinchGesture) { - this.pinchMoveHandler(event); - } - if (!this.pinchGesture) { - this.panMoveHandler(event); - } - } - - touchEndHandler(event) { - if (this.pinchGesture) { - this.pinchEndHandler(event); - this.pinchGesture = false; - } else if (!this.pinchGesture) { - this.updateTouchPoint(event); - } - } - - panMoveHandler(event) { - if (this.pinchGesture) return; - const { clientX, clientY } = event.changedTouches[0]; - this.mouseDeltaX = this.currentMouseX - clientX; - this.mouseDeltaY = this.currentMouseY - clientY; - this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY); - this.currentMouseX = clientX; - this.currentMouseY = clientY; - } - - - pinchStartHandler(event) { - if (!this.pinchGesture) return; - this.initialTouches = [...event.touches]; - let inputs = []; - this.initialTouches.forEach((ev) => { - inputs.push({ - x: ev.clientX, - y: ev.clientY, - }); - }); - this.prevDiff = PanZoomDrawListener.calculateDistance(...inputs); - inputs = []; - this.updateTouchPoint(event); - this.props.touchUpdate(true); - } - - pinchMoveHandler(event) { - if (!this.pinchGesture) return; - if (event.touches.length < 2) return; - let inputs = []; - [...event.touches].forEach((ev) => { - inputs.push({ - x: ev.clientX, - y: ev.clientY, - }); - }); - const currDiff = PanZoomDrawListener.calculateDistance(...inputs); - inputs = []; - if (currDiff > 0) { - if (currDiff > this.prevDiff) { - this.props.zoomChanger(this.props.zoom + (100 * ((currDiff - this.prevDiff) / 100))); - } - if (currDiff < this.prevDiff) { - this.props.zoomChanger(this.props.zoom - (100 * ((this.prevDiff - currDiff) / 100))); - } - } - this.prevDiff = currDiff; - } - - pinchEndHandler(event) { - this.initialTouches = []; - this.updateTouchPoint(event); - this.pinchGesture = false; - this.props.touchUpdate(false); - } - - mouseUpHandler(event) { - const isLeftClick = event.button === 0; - - if (isLeftClick) { - this.setState({ - pressed: false, - }); - } - } - - render() { - const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename; - const pencilDrawStyle = { - width: '100%', - height: '100%', - touchAction: 'none', - zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping - cursor: this.state.pressed ? `url('${baseName}/resources/images/whiteboard-cursor/pan-closed.png') 4 8 , default` - : `url('${baseName}/resources/images/whiteboard-cursor/pan.png') 4 8, default`, - }; - return (<div - style={pencilDrawStyle} - onMouseDown={this.mouseDownHandler} - onMouseMove={this.mouseMoveHandler} - onMouseUp={this.mouseUpHandler} - onMouseLeave={this.mouseUpHandler} - onTouchStart={this.touchStartHandler} - onTouchMove={this.touchMoveHandler} - onTouchEnd={this.touchEndHandler} - />); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx index 79134436ea..e67460a42e 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx @@ -6,6 +6,9 @@ const DRAW_START = ANNOTATION_CONFIG.status.start; const DRAW_UPDATE = ANNOTATION_CONFIG.status.update; const DRAW_END = ANNOTATION_CONFIG.status.end; +// maximum value of z-index to prevent other things from overlapping +const MAX_Z_INDEX = (2 ** 31) - 1; + export default class PencilDrawListener extends Component { constructor() { super(); @@ -40,14 +43,18 @@ export default class PencilDrawListener extends Component { } commonDrawStartHandler(clientX, clientY) { - // changing isDrawing to true - this.isDrawing = true; + const { + actions, + } = this.props; const { getTransformedSvgPoint, generateNewShapeId, svgCoordinateToPercentages, - } = this.props.actions; + } = actions; + + // changing isDrawing to true + this.isDrawing = true; // sending the first message let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); @@ -62,11 +69,15 @@ export default class PencilDrawListener extends Component { commonDrawMoveHandler(clientX, clientY) { if (this.isDrawing) { + const { + actions, + } = this.props; + const { checkIfOutOfBounds, getTransformedSvgPoint, svgCoordinateToPercentages, - } = this.props.actions; + } = actions; // get the transformed svg coordinate let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); @@ -151,23 +162,41 @@ export default class PencilDrawListener extends Component { sendCoordinates() { if (this.isDrawing && this.points.length > 0) { - const { getCurrentShapeId } = this.props.actions; + const { + actions, + } = this.props; + + const { getCurrentShapeId } = actions; this.handleDrawPencil(this.points, DRAW_UPDATE, getCurrentShapeId()); this.points = []; } } handleDrawPencil(points, status, id, dimensions) { - const { normalizeThickness, sendAnnotation } = this.props.actions; - const { whiteboardId, userId } = this.props; + const { + whiteboardId, + userId, + actions, + drawSettings, + } = this.props; + + const { + normalizeThickness, + sendAnnotation, + } = actions; + + const { + thickness, + color, + } = drawSettings; const annotation = { id, status, annotationType: 'pencil', annotationInfo: { - color: this.props.drawSettings.color, - thickness: normalizeThickness(this.props.drawSettings.thickness), + color, + thickness: normalizeThickness(thickness), points, id, whiteboardId, @@ -179,7 +208,7 @@ export default class PencilDrawListener extends Component { position: 0, }; - // dimensions are added to the 'DRAW_END', last message + // dimensions are added to the 'DRAW_END', last message if (dimensions) { annotation.annotationInfo.dimensions = dimensions; } @@ -189,8 +218,13 @@ export default class PencilDrawListener extends Component { sendLastMessage() { if (this.isDrawing) { - const { getCurrentShapeId } = this.props.actions; - const { physicalSlideWidth, physicalSlideHeight } = this.props; + const { + physicalSlideWidth, + physicalSlideHeight, + actions, + } = this.props; + + const { getCurrentShapeId } = actions; this.handleDrawPencil( this.points, @@ -216,23 +250,38 @@ export default class PencilDrawListener extends Component { } discardAnnotation() { - const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; - const { whiteboardId } = this.props; + const { + whiteboardId, + actions, + } = this.props; + + const { + getCurrentShapeId, + addAnnotationToDiscardedList, + undoAnnotation, + } = actions; + undoAnnotation(whiteboardId); addAnnotationToDiscardedList(getCurrentShapeId()); } render() { + const { + actions, + } = this.props; + + const { contextMenuHandler } = actions; + const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename; const pencilDrawStyle = { width: '100%', height: '100%', touchAction: 'none', - zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping + zIndex: MAX_Z_INDEX, cursor: `url('${baseName}/resources/images/whiteboard-cursor/pencil.png') 2 22, default`, }; - const { contextMenuHandler } = this.props.actions; + return ( <div onTouchStart={this.handleTouchStart} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js index 423b8965ea..95a0eb272a 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js @@ -1,6 +1,7 @@ import Storage from '/imports/ui/services/storage/session'; import Auth from '/imports/ui/services/auth'; import { sendAnnotation, addAnnotationToDiscardedList } from '/imports/ui/components/whiteboard/service'; +import { publishCursorUpdate } from '/imports/ui/components/cursor/service'; const DRAW_SETTINGS = 'drawSettings'; @@ -49,6 +50,10 @@ const getCurrentUserId = () => Auth.userID; const contextMenuHandler = event => event.preventDefault(); +const updateCursor = (payload) => { + publishCursorUpdate(payload); +}; + export default { addAnnotationToDiscardedList, sendAnnotation, @@ -57,4 +62,5 @@ export default { resetTextShapeSession, getCurrentUserId, contextMenuHandler, + updateCursor, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx index 9c7136151c..554c2469a8 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx @@ -6,6 +6,9 @@ const DRAW_START = ANNOTATION_CONFIG.status.start; const DRAW_UPDATE = ANNOTATION_CONFIG.status.update; const DRAW_END = ANNOTATION_CONFIG.status.end; +// maximum value of z-index to prevent other things from overlapping +const MAX_Z_INDEX = (2 ** 31) - 1; + export default class ShapeDrawListener extends Component { constructor(props) { super(props); @@ -56,11 +59,15 @@ export default class ShapeDrawListener extends Component { commonDrawStartHandler(clientX, clientY) { this.isDrawing = true; + const { + actions, + } = this.props; + const { getTransformedSvgPoint, generateNewShapeId, svgCoordinateToPercentages, - } = this.props.actions; + } = actions; // sending the first message let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); @@ -94,11 +101,15 @@ export default class ShapeDrawListener extends Component { return; } + const { + actions, + } = this.props; + const { checkIfOutOfBounds, getTransformedSvgPoint, svgCoordinateToPercentages, - } = this.props.actions; + } = actions; // get the transformed svg coordinate let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); @@ -181,30 +192,35 @@ export default class ShapeDrawListener extends Component { } sendCoordinates() { + const { + actions, + drawSettings, + } = this.props; + // check the current drawing status if (!this.isDrawing) { return; } // check if a current coordinate is not the same as an initial one // it prevents us from drawing dots on random clicks - if (this.currentCoordinate.x === this.initialCoordinate.x && - this.currentCoordinate.y === this.initialCoordinate.y) { + if (this.currentCoordinate.x === this.initialCoordinate.x + && this.currentCoordinate.y === this.initialCoordinate.y) { return; } // check if previously sent coordinate is not equal to a current one - if (this.currentCoordinate.x === this.lastSentCoordinate.x && - this.currentCoordinate.y === this.lastSentCoordinate.y) { + if (this.currentCoordinate.x === this.lastSentCoordinate.x + && this.currentCoordinate.y === this.lastSentCoordinate.y) { return; } - const { getCurrentShapeId } = this.props.actions; + const { getCurrentShapeId } = actions; this.handleDrawCommonAnnotation( this.initialCoordinate, this.currentCoordinate, this.currentStatus, getCurrentShapeId(), - this.props.drawSettings.tool, + drawSettings.tool, ); this.lastSentCoordinate = this.currentCoordinate; @@ -214,17 +230,22 @@ export default class ShapeDrawListener extends Component { } sendLastMessage() { + const { + actions, + drawSettings, + } = this.props; + if (this.isDrawing) { // make sure we are drawing and we have some coordinates sent for this shape before // to prevent sending DRAW_END on a random mouse click if (this.lastSentCoordinate.x && this.lastSentCoordinate.y) { - const { getCurrentShapeId } = this.props.actions; + const { getCurrentShapeId } = actions; this.handleDrawCommonAnnotation( this.initialCoordinate, this.currentCoordinate, DRAW_END, getCurrentShapeId(), - this.props.drawSettings.tool, + drawSettings.tool, ); } this.resetState(); @@ -258,9 +279,22 @@ export default class ShapeDrawListener extends Component { // since Rectangle / Triangle / Ellipse / Line have the same coordinate structure // we use the same function for all of them handleDrawCommonAnnotation(startPoint, endPoint, status, id, shapeType) { - const { normalizeThickness, sendAnnotation } = this.props.actions; - const { whiteboardId, userId } = this.props; - const { color, thickness } = this.props.drawSettings; + const { + whiteboardId, + userId, + actions, + drawSettings, + } = this.props; + + const { + normalizeThickness, + sendAnnotation, + } = actions; + + const { + color, + thickness, + } = drawSettings; const annotation = { id, @@ -289,24 +323,44 @@ export default class ShapeDrawListener extends Component { } discardAnnotation() { - const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; - const { whiteboardId } = this.props; + const { + whiteboardId, + actions, + } = this.props; + + const { + getCurrentShapeId, + addAnnotationToDiscardedList, + undoAnnotation, + } = actions; undoAnnotation(whiteboardId); addAnnotationToDiscardedList(getCurrentShapeId()); } render() { - const { tool } = this.props.drawSettings; + const { + actions, + drawSettings, + } = this.props; + + const { + contextMenuHandler, + } = actions; + + const { + tool, + } = drawSettings; + const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename; const shapeDrawStyle = { width: '100%', height: '100%', touchAction: 'none', - zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping + zIndex: MAX_Z_INDEX, cursor: `url('${baseName}/resources/images/whiteboard-cursor/${tool !== 'rectangle' ? tool : 'square'}.png'), default`, }; - const { contextMenuHandler } = this.props.actions; + return ( <div onTouchStart={this.handleTouchStart} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx index 57a279d264..52e84ead7e 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx @@ -6,6 +6,9 @@ const DRAW_START = ANNOTATION_CONFIG.status.start; const DRAW_UPDATE = ANNOTATION_CONFIG.status.update; const DRAW_END = ANNOTATION_CONFIG.status.end; +// maximum value of z-index to prevent other things from overlapping +const MAX_Z_INDEX = (2 ** 31) - 1; + export default class TextDrawListener extends Component { constructor() { super(); @@ -74,7 +77,11 @@ export default class TextDrawListener extends Component { } componentDidUpdate(prevProps) { - const { drawSettings } = this.props; + const { + drawSettings, + actions, + } = this.props; + const prevDrawsettings = prevProps.drawSettings; const prevTextShapeValue = prevProps.drawSettings.textShapeValue; @@ -87,7 +94,7 @@ export default class TextDrawListener extends Component { const textShapeIdNotEmpty = drawSettings.textShapeActiveId !== ''; if ((fontSizeChanged || colorChanged || textShapeValueChanged) && textShapeIdNotEmpty) { - const { getCurrentShapeId } = this.props.actions; + const { getCurrentShapeId } = actions; this.currentStatus = DRAW_UPDATE; this.handleDrawText( @@ -113,8 +120,11 @@ export default class TextDrawListener extends Component { // returns true if textarea was focused // currently used only with iOS devices checkTextAreaFocus() { - const { getCurrentShapeId } = this.props.actions; - const textarea = document.getElementById(getCurrentShapeId()); + const { + actions, + } = this.props; + + const textarea = document.getElementById(actions.getCurrentShapeId()); if (document.activeElement === textarea) { return true; @@ -124,6 +134,11 @@ export default class TextDrawListener extends Component { } handleTouchStart(event) { + const { + isDrawing, + isWritingText, + } = this.state; + this.hasBeenTouchedRecently = true; setTimeout(() => { this.hasBeenTouchedRecently = false; }, 500); // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box @@ -131,7 +146,7 @@ export default class TextDrawListener extends Component { // if our current drawing state is not drawing the box and not writing the text - if (!this.state.isDrawing && !this.state.isWritingText) { + if (!isDrawing && !isWritingText) { window.addEventListener('touchend', this.handleTouchEnd, { passive: false }); window.addEventListener('touchmove', this.handleTouchMove, { passive: false }); window.addEventListener('touchcancel', this.handleTouchCancel, true); @@ -141,7 +156,7 @@ export default class TextDrawListener extends Component { // this case is specifically for iOS, since text shape is working in 3 steps there: // touch to draw a box -> tap to focus -> tap to publish - } else if (!this.state.isDrawing && this.state.isWritingText && !this.checkTextAreaFocus()) { + } else if (!isDrawing && isWritingText && !this.checkTextAreaFocus()) { // if you switch to a different window using Alt+Tab while mouse is down and release it // it wont catch mouseUp and will keep tracking the movements. Thus we need this check. @@ -172,6 +187,11 @@ export default class TextDrawListener extends Component { // main mouse down handler handleMouseDown(event) { + const { + isDrawing, + isWritingText, + } = this.state; + const isLeftClick = event.button === 0; const isRightClick = event.button === 2; @@ -180,7 +200,7 @@ export default class TextDrawListener extends Component { } // if our current drawing state is not drawing the box and not writing the text - if (!this.state.isDrawing && !this.state.isWritingText) { + if (!isDrawing && !isWritingText) { if (isLeftClick) { window.addEventListener('mouseup', this.handleMouseUp); window.addEventListener('mousemove', this.handleMouseMove, true); @@ -213,9 +233,11 @@ export default class TextDrawListener extends Component { } commonDrawStartHandler(clientX, clientY) { - const { getTransformedSvgPoint } = this.props.actions; + const { + actions, + } = this.props; - const transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); + const transformedSvgPoint = actions.getTransformedSvgPoint(clientX, clientY); // saving initial X and Y coordinates for further displaying of the textarea this.initialX = transformedSvgPoint.x; @@ -229,11 +251,19 @@ export default class TextDrawListener extends Component { } sendLastMessage() { - if (!this.state.isWritingText) { + const { + drawSettings, + actions, + } = this.props; + + const { + isWritingText, + } = this.state; + + if (!isWritingText) { return; } - const { getCurrentShapeId } = this.props.actions; this.currentStatus = DRAW_END; this.handleDrawText( @@ -241,14 +271,17 @@ export default class TextDrawListener extends Component { this.currentWidth, this.currentHeight, this.currentStatus, - getCurrentShapeId(), - this.props.drawSettings.textShapeValue, + actions.getCurrentShapeId(), + drawSettings.textShapeValue, ); this.resetState(); } resetState() { + const { + actions, + } = this.props; // resetting the current drawing state window.removeEventListener('mouseup', this.handleMouseUp); window.removeEventListener('mousemove', this.handleMouseMove, true); @@ -258,7 +291,7 @@ export default class TextDrawListener extends Component { window.removeEventListener('touchcancel', this.handleTouchCancel, true); // resetting the text shape session values - this.props.actions.resetTextShapeSession(); + actions.resetTextShapeSession(); // resetting the current state this.currentX = undefined; this.currentY = undefined; @@ -279,23 +312,25 @@ export default class TextDrawListener extends Component { } commonDrawMoveHandler(clientX, clientY) { - const { checkIfOutOfBounds, getTransformedSvgPoint } = this.props.actions; + const { + actions, + } = this.props; // get the transformed svg coordinate - let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY); + let transformedSvgPoint = actions.getTransformedSvgPoint(clientX, clientY); // check if it's out of bounds - transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint); + transformedSvgPoint = actions.checkIfOutOfBounds(transformedSvgPoint); // check if we need to use initial or new coordinates for the top left corner of the rectangle const x = transformedSvgPoint.x < this.initialX ? transformedSvgPoint.x : this.initialX; const y = transformedSvgPoint.y < this.initialY ? transformedSvgPoint.y : this.initialY; // calculating the width and height of the displayed text box - const width = transformedSvgPoint.x > this.initialX ? - transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x; - const height = transformedSvgPoint.y > this.initialY ? - transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y; + const width = transformedSvgPoint.x > this.initialX + ? transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x; + const height = transformedSvgPoint.y > this.initialY + ? transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y; this.setState({ textBoxWidth: width, @@ -307,8 +342,23 @@ export default class TextDrawListener extends Component { commonDrawEndHandler() { + const { + actions, + slideWidth, + slideHeight, + } = this.props; + + const { + isDrawing, + isWritingText, + textBoxX, + textBoxY, + textBoxWidth, + textBoxHeight, + } = this.state; + // TODO - find if the size is large enough to display the text area - if (!this.state.isDrawing && this.state.isWritingText) { + if (!isDrawing && isWritingText) { return; } @@ -316,14 +366,14 @@ export default class TextDrawListener extends Component { generateNewShapeId, getCurrentShapeId, setTextShapeActiveId, - } = this.props.actions; + } = actions; // coordinates and width/height of the textarea in percentages of the current slide // saving them in the class since they will be used during all updates - this.currentX = (this.state.textBoxX / this.props.slideWidth) * 100; - this.currentY = (this.state.textBoxY / this.props.slideHeight) * 100; - this.currentWidth = (this.state.textBoxWidth / this.props.slideWidth) * 100; - this.currentHeight = (this.state.textBoxHeight / this.props.slideHeight) * 100; + this.currentX = (textBoxX / slideWidth) * 100; + this.currentY = (textBoxY / slideHeight) * 100; + this.currentWidth = (textBoxWidth / slideWidth) * 100; + this.currentHeight = (textBoxHeight / slideHeight) * 100; this.currentStatus = DRAW_START; this.handleDrawText( { x: this.currentX, y: this.currentY }, @@ -347,9 +397,22 @@ export default class TextDrawListener extends Component { } handleDrawText(startPoint, width, height, status, id, text) { - const { normalizeFont, sendAnnotation } = this.props.actions; - const { whiteboardId, userId } = this.props; - const { color, textFontSize } = this.props.drawSettings; + const { + whiteboardId, + userId, + actions, + drawSettings, + } = this.props; + + const { + normalizeFont, + sendAnnotation, + } = actions; + + const { + color, + textFontSize, + } = drawSettings; const annotation = { id, @@ -366,7 +429,7 @@ export default class TextDrawListener extends Component { id, whiteboardId, status, - fontSize: this.props.drawSettings.textFontSize, + fontSize: textFontSize, dataPoints: `${startPoint.x},${startPoint.y}`, type: 'text', }, @@ -379,23 +442,46 @@ export default class TextDrawListener extends Component { } discardAnnotation() { - const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; - const { whiteboardId } = this.props; + const { + whiteboardId, + actions, + } = this.props; + + const { + getCurrentShapeId, + addAnnotationToDiscardedList, + undoAnnotation, + } = actions; undoAnnotation(whiteboardId); addAnnotationToDiscardedList(getCurrentShapeId()); } render() { + const { + actions, + } = this.props; + + const { + textBoxX, + textBoxY, + textBoxWidth, + textBoxHeight, + isWritingText, + isDrawing, + } = this.state; + + const { contextMenuHandler } = actions; + const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename; const textDrawStyle = { width: '100%', height: '100%', touchAction: 'none', - zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping + zIndex: MAX_Z_INDEX, cursor: `url('${baseName}/resources/images/whiteboard-cursor/text.png'), default`, }; - const { contextMenuHandler } = this.props.actions; + return ( <div role="presentation" @@ -404,25 +490,29 @@ export default class TextDrawListener extends Component { onTouchStart={this.handleTouchStart} onContextMenu={contextMenuHandler} > - {this.state.isDrawing ? - <svg - width="100%" - height="100%" - xmlns="http://www.w3.org/2000/svg" - > - {!this.state.isWritingText ? - <rect - x={this.state.textBoxX} - y={this.state.textBoxY} - fill="none" - stroke="black" - strokeWidth="1" - width={this.state.textBoxWidth} - height={this.state.textBoxHeight} - /> - : null } - </svg> - : null } + {isDrawing + ? ( + <svg + width="100%" + height="100%" + xmlns="http://www.w3.org/2000/svg" + > + {!isWritingText + ? ( + <rect + x={textBoxX} + y={textBoxY} + fill="none" + stroke="black" + strokeWidth="1" + width={textBoxWidth} + height={textBoxHeight} + /> + ) + : null } + </svg> + ) + : null } </div> ); } diff --git a/bigbluebutton-html5/imports/utils/slideCalcUtils.js b/bigbluebutton-html5/imports/utils/slideCalcUtils.js old mode 100644 new mode 100755 index 153e55f498..354c6169f6 --- a/bigbluebutton-html5/imports/utils/slideCalcUtils.js +++ b/bigbluebutton-html5/imports/utils/slideCalcUtils.js @@ -42,22 +42,30 @@ export default class SlideCalcUtil { } static calcViewedRegionX(cpx, cpw) { - return (cpx * HUNDRED_PERCENT) / cpw || 0; + const viewX = -cpx / 2 / cpw * 100; + if (viewX > 0) { + return 0; + } + return viewX; } static calcViewedRegionY(cpy, cph) { - return (cpy * HUNDRED_PERCENT) / cph || 0; + const viewY = -cpy / 2 / cph * 100; + if (viewY > 0) { + return 0; + } + return viewY; } static calculateViewportX(vpw, pw) { - if (vpw == pw) { + if (vpw === pw) { return 0; } return (pw - vpw) / MYSTERY_NUM; } static calculateViewportY(vph, ph) { - if (vph == ph) { + if (vph === ph) { return 0; } return (ph - vph) / MYSTERY_NUM; diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 523625b242..8f84d5febd 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -188,6 +188,7 @@ public: echoTestNumber: '9196' presentation: defaultPresentationFile: default.pdf + panZoomThrottle: 200 uploadEndpoint: "/bigbluebutton/presentation/upload" uploadSizeMin: 0 uploadSizeMax: 50000000 -- GitLab