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 index f3ce23f680d257d6a0c58527fa978281efd2e3e0..195718c1bf4e23a0879085d83f22d097b637c579 100644 --- 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 @@ -15,12 +15,12 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait { def handleSelectRandomViewerReqMsg(msg: SelectRandomViewerReqMsg): Unit = { log.debug("Received SelectRandomViewerReqMsg {}", SelectRandomViewerReqMsg) - def broadcastEvent(msg: SelectRandomViewerReqMsg, selectedUser: UserState): Unit = { + def broadcastEvent(msg: SelectRandomViewerReqMsg, users: Vector[String], choice: Integer): 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 body = SelectRandomViewerRespMsgBody(msg.header.userId, users, choice) val event = SelectRandomViewerRespMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) outGW.send(msgEvent) @@ -34,9 +34,8 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait { val users = Users2x.findNotPresentersNorModerators(liveMeeting.users2x) val randNum = new scala.util.Random - if (users.size > 0) { - broadcastEvent(msg, users(randNum.nextInt(users.size))) - } + val userIds = users.map { case (v) => v.intId } + broadcastEvent(msg, userIds, if (users.size == 0) -1 else randNum.nextInt(users.size)) } } } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala index e163c73be83c9ee806cbbc0ab94c9fcb82744c94..d4c95f00d02666b4543a06b8499fdcc1168de644 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala @@ -408,4 +408,4 @@ case class SelectRandomViewerReqMsgBody(requestedBy: String) */ object SelectRandomViewerRespMsg { val NAME = "SelectRandomViewerRespMsg" } case class SelectRandomViewerRespMsg(header: BbbClientMsgHeader, body: SelectRandomViewerRespMsgBody) extends StandardMsg -case class SelectRandomViewerRespMsgBody(requestedBy: String, selectedUserId: String) +case class SelectRandomViewerRespMsgBody(requestedBy: String, userIds: Vector[String], choice: Integer) diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js index 51c1d76f4a016ec254e9b8b2ed5506f08d204b99..fc3206099a6c48bac6ab78535c509144fcd7c861 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/selectRandomViewer.js @@ -2,12 +2,13 @@ import { check } from 'meteor/check'; import updateRandomViewer from '../modifiers/updateRandomViewer'; export default function randomlySelectedUser({ header, body }) { - const { selectedUserId, requestedBy } = body; + const { userIds, choice, requestedBy } = body; const { meetingId } = header; check(meetingId, String); check(requestedBy, String); - check(selectedUserId, String); + check(userIds, Array); + check(choice, Number); - updateRandomViewer(meetingId, selectedUserId, requestedBy); + updateRandomViewer(meetingId, userIds, choice, requestedBy); } diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js index c7d46f4d75f001bd9c4d5cf9a96c87456ca50cec..08fee9305ca93cecfee4ff0ec0d6fab4f0e27440 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js @@ -15,7 +15,7 @@ export default function clearRandomlySelectedUser() { const modifier = { $set: { - randomlySelectedUser: '', + randomlySelectedUser: [], }, }; diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index 1d8067817cf64f83249af366f1f857c5fdf8d48a..9aaff67b77824cfae294e83c9a370b722b6cc4fb 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -147,7 +147,7 @@ export default function addMeeting(meeting) { meetingEnded, publishedPoll: false, guestLobbyMessage: '', - randomlySelectedUser: '', + 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 index b4a638572722055fda5c12829868b1dcf23f767b..30066e534a9305aa2010e7dc3c1bf9152bca9ca9 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomViewer.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/updateRandomViewer.js @@ -2,27 +2,53 @@ 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) { +export default function updateRandomUser(meetingId, userIds, choice, requesterId) { check(meetingId, String); - check(userId, String); + check(userIds, Array); + check(choice, Number); check(requesterId, String); const selector = { meetingId, }; + const userList = []; + if (choice < 0) { // no viewer + userList.push([requesterId,0]); + } else if (userIds.length == 1) { + userList.push([userIds[0],0]); + } else { + const intervals = [0, 200, 450, 750, 1100, 1500]; + while (intervals.length > 0) { + const userId = userIds[Math.floor(Math.random() * userIds.length )]; + if (userList.length != 0 && userList[userList.length-1][0] == userId) {// prevent same viewer from being selected sequentially + continue; + } + userList.push([userId, intervals.shift()]); + } + userList[userList.length-1][0] = userIds[choice]; // last one should be chosen in akka-app + } + + if (userIds.length == 2) { + // I don't like this.. When the userList is same as previous one, we need to change it a bit to make sure that clients does not skip the change in MongoDB + const previousMeeting = Meetings.findOne(selector, { fields: {randomlySelectedUser:1}}); + if (previousMeeting.randomlySelectedUser.length != 0 && userList[0][0] == previousMeeting.randomlySelectedUser[0][0]) { + userList[0][0] = userList[1][0]; + } + } + const modifier = { $set: { - randomlySelectedUser: userId, + randomlySelectedUser: userList, }, }; try { const { insertedId } = Meetings.upsert(selector, modifier); if (insertedId) { - Logger.info(`Set randomly selected userId=${userId} by requesterId=${requesterId} in meeitingId=${meetingId}`); + Logger.info(`Set randomly selected userId and interval = ${userList} by requesterId=${requesterId} in meeitingId=${meetingId}`); } } catch (err) { - Logger.error(`Setting randomly selected userId=${userId} by requesterId=${requesterId} in meetingId=${meetingId}`); + Logger.error(`Setting randomly selected userId and interval = ${userList} by requesterId=${requesterId} in meetingId=${meetingId}`); } } diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 337909c158621400ace313ce94a793a721f087d3..d9b27db103d004c2a3bcb26d5d93d0220907cf1d 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -179,9 +179,10 @@ class App extends Component { randomlySelectedUser, currentUserId, mountModal, + isPresenter, } = this.props; - if (randomlySelectedUser === currentUserId) mountModal(<RandomUserSelectContainer />); + if (!isPresenter) mountModal(<RandomUserSelectContainer />); if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) { const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` }) diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 5329a67fe801da17c9c5f2d1a2917a956169327f..590ceb48c80ede484750fef48d20fa170ba023ea 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -91,7 +91,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) }, }); - const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1, userId: 1 } }); + const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1, userId: 1, presenter: 1 } }); const currentMeeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { publishedPoll: 1, voiceProp: 1, randomlySelectedUser: 1 } }); const { publishedPoll, voiceProp, randomlySelectedUser } = currentMeeting; @@ -124,6 +124,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) handleNetworkConnection: () => updateNavigatorConnection(navigator.connection), randomlySelectedUser, currentUserId: currentUser.userId, + isPresenter: currentUser.presenter, }; })(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 index 100085a3ff4e336790107fb90eace8525c41292e..c00a3e332b869742745cc3d722a53b97df06e2cb 100644 --- a/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx @@ -3,6 +3,7 @@ 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 AudioService from '/imports/ui/components/audio/service'; import { styles } from './styles'; const messages = defineMessages({ @@ -18,6 +19,14 @@ const messages = defineMessages({ id: 'app.modal.randomUser.title', description: 'Modal title label', }, + whollbeSelected: { + id: 'app.modal.randomUser.who', + description: 'Label shown during the selection', + }, + onlyOneViewerTobeSelected: { + id: 'app.modal.randomUser.alone', + description: 'Label shown when only one viewer to be selected', + }, reselect: { id: 'app.modal.randomUser.reselect.label', description: 'select new random user button label', @@ -44,29 +53,80 @@ class RandomUserSelect extends Component { if (props.currentUser.presenter) { props.randomUserReq(); } + + this.state = { + count: 0, + }; + + this.play = this.play.bind(this); + } + + iterateSelection() { + if (this.props.mappedRandomlySelectedUsers.length > 1){ + let that = this; + setTimeout(delay(that.props.mappedRandomlySelectedUsers, 1), that.props.mappedRandomlySelectedUsers[1][1]); + function delay(arr, num) { + that.setState({ + count: num, + }); + if (num < that.props.mappedRandomlySelectedUsers.length-1){ + setTimeout(function() { delay(arr, num+1); }, arr[num+1][1]); + } + } + } + } + + componentDidMount() { + if (!this.props.currentUser.presenter) { + this.iterateSelection(); + } } componentDidUpdate() { - const { selectedUser, currentUser, mountModal } = this.props; - if (selectedUser && selectedUser.userId !== currentUser.userId && !currentUser.presenter) { - mountModal(null); + if (this.props.currentUser.presenter && this.state.count == 0) { + this.iterateSelection(); } } + play() { + AudioService.playAlertSound(`${Meteor.settings.public.app.cdn + + Meteor.settings.public.app.basename + + Meteor.settings.public.app.instanceId}` + + '/resources/sounds/Poll.mp3'); + } + + + reselect() { + this.setState({ + count: 0, + }); + this.props.randomUserReq(); + } + render() { const { intl, mountModal, numAvailableViewers, randomUserReq, - selectedUser, currentUser, clearRandomlySelectedUser, + mappedRandomlySelectedUsers, } = this.props; + if (mappedRandomlySelectedUsers.length < this.state.count+1) return null; + + const selectedUser = mappedRandomlySelectedUsers[this.state.count][0]; + const countDown = mappedRandomlySelectedUsers.length - this.state.count -1; + + this.play(); + let viewElement; - if (numAvailableViewers < 1) { // there's no viewers to select from + const amISelectedUser = currentUser.userId === selectedUser.userId; + if (numAvailableViewers < 1 || (currentUser.presenter && amISelectedUser)) { // there's no viewers to select from, + //or when you are the presenter but selected, which happens when the presenter ability is passed to somebody + // and people are entering and leaving the meeting // display modal informing presenter that there's no viewers to select from viewElement = ( <div className={styles.modalViewContainer}> @@ -80,13 +140,18 @@ class RandomUserSelect extends Component { if (!selectedUser) return null; // rendering triggered before selectedUser is available // display modal with random user selection - const amISelectedUser = currentUser.userId === selectedUser.userId; viewElement = ( - <div className={styles.modalViewContainer}> + <div className={countDown == 0 + ? styles.modalViewFinalContainer + : styles.modalViewContainer}> <div className={styles.modalViewTitle}> - {amISelectedUser - ? `${intl.formatMessage(messages.selected)}` - : `${intl.formatMessage(messages.randUserTitle)}` + {countDown == 0 + ? amISelectedUser + ? `${intl.formatMessage(messages.selected)}` + : numAvailableViewers == 1 && currentUser.presenter + ? `${intl.formatMessage(messages.onlyOneViewerTobeSelected)}` + : `${intl.formatMessage(messages.randUserTitle)}` + : `${intl.formatMessage(messages.whollbeSelected)} ${countDown}` } </div> <div aria-hidden className={styles.modalAvatar} style={{ backgroundColor: `${selectedUser.color}` }}> @@ -95,14 +160,15 @@ class RandomUserSelect extends Component { <div className={styles.selectedUserName}> {selectedUser.name} </div> - {!amISelectedUser + {currentUser.presenter + && countDown == 0 && ( <Button label={intl.formatMessage(messages.reselect)} color="primary" size="md" className={styles.selectBtn} - onClick={() => randomUserReq()} + onClick={() => this.reselect()} /> ) } diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx b/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx index 0488a4a3996f60d9bfdd6894560c6911ca29d175..546591ac0e6089f0ec96b0cb28957b61cb40213e 100644 --- a/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/container.jsx @@ -28,17 +28,23 @@ export default withModalMounter(withTracker(({ mountModal }) => { }, }); - const selectedUser = Users.findOne({ - meetingId: Auth.meetingID, - userId: meeting.randomlySelectedUser, - }, { - fields: { - userId: 1, - avatar: 1, - color: 1, - name: 1, - }, - }); + let mappedRandomlySelectedUsers = []; + if (meeting.randomlySelectedUser) { + mappedRandomlySelectedUsers = meeting.randomlySelectedUser.map(function(ui) { + const selectedUser = Users.findOne({ + meetingId: Auth.meetingID, + userId: ui[0], + }, { + fields: { + userId: 1, + avatar: 1, + color: 1, + name: 1, + }, + }); + return [selectedUser,ui[1]]; + }); + } const currentUser = Users.findOne( { userId: Auth.userID }, @@ -53,7 +59,7 @@ export default withModalMounter(withTracker(({ mountModal }) => { closeModal: () => mountModal(null), numAvailableViewers: viewerPool.length, randomUserReq, - selectedUser, + mappedRandomlySelectedUsers, currentUser, clearRandomlySelectedUser, }); diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss index a6d7b1f84352bee67ff6f4594ba32f8e1e8497df..aafd9a07779a93ebc1e4051d1f5c109e677cfacb 100644 --- a/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/styles.scss @@ -4,6 +4,13 @@ align-items: center; } +.modalViewFinalContainer { + display: flex; + flex-flow: column; + align-items: center; + background-color: bisque; +} + .modalViewTitle { font-weight: 600; font-size: var(--font-size-large); @@ -39,3 +46,4 @@ .selectBtn { margin-bottom: var(--md-padding-x); } + diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index f295b716b43ce4cc21ae813857e593a08079d7af..c6981279b95dbcf369abfb911cdd95660e2ca674 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -528,6 +528,8 @@ "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.who": "Who will be selected..?", + "app.modal.randomUser.alone": "There is only one viewer", "app.modal.randomUser.reselect.label": "Select again", "app.modal.randomUser.ariaLabel.title": "Randomly selected User Modal", "app.dropdown.close": "Close",