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