From eb5c061e8f0bd3f6f050198d0f0c261ad798cbda Mon Sep 17 00:00:00 2001
From: Bobak Oftadeh <bobakoftadeh@outlook.com>
Date: Thu, 3 Jan 2019 18:14:35 +0000
Subject: [PATCH] Implemented recording timer

---
 .../api/meetings/server/eventHandlers.js      |  2 +
 .../server/handlers/recordingTimerChange.js   | 23 ++++++++
 .../actions-dropdown/component.jsx            | 26 ---------
 .../ui/components/actions-bar/container.jsx   | 14 -----
 .../ui/components/actions-bar/service.js      | 26 ---------
 .../ui/components/nav-bar/component.jsx       | 57 ++++++++++++++++++-
 .../ui/components/nav-bar/container.jsx       | 13 ++++-
 .../nav-bar/recording-indicator/component.jsx | 22 ++++++-
 .../nav-bar/recording-indicator/styles.scss   | 54 +++++++++++++++---
 .../imports/ui/components/nav-bar/service.js  | 29 ++++++++++
 .../ui/components/recording/component.jsx     | 27 +++++++--
 .../ui/components/recording/container.jsx     |  3 +-
 .../imports/ui/components/toast/container.jsx |  9 +--
 .../private/config/settings.yml               |  2 +-
 14 files changed, 215 insertions(+), 92 deletions(-)
 create mode 100755 bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js
 mode change 100644 => 100755 bigbluebutton-html5/imports/ui/components/toast/container.jsx

diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
index a489f44738..e0fbf321e6 100644
--- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
@@ -6,6 +6,7 @@ import handleMeetingDestruction from './handlers/meetingDestruction';
 import handleMeetingLocksChange from './handlers/meetingLockChange';
 import handleUserLockChange from './handlers/userLockChange';
 import handleRecordingStatusChange from './handlers/recordingStatusChange';
+import handleRecordingTimerChange from './handlers/recordingTimerChange';
 import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator';
 
 RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation);
@@ -15,4 +16,5 @@ RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
 RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
 RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
 RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
+RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
 RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js
new file mode 100755
index 0000000000..28cb2c4ffb
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js
@@ -0,0 +1,23 @@
+import { check } from 'meteor/check';
+import Meetings from '/imports/api/meetings';
+import Logger from '/imports/startup/server/logger';
+
+export default function handleRecordingStatusChange({ body }, meetingId) {
+  const { time } = body;
+
+  const selector = {
+    meetingId,
+  };
+
+  const modifier = {
+    $set: { 'recordProp.time': time },
+  };
+
+  const cb = (err) => {
+    if (err) {
+      Logger.error(`Changing recording time: ${err}`);
+    }
+  };
+
+  return Meetings.upsert(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
index 1e25af4a5e..fa53c39b39 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
@@ -9,12 +9,9 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
 import { withModalMounter } from '/imports/ui/components/modal/service';
-import getFromUserSettings from '/imports/ui/services/users-settings';
 import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
-import RecordingContainer from '/imports/ui/components/recording/container';
 import BreakoutRoom from '../create-breakout-room/component';
 import { styles } from '../styles';
-import ActionBarService from '../service';
 
 const propTypes = {
   isUserPresenter: PropTypes.bool.isRequired,
@@ -91,14 +88,6 @@ class ActionsDropdown extends Component {
     this.createBreakoutRoomId = _.uniqueId('action-item-');
   }
 
-  componentDidMount() {
-    if (Meteor.settings.public.allowOutsideCommands.toggleRecording ||
-      getFromUserSettings('outsideToggleRecording', false)) {
-      ActionBarService.connectRecordingObserver();
-      window.addEventListener('message', ActionBarService.processOutsideToggleRecording);
-    }
-  }
-
   componentWillUpdate(nextProps) {
     const { isUserPresenter: isPresenter } = nextProps;
     const { isUserPresenter: wasPresenter, mountModal } = this.props;
@@ -112,12 +101,8 @@ class ActionsDropdown extends Component {
       intl,
       isUserPresenter,
       isUserModerator,
-      allowStartStopRecording,
-      isRecording,
-      record,
       togglePollMenu,
       meetingIsBreakout,
-      mountModal,
       hasBreakoutRoom,
     } = this.props;
 
@@ -141,17 +126,6 @@ class ActionsDropdown extends Component {
           onClick={this.handlePresentationClick}
         />
         : null),
-      (record && isUserModerator && allowStartStopRecording ?
-        <DropdownListItem
-          icon="record"
-          label={intl.formatMessage(isRecording ?
-            intlMessages.stopRecording : intlMessages.startRecording)}
-          description={intl.formatMessage(isRecording ?
-            intlMessages.stopRecording : intlMessages.startRecording)}
-          key={this.recordId}
-          onClick={() => mountModal(<RecordingContainer />)}
-        />
-        : null),
       (isUserModerator && !meetingIsBreakout && !hasBreakoutRoom ?
         <DropdownListItem
           icon="rooms"
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index da38b1bec2..9b45847331 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -2,8 +2,6 @@ import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
 import { Session } from 'meteor/session';
 import getFromUserSettings from '/imports/ui/services/users-settings';
-import Meetings from '/imports/api/meetings';
-import Auth from '/imports/ui/services/auth';
 import ActionsBar from './component';
 import Service from './service';
 import VideoService from '../video-provider/service';
@@ -29,18 +27,6 @@ export default withTracker(() => {
     return showPoll ? show() : hide();
   };
 
-  Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
-    changed: (id, fields) => {
-      if (fields.recordProp && fields.recordProp.recording) {
-        this.window.parent.postMessage({ response: 'recordingStarted' }, '*');
-      }
-
-      if (fields.recordProp && !fields.recordProp.recording) {
-        this.window.parent.postMessage({ response: 'recordingStopped' }, '*');
-      }
-    },
-  });
-
   return {
     isUserPresenter: Service.isUserPresenter(),
     isUserModerator: Service.isUserModerator(),
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
index 1e673ea031..44d8a4357b 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
@@ -4,32 +4,7 @@ import { makeCall } from '/imports/ui/services/api';
 import Meetings from '/imports/api/meetings';
 import Breakouts from '/imports/api/breakouts';
 
-const processOutsideToggleRecording = (e) => {
-  switch (e.data) {
-    case 'c_record': {
-      makeCall('toggleRecording');
-      break;
-    }
-    case 'c_recording_status': {
-      const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording;
-      const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped';
-      this.window.parent.postMessage({ response: recordingMessage }, '*');
-      break;
-    }
-    default: {
-      // console.log(e.data);
-    }
-  }
-};
-
-const connectRecordingObserver = () => {
-  // notify on load complete
-  this.window.parent.postMessage({ response: 'readyToConnect' }, '*');
-};
-
-
 export default {
-  connectRecordingObserver: () => connectRecordingObserver(),
   isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
   isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
   recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
@@ -37,6 +12,5 @@ export default {
   meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID }).meetingProp.name,
   hasBreakoutRoom: () => Breakouts.find({ parentMeetingId: Auth.meetingID }).fetch().length > 0,
   toggleRecording: () => makeCall('toggleRecording'),
-  processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
   createBreakoutRoom: (numberOfRooms, durationInMinutes, freeJoin = true, record = false) => makeCall('createBreakoutRoom', numberOfRooms, durationInMinutes, freeJoin, record),
 };
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
index d4a16804c4..4853e0f831 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
@@ -11,11 +11,13 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import { withModalMounter } from '/imports/ui/components/modal/service';
 import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
+import getFromUserSettings from '/imports/ui/services/users-settings';
 import { defineMessages, injectIntl } from 'react-intl';
 import { styles } from './styles.scss';
 import Button from '../button/component';
 import RecordingIndicator from './recording-indicator/component';
 import SettingsDropdownContainer from './settings-dropdown/container';
+import ActionBarService from './service';
 
 const intlMessages = defineMessages({
   toggleUserListLabel: {
@@ -42,22 +44,30 @@ const intlMessages = defineMessages({
     id: 'app.navBar.recording.off',
     description: 'label for indicator when the session is not being recorded',
   },
+  startTitle: {
+    id: 'app.recording.startTitle',
+    description: 'start recording title',
+  },
+  stopTitle: {
+    id: 'app.recording.stopTitle',
+    description: 'stop recording title',
+  },
 });
 
 const propTypes = {
   presentationTitle: PropTypes.string,
   hasUnreadMessages: PropTypes.bool,
-  beingRecorded: PropTypes.object,
+  beingRecorded: PropTypes.objectOf(PropTypes.any),
   shortcuts: PropTypes.string,
 };
 
 const defaultProps = {
   presentationTitle: 'Default Room Title',
   hasUnreadMessages: false,
-  beingRecorded: false,
+  beingRecorded: null,
   shortcuts: '',
 };
-
+const interval = null;
 const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) =>
   mountModal(<BreakoutJoinConfirmation
     breakout={breakout}
@@ -74,18 +84,36 @@ class NavBar extends PureComponent {
     this.state = {
       isActionsOpen: false,
       didSendBreakoutInvite: false,
+      time: (props.beingRecorded.time ? props.beingRecorded.time : 0),
     };
 
+    this.incrementTime = this.incrementTime.bind(this);
     this.handleToggleUserList = this.handleToggleUserList.bind(this);
   }
 
+  componentDidMount() {
+    if (Meteor.settings.public.allowOutsideCommands.toggleRecording ||
+      getFromUserSettings('outsideToggleRecording', false)) {
+      ActionBarService.connectRecordingObserver();
+      window.addEventListener('message', ActionBarService.processOutsideToggleRecording);
+    }
+  }
+
   componentDidUpdate(oldProps) {
     const {
       breakouts,
       isBreakoutRoom,
       mountModal,
+      beingRecorded,
     } = this.props;
 
+    if (!beingRecorded.recording) {
+      clearInterval(this.interval);
+      this.interval = null;
+    } else if (this.interval === null) {
+      this.interval = setInterval(this.incrementTime, 1000);
+    }
+
     const hadBreakouts = oldProps.breakouts.length;
     const hasBreakouts = breakouts.length;
 
@@ -108,6 +136,10 @@ class NavBar extends PureComponent {
     }
   }
 
+  componentWillUnmount() {
+    clearInterval(interval);
+  }
+
   handleToggleUserList() {
     this.props.toggleUserList();
   }
@@ -122,6 +154,16 @@ class NavBar extends PureComponent {
     });
   }
 
+  incrementTime() {
+    const { beingRecorded } = this.props;
+
+    if (beingRecorded.time > this.state.time) {
+      this.setState({ time: beingRecorded.time + 1 });
+    } else {
+      this.setState({ time: this.state.time + 1 });
+    }
+  }
+
   renderPresentationTitle() {
     const {
       breakouts,
@@ -178,10 +220,15 @@ class NavBar extends PureComponent {
       isExpanded,
       intl,
       shortcuts: TOGGLE_USERLIST_AK,
+      mountModal,
     } = this.props;
 
     const recordingMessage = beingRecorded.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff';
 
+    if (!this.interval) {
+      this.interval = setInterval(this.incrementTime, 1000);
+    }
+
     const toggleBtnClasses = {};
     toggleBtnClasses[styles.btn] = true;
     toggleBtnClasses[styles.btnWithNotificationDot] = hasUnreadMessages;
@@ -214,6 +261,10 @@ class NavBar extends PureComponent {
           <RecordingIndicator
             {...beingRecorded}
             title={intl.formatMessage(intlMessages[recordingMessage])}
+            buttonTitle={(!beingRecorded.recording ? intl.formatMessage(intlMessages.startTitle) :
+               intl.formatMessage(intlMessages.stopTitle))}
+            mountModal={mountModal}
+            time={this.state.time}
           />
         </div>
         <div className={styles.right}>
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
index 25a03f5159..bd22dfa7f3 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
@@ -25,7 +25,6 @@ export default withTracker(() => {
 
   let meetingTitle;
   let meetingRecorded;
-
   const meetingId = Auth.meetingID;
   const meetingObject = Meetings.findOne({
     meetingId,
@@ -51,6 +50,18 @@ export default withTracker(() => {
       .some(receiverID => ChatService.hasUnreadMessages(receiverID));
   };
 
+  Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
+    changed: (id, fields) => {
+      if (fields.recordProp && fields.recordProp.recording) {
+        this.window.parent.postMessage({ response: 'recordingStarted' }, '*');
+      }
+
+      if (fields.recordProp && !fields.recordProp.recording) {
+        this.window.parent.postMessage({ response: 'recordingStopped' }, '*');
+      }
+    },
+  });
+
   const breakouts = Service.getBreakouts();
   const currentUserId = Auth.userID;
 
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx
index d4f2b2dcd4..b005473115 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx
@@ -1,8 +1,13 @@
 import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import RecordingContainer from '/imports/ui/components/recording/container';
+import humanizeSeconds from '/imports/utils/humanizeSeconds';
 import { styles } from './styles';
+import cx from 'classnames';
 
 const RecordingIndicator = ({
-  record, title, recording,
+  record, title, recording, buttonTitle, mountModal, time,
 }) => {
   if (!record) return null;
 
@@ -11,7 +16,20 @@ const RecordingIndicator = ({
       aria-label={title}
       className={styles.recordState}
     >
-      <div className={recording ? styles.recordIndicator : styles.notRecording} />
+      <div className={styles.border}>
+        <Button
+          label={buttonTitle}
+          hideLabel
+          ghost
+          className={cx(styles.btn, recording ? styles.recordIndicator : styles.notRecording)}
+          onClick={() => {
+            mountModal(<RecordingContainer />);
+          }}
+        />
+      </div>
+      <div className={styles.presentationTitle}>
+        {recording ? humanizeSeconds(time) : <div>{buttonTitle}</div>}
+      </div>
     </div>
   );
 };
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
index 9b90b3c10b..097a52de7c 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
@@ -3,35 +3,73 @@
 %baseIndicator {
   position: relative;
   display: inline-block;
-  width: var(--font-size-base);
-  height: var(--font-size-base);
+  width: var(--font-size-large);
+  height: var(--font-size-large);
   border-radius: 50%;
   border: 1px solid var(--color-white);
 
   &:after {
     content: '';
-    width: calc(var(--font-size-base) / 2);
-    height: calc(var(--font-size-base) / 2);
+    width: calc(var(--font-size-large) / 2);
+    height: calc(var(--font-size-large) / 2);
     position: absolute;
     top: 50%;
     left: 50%;
-    margin-top: calc((var(--font-size-base) / -4));
-    margin-left: calc((var(--font-size-base) / -4));
+    margin-top: calc((var(--font-size-large) / -4));
+    margin-left: calc((var(--font-size-large) / -4));
     border-radius: 50%;
     background-color: var(--color-danger);
   }
 }
 
+.border {
+  border: 1px solid var(--color-white);
+  border-radius: 50%;
+}
+
 .recordIndicator {
   @extend %baseIndicator;
 }
 
+.btn {
+  border-radius: 50%;
+  display: block;
+  padding: 0;
+
+  &:hover,
+  &:focus {
+    background-color: transparent !important;
+    color: var(--color-white) !important;
+    opacity: .75;
+  }
+}
+
+.presentationTitle {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  font-weight: 200;
+  color: var(--color-white);
+  font-size: var(--font-size-base);
+  margin: 0;
+  padding: 0;
+  margin-left: var(--sm-padding-x);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 30vw;
+
+  > [class^="icon-bbb-"] {
+    font-size: 75%;
+  }
+}
+
 .notRecording {
   @extend %baseIndicator;
-  border: 1px solid var(--color-gray);
+  border: 1px solid var(--color-white);
 
   &:after {
-    background-color: var(--color-gray);
+    background-color: var(--color-white);
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js b/bigbluebutton-html5/imports/ui/components/nav-bar/service.js
index 56522370cb..857da9f740 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/service.js
@@ -1,8 +1,37 @@
+import Auth from '/imports/ui/services/auth';
 import Breakouts from '/imports/api/breakouts';
+import { makeCall } from '/imports/ui/services/api';
+import Meetings from '/imports/api/meetings';
 
 const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
 
+const processOutsideToggleRecording = (e) => {
+  switch (e.data) {
+    case 'c_record': {
+      makeCall('toggleRecording');
+      break;
+    }
+    case 'c_recording_status': {
+      const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording;
+      const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped';
+      this.window.parent.postMessage({ response: recordingMessage }, '*');
+      break;
+    }
+    default: {
+      // console.log(e.data);
+    }
+  }
+};
+
+
+const connectRecordingObserver = () => {
+  // notify on load complete
+  this.window.parent.postMessage({ response: 'readyToConnect' }, '*');
+};
+
 export default {
+  connectRecordingObserver: () => connectRecordingObserver(),
+  processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
   getBreakouts,
 };
 
diff --git a/bigbluebutton-html5/imports/ui/components/recording/component.jsx b/bigbluebutton-html5/imports/ui/components/recording/component.jsx
index 49ec90132e..34f408f5e8 100755
--- a/bigbluebutton-html5/imports/ui/components/recording/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/recording/component.jsx
@@ -1,5 +1,6 @@
-import React, { Component } from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import Button from '/imports/ui/components/button/component';
 import Modal from '/imports/ui/components/modal/simple/component';
 import { styles } from './styles';
@@ -31,7 +32,18 @@ const intlMessages = defineMessages({
   },
 });
 
-class RecordingComponent extends Component {
+const propTypes = {
+  intl: intlShape.isRequired,
+  closeModal: PropTypes.func.isRequired,
+  toggleRecording: PropTypes.func.isRequired,
+  recordingStatus: PropTypes.bool,
+};
+
+const defaultProps = {
+  recordingStatus: false,
+};
+
+class RecordingComponent extends React.PureComponent {
   constructor(props) {
     super(props);
     const {
@@ -44,7 +56,7 @@ class RecordingComponent extends Component {
   }
 
   render() {
-    const { intl, meeting } = this.props;
+    const { intl, recordingStatus } = this.props;
 
     return (
       <Modal
@@ -55,12 +67,12 @@ class RecordingComponent extends Component {
       >
         <div className={styles.container}>
           <div className={styles.header}>
-            <div className={styles.title}>{intl.formatMessage(!meeting.recordProp.recording ?
+            <div className={styles.title}>{intl.formatMessage(!recordingStatus ?
             intlMessages.startTitle : intlMessages.stopTitle)}
             </div>
           </div>
           <div className={styles.description}>
-            {`${intl.formatMessage(!meeting.recordProp.recording ? intlMessages.startDescription :
+            {`${intl.formatMessage(!recordingStatus ? intlMessages.startDescription :
             intlMessages.stopDescription)}`}
           </div>
           <div className={styles.footer}>
@@ -82,4 +94,7 @@ class RecordingComponent extends Component {
   }
 }
 
+RecordingComponent.propTypes = propTypes;
+RecordingComponent.defaultProps = defaultProps;
+
 export default injectIntl(RecordingComponent);
diff --git a/bigbluebutton-html5/imports/ui/components/recording/container.jsx b/bigbluebutton-html5/imports/ui/components/recording/container.jsx
index 21f445ca6f..72815e90b4 100755
--- a/bigbluebutton-html5/imports/ui/components/recording/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/recording/container.jsx
@@ -18,5 +18,6 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
     mountModal(null);
   },
 
-  meeting: (Meetings.findOne({ meetingId: Auth.meetingID })),
+  recordingStatus: (Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording),
+
 }))(RecordingContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/toast/container.jsx b/bigbluebutton-html5/imports/ui/components/toast/container.jsx
old mode 100644
new mode 100755
index 9be9f2073c..1e742bb847
--- a/bigbluebutton-html5/imports/ui/components/toast/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/toast/container.jsx
@@ -55,13 +55,14 @@ export default injectIntl(injectNotify(withTracker(({ notify, intl }) => {
 
   const meetingId = Auth.meetingID;
 
-  Meetings.find({ meetingId }).observeChanges({
-    changed: (id, fields) => {
-      if (fields.recordProp && fields.recordProp.recording) {
+  Meetings.find({ meetingId }).observe({
+    changed: (newDocument, oldDocument) => {
+      if (newDocument.recordProp && newDocument.recordProp.recording && 
+        newDocument.recordProp.recording!== oldDocument.recordProp.recording) {
         notify(intl.formatMessage(intlMessages.notificationRecordingStart), 'success', 'record');
       }
 
-      if (fields.recordProp && !fields.recordProp.recording) {
+      if (newDocument.recordProp && !newDocument.recordProp.recording) {
         notify(intl.formatMessage(intlMessages.notificationRecordingStop), 'error', 'record');
       }
     },
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 057222f99f..7ad08ab4d7 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -99,7 +99,7 @@ public:
     enableListenOnly: false
     autoShareWebcam: false
   allowOutsideCommands:
-    toggleRecording: false
+    toggleRecording: true
     toggleSelfVoice: false
   poll:
     max_custom: 5
-- 
GitLab