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