diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SelectRandomViewerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SelectRandomViewerReqMsgHdlr.scala new file mode 100644 index 0000000000000000000000000000000000000000..904d88028bba4b00cae0a44c0b4dc579e4ba78f4 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SelectRandomViewerReqMsgHdlr.scala @@ -0,0 +1,42 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core.models.{ UserState, Users2x } +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core2.MeetingStatus2x +import scala.util.Random + +trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait { + this: UsersApp => + + val outGW: OutMsgRouter + + def handleSelectRandomViewerReqMsg(msg: SelectRandomViewerReqMsg): Unit = { + log.debug("Received SelectRandomViewerReqMsg {}", SelectRandomViewerReqMsg) + + def broadcastEvent(msg: SelectRandomViewerReqMsg, selectedUser: UserState): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId) + val envelope = BbbCoreEnvelope(SelectRandomViewerRespMsg.NAME, routing) + val header = BbbClientMsgHeader(SelectRandomViewerRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) + + val body = SelectRandomViewerRespMsgBody(msg.header.userId, selectedUser.intId) + val event = SelectRandomViewerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to select random user." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) + } else { + val users = Users2x.findViewers(liveMeeting.users2x) + val randNum = new scala.util.Random + + if (users.size > 0) { + broadcastEvent(msg, users(randNum.nextInt(users.size))) + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala index dc13f8c106867dae8fe4e552351a0a0a7118d250..040aa8f766782dde2ef9f1be600f652b353387e8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala @@ -145,6 +145,7 @@ class UsersApp( with SendRecordingTimerInternalMsgHdlr with UpdateWebcamsOnlyForModeratorCmdMsgHdlr with GetRecordingStatusReqMsgHdlr + with SelectRandomViewerReqMsgHdlr with GetWebcamsOnlyForModeratorReqMsgHdlr with AssignPresenterReqMsgHdlr with EjectDuplicateUserReqMsgHdlr diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala index 3e00cca80fe660be7b8fd03bb257915af567a110..826e83cd8cdbb9e7d7eeec3956dbe1f720515e24 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala @@ -59,6 +59,10 @@ object Users2x { users.toVector.filter(u => !u.presenter) } + def findViewers(users: Users2x): Vector[UserState] = { + users.toVector.filter(u => u.role == Roles.VIEWER_ROLE) + } + def updateLastUserActivity(users: Users2x, u: UserState): UserState = { val newUserState = modify(u)(_.lastActivityTime).setTo(TimeUtil.timeNowInMs()) users.save(newUserState) @@ -241,6 +245,7 @@ class Users2x { } } } + } case class OldPresenter(userId: String, changedPresenterOn: Long) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 683f07d0b0aabcd85962600150d2e8d9751bb5d4..5d9de23e6a4b3421d1fc5d8637bc61f73e068616 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -103,6 +103,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[GetPresenterGroupReqMsg](envelope, jsonNode) case UserActivitySignCmdMsg.NAME => routeGenericMsg[UserActivitySignCmdMsg](envelope, jsonNode) + case SelectRandomViewerReqMsg.NAME => + routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode) // Poll case StartCustomPollReqMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index c280dbc2bf83ca23004c76ad2fe5170e9c9ae5f0..b410bd7ad35a1cee64dfcf3de9dab98547ce9213 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -308,6 +308,7 @@ class MeetingActor( case m: UpdateWebcamsOnlyForModeratorCmdMsg => usersApp.handleUpdateWebcamsOnlyForModeratorCmdMsg(m) case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m) case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m) + case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m) // Client requested to eject user case m: EjectUserFromMeetingCmdMsg => diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMgs.scala index 3d62d9dfa43bc15dc61f60809c548acba2d3599d..776ae62ea105a618366e6653e4a564e2314734ad 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMgs.scala @@ -394,3 +394,17 @@ case class UserInactivityInspectMsgBody(meetingId: String, responseDelay: Long) object UserActivitySignCmdMsg { val NAME = "UserActivitySignCmdMsg" } case class UserActivitySignCmdMsg(header: BbbClientMsgHeader, body: UserActivitySignCmdMsgBody) extends StandardMsg case class UserActivitySignCmdMsgBody(userId: String) + +/** + * Sent from client to randomly select a viewer + */ +object SelectRandomViewerReqMsg { val NAME = "SelectRandomViewerReqMsg" } +case class SelectRandomViewerReqMsg(header: BbbClientMsgHeader, body: SelectRandomViewerReqMsgBody) extends StandardMsg +case class SelectRandomViewerReqMsgBody(requestedBy: String) + +/** + * Response to request for a random viewer + */ +object SelectRandomViewerRespMsg { val NAME = "SelectRandomViewerRespMsg" } +case class SelectRandomViewerRespMsg(header: BbbClientMsgHeader, body: SelectRandomViewerRespMsgBody) extends StandardMsg +case class SelectRandomViewerRespMsgBody(requestedBy: String, selectedUserId: String) diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js index 5d08df5ac10c0511ad7d1390f0b02aba7ee24bb7..81f0ef206a42523e373fd5b1f58f316a19b870b6 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js @@ -9,6 +9,7 @@ import handleRecordingStatusChange from './handlers/recordingStatusChange'; import handleRecordingTimerChange from './handlers/recordingTimerChange'; import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate'; import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator'; +import handleSelectRandomViewer from './handlers/selectRandomViewer'; RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation); RedisPubSub.on('SyncGetMeetingInfoRespMsg', handleGetAllMeetings); @@ -21,3 +22,4 @@ RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange); RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate); +RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js new file mode 100644 index 0000000000000000000000000000000000000000..51c1d76f4a016ec254e9b8b2ed5506f08d204b99 --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js @@ -0,0 +1,13 @@ +import { check } from 'meteor/check'; +import updateRandomViewer from '../modifiers/updateRandomViewer'; + +export default function randomlySelectedUser({ header, body }) { + const { selectedUserId, requestedBy } = body; + const { meetingId } = header; + + check(meetingId, String); + check(requestedBy, String); + check(selectedUserId, String); + + updateRandomViewer(meetingId, selectedUserId, requestedBy); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods.js b/bigbluebutton-html5/imports/api/meetings/server/methods.js index 960ef8082efae97b4e628dc50ec470774f70c746..df2c1e468f32a3a3e9196177bc234dc0f2773a61 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 clearRandomlySelectedUser from './methods/clearRandomlySelectedUser'; Meteor.methods({ endMeeting, @@ -11,4 +12,5 @@ Meteor.methods({ toggleLockSettings, transferUser, toggleWebcamsOnlyForModerator, + clearRandomlySelectedUser, }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js new file mode 100644 index 0000000000000000000000000000000000000000..1cd5303db8732836d76864d7ff95f0253e8e148f --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js @@ -0,0 +1,26 @@ +import Logger from '/imports/startup/server/logger'; +import Meetings from '/imports/api/meetings'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function clearRandomlySelectedUser() { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + const selector = { + meetingId, + }; + + const modifier = { + $set: { + randomlySelectedUser: '', + }, + }; + + try { + const { insertedId } = Meetings.update(selector, modifier); + if (insertedId) { + Logger.info(`Cleared randomly selected user from meeting=${meetingId} by id=${requesterUserId}`); + } + } catch (err) { + Logger.error(`Clearing randomly selected user : ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index d4186730315beba3a3fa15e450257b1e279a1ed8..389d816a62a1037b4c7266b7a900e32cdd3b7874 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -146,6 +146,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/updateRandomViewer.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomViewer.js new file mode 100644 index 0000000000000000000000000000000000000000..b4a638572722055fda5c12829868b1dcf23f767b --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomViewer.js @@ -0,0 +1,28 @@ +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, + }, + }; + + try { + const { insertedId } = Meetings.upsert(selector, modifier); + if (insertedId) { + Logger.info(`Set randomly selected userId=${userId} by requesterId=${requesterId} in meeitingId=${meetingId}`); + } + } catch (err) { + Logger.error(`Setting randomly selected userId=${userId} by requesterId=${requesterId} in meetingId=${meetingId}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js index 98aa67982f0e58ce2ce66cfd2c52927127b70d14..9ab6ccb80d4621bf041dbed2c03d154ce3f9dc01 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods.js +++ b/bigbluebutton-html5/imports/api/users/server/methods.js @@ -8,6 +8,7 @@ import toggleUserLock from './methods/toggleUserLock'; import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType'; import userActivitySign from './methods/userActivitySign'; import userLeftMeeting from './methods/userLeftMeeting'; +import setRandomUser from './methods/setRandomUser'; Meteor.methods({ setEmojiStatus, @@ -19,4 +20,5 @@ Meteor.methods({ setUserEffectiveConnectionType, userActivitySign, userLeftMeeting, + setRandomUser, }); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js b/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js new file mode 100644 index 0000000000000000000000000000000000000000..a0da24a993a23e6d69cb11557540569beb9354f7 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js @@ -0,0 +1,17 @@ +import { Meteor } from 'meteor/meteor'; +import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function setRandomUser() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SelectRandomViewerReqMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + const payload = { + requestedBy: requesterUserId, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} 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 de55a9fdf108a57d6290bc36f3079155570c57aa..1a84ba7a5a8f2b4f9db508b1995e0306611496d6 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 168f73397ae644fa061580917a5d627ad8a09e2a..8b7dfcd2a360df44e4aba30b6aecd5b1d4249c47 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -22,6 +22,7 @@ import StatusNotifier from '/imports/ui/components/status-notifier/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'; @@ -154,9 +155,18 @@ 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 />); + 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 3f9dc2986bcef34c38ecad06e9d4f18bd6ed33a7..6e1bbc806b9ddedd0bfff418d82d7adcb449b21f 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -91,10 +91,10 @@ 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)); @@ -122,6 +122,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 0000000000000000000000000000000000000000..62ab5698ec1faccb473a644217c40a7556e2adc4 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx @@ -0,0 +1,122 @@ +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, + randomUserReq: PropTypes.func.isRequired, +}; + +class RandomUserSelect extends Component { + constructor(props) { + super(props); + + if (props.currentUser.presenter) { + props.randomUserReq(); + } + } + + componentDidUpdate() { + const { selectedUser, currentUser, mountModal } = this.props; + if (selectedUser && selectedUser.userId !== currentUser.userId && !currentUser.presenter) { + mountModal(null); + } + } + + render() { + const { + intl, + mountModal, + numAvailableViewers, + randomUserReq, + selectedUser, + currentUser, + clearRandomlySelectedUser, + } = this.props; + + if (!selectedUser) return null; + + const isSelectedUser = currentUser.userId === selectedUser.userId; + + const 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: `${selectedUser.color}` }}> + {selectedUser.name.slice(0, 2)} + </div> + <div className={styles.selectedUserName}> + {selectedUser.name} + </div> + {!isSelectedUser + && ( + <Button + label={intl.formatMessage(messages.reselect)} + color="primary" + size="md" + className={styles.selectBtn} + onClick={() => randomUserReq()} + /> + ) + } + </div> + ); + + return ( + <Modal + hideBorder + onRequestClose={() => { + if (currentUser.presenter) clearRandomlySelectedUser(); + 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 0000000000000000000000000000000000000000..50d9b7a3138c94a1b9384dc174c6bf2c42587c6c --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx @@ -0,0 +1,58 @@ +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 }) => { + const viewerPool = Users.find({ + meetingId: Auth.meetingID, + presenter: { $ne: true }, + role: { $eq: 'VIEWER' }, + }, { + fields: { + userId: 1, + }, + }).fetch(); + + const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { + fields: { + randomlySelectedUser: 1, + }, + }); + + const selectedUser = Users.findOne({ + meetingId: Auth.meetingID, + userId: meeting.randomlySelectedUser, + }, { + fields: { + userId: 1, + avatar: 1, + color: 1, + name: 1, + }, + }); + + const currentUser = Users.findOne( + { userId: Auth.userID }, + { fields: { userId: 1, presenter: 1 } }, + ); + + const randomUserReq = () => makeCall('setRandomUser'); + + const clearRandomlySelectedUser = () => makeCall('clearRandomlySelectedUser'); + + return ({ + closeModal: () => mountModal(null), + numAvailableViewers: viewerPool.length, + randomUserReq, + selectedUser, + currentUser, + clearRandomlySelectedUser, + }); +})(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 0000000000000000000000000000000000000000..a6d7b1f84352bee67ff6f4594ba32f8e1e8497df --- /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 492a95241e8efe68fb598d2e72ba61e68c458f96..80fa90bcacdbbaf36243fff87968dc7575281b52 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 c9448656b5c849d427f5b9785259e3a14aeb8641..9b0493b4ffaa4d925596b6b397f7af8ca7953f5f 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -370,6 +370,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", @@ -487,6 +489,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.dropdown.list.item.activeLabel": "Active", "app.error.400": "Bad Request",