From e9d4f8e42c78a100120759ffc1af8c519883788b Mon Sep 17 00:00:00 2001
From: KDSBrowne <kert.browne85@gmail.com>
Date: Fri, 28 Aug 2020 17:23:27 +0000
Subject: [PATCH] adds a select random user feature for presenter

---
 .../imports/api/meetings/server/methods.js    |   2 +
 .../meetings/server/methods/setRandomUser.js  |   7 +
 .../meetings/server/modifiers/addMeeting.js   |   1 +
 .../server/modifiers/updateRandomUser.js      |  29 ++++
 .../actions-dropdown/component.jsx            |  22 +++
 .../imports/ui/components/app/component.jsx   |   5 +-
 .../imports/ui/components/app/container.jsx   |   8 +-
 .../modal/random-user/component.jsx           | 151 ++++++++++++++++++
 .../modal/random-user/container.jsx           |  63 ++++++++
 .../components/modal/random-user/styles.scss  |  41 +++++
 .../ui/stylesheets/variables/typography.scss  |   1 +
 bigbluebutton-html5/private/locales/en.json   |   7 +
 12 files changed, 333 insertions(+), 4 deletions(-)
 create mode 100644 bigbluebutton-html5/imports/api/meetings/server/methods/setRandomUser.js
 create mode 100644 bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomUser.js
 create mode 100644 bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
 create mode 100644 bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx
 create mode 100644 bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss

diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods.js b/bigbluebutton-html5/imports/api/meetings/server/methods.js
index 960ef8082e..09ac6b1e31 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/methods.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/methods.js
@@ -4,6 +4,7 @@ import toggleRecording from './methods/toggleRecording';
 import transferUser from './methods/transferUser';
 import toggleLockSettings from './methods/toggleLockSettings';
 import toggleWebcamsOnlyForModerator from './methods/toggleWebcamsOnlyForModerator';
+import setRandomUser from './methods/setRandomUser';
 
 Meteor.methods({
   endMeeting,
@@ -11,4 +12,5 @@ Meteor.methods({
   toggleLockSettings,
   transferUser,
   toggleWebcamsOnlyForModerator,
+  setRandomUser,
 });
diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/setRandomUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/setRandomUser.js
new file mode 100644
index 0000000000..f810a61311
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/methods/setRandomUser.js
@@ -0,0 +1,7 @@
+import { extractCredentials } from '/imports/api/common/server/helpers';
+import updateRandomUser from '../modifiers/updateRandomUser';
+
+export default function setRandomUser(userId) {
+  const { meetingId, requesterUserId } = extractCredentials(this.userId);
+  updateRandomUser(meetingId, userId, requesterUserId);
+}
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
index 7f15e961f2..ce20af2d19 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
@@ -145,6 +145,7 @@ export default function addMeeting(meeting) {
       meetingId,
       meetingEnded,
       publishedPoll: false,
+      randomlySelectedUser: '',
     }, flat(newMeeting, {
       safe: true,
     })),
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomUser.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomUser.js
new file mode 100644
index 0000000000..103aba740b
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomUser.js
@@ -0,0 +1,29 @@
+import Meetings from '/imports/api/meetings';
+import Logger from '/imports/startup/server/logger';
+import { check } from 'meteor/check';
+
+export default function updateRandomUser(meetingId, userId, requesterId) {
+  check(meetingId, String);
+  check(userId, String);
+  check(requesterId, String);
+
+  const selector = {
+    meetingId,
+  };
+
+  const modifier = {
+    $set: {
+      randomlySelectedUser: userId,
+    },
+  };
+
+  const cb = (err) => {
+    if (err != null) {
+      return Logger.error(`Setting randomly selected userId=${userId} by requesterId=${requesterId} in meetingId=${meetingId}`);
+    }
+
+    return Logger.info(`Set randomly selected userId=${userId} by requesterId=${requesterId} in meeitingId=${meetingId}`);
+  };
+
+  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 1fe1f16c0d..a42f0dd992 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
@@ -12,6 +12,7 @@ import { withModalMounter } from '/imports/ui/components/modal/service';
 import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
 import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
 import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container';
+import RandomUserSelectContainer from '/imports/ui/components/modal/random-user/container';
 import cx from 'classnames';
 import { styles } from '../styles';
 
@@ -75,6 +76,14 @@ const intlMessages = defineMessages({
     id: 'app.actionsBar.actionsDropdown.stopShareExternalVideo',
     description: 'Stop sharing external video button',
   },
+  selectRandUserLabel: {
+    id: 'app.actionsBar.actionsDropdown.selectRandUserLabel',
+    description: 'Label for selecting a random user',
+  },
+  selectRandUserDesc: {
+    id: 'app.actionsBar.actionsDropdown.selectRandUserDesc',
+    description: 'Description for select random user option',
+  },
 });
 
 const handlePresentationClick = () => Session.set('showUploadPresentationView', true);
@@ -86,6 +95,7 @@ class ActionsDropdown extends PureComponent {
     this.presentationItemId = _.uniqueId('action-item-');
     this.pollId = _.uniqueId('action-item-');
     this.takePresenterId = _.uniqueId('action-item-');
+    this.selectUserRandId = _.uniqueId('action-item-');
 
     this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this);
     this.makePresentationItems = this.makePresentationItems.bind(this);
@@ -108,6 +118,7 @@ class ActionsDropdown extends PureComponent {
       isSharingVideo,
       isPollingEnabled,
       stopExternalVideoShare,
+      mountModal,
     } = this.props;
 
     const {
@@ -177,6 +188,17 @@ class ActionsDropdown extends PureComponent {
           />
         )
         : null),
+      (amIPresenter
+        ? (
+          <DropdownListItem
+            icon="user"
+            label={intl.formatMessage(intlMessages.selectRandUserLabel)}
+            description={intl.formatMessage(intlMessages.selectRandUserDesc)}
+            key={this.selectUserRandId}
+            onClick={() => mountModal(<RandomUserSelectContainer isSelectedUser={false} />)}
+          />
+        )
+        : null),
     ]);
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index 54367a38c5..a8160d6565 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -23,6 +23,7 @@ import PingPongContainer from '/imports/ui/components/ping-pong/container';
 import MediaService from '/imports/ui/components/media/service';
 import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
 import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
+import RandomUserSelectContainer from '/imports/ui/components/modal/random-user/container';
 import { withDraggableContext } from '../media/webcam-draggable-overlay/context';
 import { styles } from './styles';
 import { NAVBAR_HEIGHT } from '/imports/ui/components/layout/layout-manager';
@@ -155,9 +156,11 @@ class App extends Component {
 
   componentDidUpdate(prevProps) {
     const {
-      meetingMuted, notify, currentUserEmoji, intl, hasPublishedPoll,
+      meetingMuted, notify, currentUserEmoji, intl, hasPublishedPoll, randomlySelectedUser, currentUserId, mountModal,
     } = this.props;
 
+    if (randomlySelectedUser === currentUserId) mountModal(<RandomUserSelectContainer isSelectedUser />);
+
     if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) {
       const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` })
       || currentUserEmoji.status;
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 78e2ef6ba6..b8822df787 100755
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -77,10 +77,10 @@ const currentUserEmoji = currentUser => (currentUser ? {
 });
 
 export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
-  const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1 } });
+  const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1, userId: 1 } });
   const currentMeeting = Meetings.findOne({ meetingId: Auth.meetingID },
-    { fields: { publishedPoll: 1, voiceProp: 1 } });
-  const { publishedPoll, voiceProp } = currentMeeting;
+    { fields: { publishedPoll: 1, voiceProp: 1, randomlySelectedUser: 1 } });
+  const { publishedPoll, voiceProp, randomlySelectedUser } = currentMeeting;
 
   if (!currentUser.approved) {
     baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
@@ -119,6 +119,8 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
     hasPublishedPoll: publishedPoll,
     startBandwidthMonitoring,
     handleNetworkConnection: () => updateNavigatorConnection(navigator.connection),
+    randomlySelectedUser,
+    currentUserId: currentUser.userId,
   };
 })(AppContainer)));
 
diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
new file mode 100644
index 0000000000..5b7242934f
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
@@ -0,0 +1,151 @@
+import React, { Component } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import Modal from '/imports/ui/components/modal/simple/component';
+import Button from '/imports/ui/components/button/component';
+import { styles } from './styles';
+
+const messages = defineMessages({
+  noViewers: {
+    id: 'app.modal.randomUser.noViewers.description',
+    description: 'Label displayed when no viewers are avaiable',
+  },
+  selected: {
+    id: 'app.modal.randomUser.selected.description',
+    description: 'Label shown to the selected user',
+  },
+  randUserTitle: {
+    id: 'app.modal.randomUser.title',
+    description: 'Modal title label',
+  },
+  reselect: {
+    id: 'app.modal.randomUser.reselect.label',
+    description: 'select new random user button label',
+  },
+  ariaModalTitle: {
+    id: 'app.modal.randomUser.ariaLabel.title',
+    description: 'modal title displayed to screen reader',
+  },
+});
+
+const propTypes = {
+  intl: PropTypes.shape({
+    formatMessage: PropTypes.func.isRequired,
+  }).isRequired,
+  mountModal: PropTypes.func.isRequired,
+  numAvailableViewers: PropTypes.number.isRequired,
+  setRandomUser: PropTypes.func.isRequired,
+  isSelectedUser: PropTypes.bool.isRequired,
+  getActiveRandomUser: PropTypes.func.isRequired,
+  getRandomUser: PropTypes.func.isRequired,
+};
+
+class RandomUserSelect extends Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      selectedUser: props.getRandomUser(),
+    };
+
+    this.findNewUser = this.findNewUser.bind(this);
+  }
+
+  componentDidMount() {
+    const { setRandomUser, isSelectedUser } = this.props;
+    const { selectedUser } = this.state;
+    if (!isSelectedUser && selectedUser) setRandomUser(selectedUser.userId);
+  }
+
+  findNewUser() {
+    const { selectedUser } = this.state;
+    const { getRandomUser, numAvailableViewers } = this.props;
+    const user = getRandomUser();
+    if (user.userId === selectedUser.userId && numAvailableViewers > 1) {
+      return this.findNewUser();
+    }
+    return user;
+  }
+
+  render() {
+    const {
+      intl,
+      mountModal,
+      numAvailableViewers,
+      setRandomUser,
+      isSelectedUser,
+      getActiveRandomUser,
+    } = this.props;
+
+    const { selectedUser } = this.state;
+
+    let viewElement = null;
+    let userData = null;
+    let avatarColor = selectedUser ? selectedUser.color : null;
+    let userName = selectedUser ? selectedUser.name : null;
+
+    if (isSelectedUser) {
+      userData = getActiveRandomUser();
+      avatarColor = userData.color;
+      userName = userData.name;
+    }
+
+    viewElement = numAvailableViewers < 1 ? (
+      <div className={styles.modalViewContainer}>
+        <div className={styles.modalViewTitle}>
+          {intl.formatMessage(messages.randUserTitle)}
+        </div>
+        <div>{intl.formatMessage(messages.noViewers)}</div>
+      </div>
+    ) : (
+      <div className={styles.modalViewContainer}>
+        <div className={styles.modalViewTitle}>
+          {isSelectedUser
+            ? `${intl.formatMessage(messages.selected)}`
+            : `${intl.formatMessage(messages.randUserTitle)}`
+          }
+        </div>
+        <div aria-hidden className={styles.modalAvatar} style={{ backgroundColor: `${avatarColor}` }}>
+          {userName.slice(0, 2)}
+        </div>
+        <div className={styles.selectedUserName}>
+          {userName}
+        </div>
+        {!isSelectedUser
+          && (
+          <Button
+            label={intl.formatMessage(messages.reselect)}
+            color="primary"
+            size="md"
+            className={styles.selectBtn}
+            onClick={() => {
+              this.setState({
+                selectedUser: this.findNewUser(),
+              }, () => {
+                const { selectedUser: updatedUser } = this.state;
+                return setRandomUser(updatedUser.userId);
+              });
+            }}
+          />
+          )
+        }
+      </div>
+    );
+
+    return (
+      <Modal
+        hideBorder
+        onRequestClose={() => {
+          if (isSelectedUser) setRandomUser('');
+          mountModal(null);
+        }}
+        contentLabel={intl.formatMessage(messages.ariaModalTitle)}
+      >
+        {viewElement}
+      </Modal>
+    );
+  }
+}
+
+RandomUserSelect.propTypes = propTypes;
+export default injectIntl(RandomUserSelect);
diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx b/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx
new file mode 100644
index 0000000000..c474a39508
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import Meetings from '/imports/api/meetings';
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import { makeCall } from '/imports/ui/services/api';
+import RandomUserSelect from './component';
+
+const RandomUserSelectContainer = props => <RandomUserSelect {...props} />;
+
+export default withModalMounter(withTracker(({ mountModal, isSelectedUser }) => {
+  const randomUserPool = Users.find({
+    meetingId: Auth.meetingID,
+    presenter: { $ne: true },
+    connectionStatus: 'online',
+    role: { $eq: 'VIEWER' },
+  }, {
+    fields: {
+      userId: 1,
+      avatar: 1,
+      color: 1,
+      name: 1,
+    },
+  }).fetch();
+
+  const getRandomUser = () => {
+    const { length } = randomUserPool;
+    const randomIndex = Math.floor(Math.random() * Math.floor(length));
+    return length > 0 ? randomUserPool[randomIndex] : null;
+  };
+
+  const getActiveRandomUser = () => {
+    const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, {
+      fields: {
+        randomlySelectedUser: 1,
+      },
+    });
+
+    return Users.findOne({
+      meetingId: Auth.meetingID,
+      userId: meeting.randomlySelectedUser,
+    }, {
+      fields: {
+        userId: 1,
+        avatar: 1,
+        color: 1,
+        name: 1,
+      },
+    });
+  };
+
+  const setRandomUser = userId => makeCall('setRandomUser', userId);
+
+  return ({
+    closeModal: () => mountModal(null),
+    getRandomUser,
+    numAvailableViewers: randomUserPool.length,
+    setRandomUser,
+    getActiveRandomUser,
+    isSelectedUser,
+  });
+})(RandomUserSelectContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss
new file mode 100644
index 0000000000..a6d7b1f843
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss
@@ -0,0 +1,41 @@
+.modalViewContainer {
+    display: flex;
+    flex-flow: column;
+    align-items: center;
+}
+
+.modalViewTitle {
+    font-weight: 600;
+    font-size: var(--font-size-large);
+    margin-bottom: var(--md-padding-x);
+}
+
+.modalAvatar {
+    height: 6rem;
+    width: 6rem;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: white;
+    font-size: var(--font-size-xxl);
+    font-weight: 400;
+    margin-bottom: var(--sm-padding-x);
+    text-transform: capitalize;
+}
+
+.selectedUserName {
+    margin-bottom: var(--md-padding-x);;
+    font-weight: var(--headings-font-weight);
+    font-size: 2rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    position: relative;
+    width: 100%;
+    text-align: center;
+}
+
+.selectBtn {
+    margin-bottom: var(--md-padding-x);
+}
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
index 492a95241e..80fa90bcac 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
@@ -6,6 +6,7 @@
   --font-family-base: var(--font-family-sans-serif);
 
   --font-size-base: 1rem;
+  --font-size-xxl: 2.75rem;
   --font-size-xl: 1.75rem;
   --font-size-larger: 1.5rem;
   --font-size-large: 1.25rem;
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index f73dbe331e..963036cbb5 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -367,6 +367,8 @@
     "app.actionsBar.actionsDropdown.captionsDesc": "Toggles captions pane",
     "app.actionsBar.actionsDropdown.takePresenter": "Take presenter",
     "app.actionsBar.actionsDropdown.takePresenterDesc": "Assign yourself as the new presenter",
+    "app.actionsBar.actionsDropdown.selectRandUserLabel": "Select Random User",
+    "app.actionsBar.actionsDropdown.selectRandUserDesc": "Chooses a user from available viewers at random",
     "app.actionsBar.emojiMenu.statusTriggerLabel": "Set status",
     "app.actionsBar.emojiMenu.awayLabel": "Away",
     "app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
@@ -484,6 +486,11 @@
     "app.modal.confirm": "Done",
     "app.modal.newTab": "(opens new tab)",
     "app.modal.confirm.description": "Saves changes and closes the modal",
+    "app.modal.randomUser.noViewers.description": "No viewers available to randomly select from",
+    "app.modal.randomUser.selected.description": "You have been randomly selected",
+    "app.modal.randomUser.title": "Randomly selected user",
+    "app.modal.randomUser.reselect.label": "Select again",
+    "app.modal.randomUser.ariaLabel.title": "Randomly selected User Modal",
     "app.dropdown.close": "Close",
     "app.error.400": "Bad Request",
     "app.error.401": "Unauthorized",
-- 
GitLab