diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js index e13e7c5f2da353bd9835b1067186ad5c617d7c25..041b4ae23b01b0a45a80e917a7381d780e758cf2 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js @@ -4,6 +4,7 @@ import handleGetAllMeetings from './handlers/getAllMeetings'; import handleMeetingEnd from './handlers/meetingEnd'; import handleMeetingDestruction from './handlers/meetingDestruction'; import handleMeetingLocksChange from './handlers/meetingLockChange'; +import handleGuestPolicyChanged from './handlers/guestPolicyChanged'; import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged'; import handleUserLockChange from './handlers/userLockChange'; import handleRecordingStatusChange from './handlers/recordingStatusChange'; @@ -22,6 +23,7 @@ RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange); RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange); +RedisPubSub.on('GuestPolicyChangedEvtMsg', handleGuestPolicyChanged); RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged); RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate); RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js new file mode 100644 index 0000000000000000000000000000000000000000..b7e42aeb9ef03d5a780311a8476ec28c4e1d27e1 --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js @@ -0,0 +1,12 @@ +import setGuestPolicy from '../modifiers/setGuestPolicy'; +import { check } from 'meteor/check'; + +export default function handleGuestPolicyChanged({ body }, meetingId) { + const { policy } = body; + + check(meetingId, String); + check(policy, String); + + + return setGuestPolicy(meetingId, policy); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js new file mode 100644 index 0000000000000000000000000000000000000000..3c1b3a247b964011be1292cb07ca6134a379903a --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js @@ -0,0 +1,34 @@ +import Meetings from '/imports/api/meetings'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; + +export default function setGuestPolicy(meetingId, guestPolicy) { + check(meetingId, String); + check(guestPolicy, String); + + const selector = { + meetingId, + }; + + const modifier = { + $set: { + usersProp: { + guestPolicy, + } + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Changing meeting guest policy: ${err}`); + } + + if (!numChanged) { + return Logger.info(`Meeting's ${meetingId} guest policy=${guestPolicy} wasn't updated`); + } + + return Logger.info(`Meeting's ${meetingId} guest policy=${guestPolicy} updated`); + }; + + return Meetings.update(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index 0cf23728298e01d8780c27784e30353024bf6fc7..b5a6820b88ded6c68eb7e9a1595288830f8a26e4 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -11,6 +11,7 @@ import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import LockViewersContainer from '/imports/ui/components/lock-viewers/container'; import ConnectionStatusContainer from '/imports/ui/components/connection-status/modal/container'; +import GuestPolicyContainer from '/imports/ui/components/waiting-users/guest-policy/container'; import BreakoutRoom from '/imports/ui/components/actions-bar/create-breakout-room/container'; import CaptionsService from '/imports/ui/components/captions/service'; import ConnectionStatusService from '/imports/ui/components/connection-status/service'; @@ -29,6 +30,7 @@ const propTypes = { toggleStatus: PropTypes.func.isRequired, mountModal: PropTypes.func.isRequired, users: PropTypes.arrayOf(Object).isRequired, + guestPolicy: PropTypes.string.isRequired, meetingIsBreakout: PropTypes.bool.isRequired, hasBreakoutRoom: PropTypes.bool.isRequired, isBreakoutEnabled: PropTypes.bool.isRequired, @@ -80,6 +82,14 @@ const intlMessages = defineMessages({ id: 'app.userList.userOptions.connectionStatusDesc', description: 'Connection status description', }, + guestPolicyLabel: { + id: 'app.userList.userOptions.guestPolicyLabel', + description: 'Guest policy label', + }, + guestPolicyDesc: { + id: 'app.userList.userOptions.guestPolicyDesc', + description: 'Guest policy description', + }, muteAllExceptPresenterLabel: { id: 'app.userList.userOptions.muteAllExceptPresenterLabel', description: 'Mute all except presenter label', @@ -139,6 +149,7 @@ class UserOptions extends PureComponent { this.muteAllId = _.uniqueId('list-item-'); this.lockId = _.uniqueId('list-item-'); this.connectionStatusId = _.uniqueId('list-item-'); + this.guestPolicyId = _.uniqueId('list-item-'); this.createBreakoutId = _.uniqueId('list-item-'); this.saveUsersNameId = _.uniqueId('list-item-'); this.captionsId = _.uniqueId('list-item-'); @@ -296,6 +307,15 @@ class UserOptions extends PureComponent { onClick={() => mountModal(<ConnectionStatusContainer />)} />) : null ), + (isMeteorConnected ? ( + <DropdownListItem + key={this.guestPolicyId} + icon="user" + label={intl.formatMessage(intlMessages.guestPolicyLabel)} + description={intl.formatMessage(intlMessages.guestPolicyDesc)} + onClick={() => mountModal(<GuestPolicyContainer />)} + />) : null + ), (isMeteorConnected ? <DropdownListSeparator key={_.uniqueId('list-separator-')} /> : null), (canCreateBreakout && isMeteorConnected ? ( <DropdownListItem diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx index f24b76e177f05099e697b713e0f6a94bad863d2c..77ebcb3d053a1e6482f76ef631775c10cad8f2a2 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx @@ -4,6 +4,7 @@ import Auth from '/imports/ui/services/auth'; import Meetings from '/imports/api/meetings'; import ActionsBarService from '/imports/ui/components/actions-bar/service'; import UserListService from '/imports/ui/components/user-list/service'; +import WaitingUsersService from '/imports/ui/components/waiting-users/service'; import logger from '/imports/startup/client/logger'; import { defineMessages, injectIntl } from 'react-intl'; import { notify } from '/imports/ui/services/notification'; @@ -84,6 +85,7 @@ const UserOptionsContainer = withTracker((props) => { isBreakoutEnabled: ActionsBarService.isBreakoutEnabled(), isBreakoutRecordable: ActionsBarService.isBreakoutRecordable(), users: ActionsBarService.users(), + guestPolicy: WaitingUsersService.getGuestPolicy(), isMeteorConnected: Meteor.status().connected, meetingName: getMeetingName(), }; diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/component.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f98183d5b4b6d23144cbfc93bc9d9eb7c4d3966d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/component.jsx @@ -0,0 +1,115 @@ +import React, { PureComponent } 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 ASK_MODERATOR = 'ASK_MODERATOR'; +const ALWAYS_ACCEPT = 'ALWAYS_ACCEPT'; +const ALWAYS_DENY = 'ALWAYS_DENY'; + +const intlMessages = defineMessages({ + ariaModalTitle: { + id: 'app.guest-policy.ariaTitle', + description: 'Guest policy aria title', + }, + guestPolicyTitle: { + id: 'app.guest-policy.title', + description: 'Guest policy title', + }, + guestPolicyDescription: { + id: 'app.guest-policy.description', + description: 'Guest policy description', + }, + askModerator: { + id: 'app.guest-policy.button.askModerator', + description: 'Ask moderator button label', + }, + alwaysAccept: { + id: 'app.guest-policy.button.alwaysAccept', + description: 'Always accept button label', + }, + alwaysDeny: { + id: 'app.guest-policy.button.alwaysDeny', + description: 'Always deny button label', + }, +}); + +const propTypes = { + closeModal: PropTypes.func.isRequired, + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + guestPolicy: PropTypes.string.isRequired, + changeGuestPolicy: PropTypes.func.isRequired, +}; + +class GuestPolicyComponent extends PureComponent { + render() { + const { + closeModal, + intl, + guestPolicy, + changeGuestPolicy, + } = this.props; + + return ( + <Modal + overlayClassName={styles.overlay} + className={styles.modal} + onRequestClose={closeModal} + hideBorder + contentLabel={intl.formatMessage(intlMessages.ariaModalTitle)} + > + <div className={styles.container}> + <div className={styles.header}> + <h2 className={styles.title}> + {intl.formatMessage(intlMessages.guestPolicyTitle)} + </h2> + </div> + <div className={styles.description}> + {intl.formatMessage(intlMessages.guestPolicyDescription)} + </div> + + <div className={styles.content}> + <Button + color="primary" + className={styles.button} + disabled={guestPolicy === ASK_MODERATOR} + label={intl.formatMessage(intlMessages.askModerator)} + onClick={() => { + changeGuestPolicy(ASK_MODERATOR); + closeModal(); + }} + /> + <Button + color="primary" + className={styles.button} + disabled={guestPolicy === ALWAYS_ACCEPT} + label={intl.formatMessage(intlMessages.alwaysAccept)} + onClick={() => { + changeGuestPolicy(ALWAYS_ACCEPT); + closeModal(); + }} + /> + <Button + color="primary" + className={styles.button} + disabled={guestPolicy === ALWAYS_DENY} + label={intl.formatMessage(intlMessages.alwaysDeny)} + onClick={() => { + changeGuestPolicy(ALWAYS_DENY); + closeModal(); + }} + /> + </div> + </div> + </Modal> + ); + } +} + +GuestPolicyComponent.propTypes = propTypes; + +export default injectIntl(GuestPolicyComponent); diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/container.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/container.jsx new file mode 100644 index 0000000000000000000000000000000000000000..faf4466ca818f73e1dd2bdc6e75930b5721bffb8 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/container.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import { withModalMounter } from '/imports/ui/components/modal/service'; +import GuestPolicyComponent from './component'; +import Service from '../service'; + +const guestPolicyContainer = props => <GuestPolicyComponent {...props} />; + +export default withModalMounter(withTracker(({ mountModal }) => ({ + closeModal: () => mountModal(null), + guestPolicy: Service.getGuestPolicy(), + changeGuestPolicy: Service.changeGuestPolicy, +}))(guestPolicyContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.scss b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..f38425fe1c105c1d2c5c173a24c96bdbd06b5551 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.scss @@ -0,0 +1,68 @@ +@import '/imports/ui/stylesheets/mixins/focus'; +@import '/imports/ui/stylesheets/variables/_all'; +@import "/imports/ui/components/modal/simple/styles"; + +:root { + --modal-margin: 3rem; + --title-position-left: 2.2rem; + --closeBtn-position-left: 2.5rem; +} + +.title { + left: var(--title-position-left); + right: auto; + color: var(--color-gray-dark); + font-weight: bold; + font-size: var(--font-size-large); + text-align: center; + + [dir="rtl"] & { + left: auto; + right: var(--title-position-left); + } +} + +.container { + margin: 0 var(--modal-margin) var(--lg-padding-x); +} + +.modal { + @extend .modal; + padding: var(--jumbo-padding-y); +} + +.overlay { + @extend .overlay; +} + +.description { + text-align: center; + color: var(--color-gray); + margin-bottom: var(--jumbo-padding-y) +} + +.label { + color: var(--color-gray-label); + font-size: var(--font-size-small); + margin-bottom: var(--lg-padding-y); +} + +.header { + margin: 0; + padding: 0; + border: none; + line-height: var(--title-position-left); + margin-bottom: var(--lg-padding-y); +} + +.content { + display: flex; + flex-direction: column; + align-items: center; +} + +.button { + width: 200px; + box-sizing: border-box; + margin: 5px; +} diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js index 62d4165000c1d6912149c9a3f8f7310c14d79722..190d2fbf15dee94a59702e665544f48ebf0be731 100644 --- a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js @@ -6,6 +6,15 @@ const guestUsersCall = (guestsArray, status) => makeCall('allowPendingUsers', gu const changeGuestPolicy = policyRule => makeCall('changeGuestPolicy', policyRule); +const getGuestPolicy = () => { + const meeting = Meetings.findOne( + { meetingId: Auth.meetingID }, + { fields: { 'usersProp.guestPolicy': 1 } }, + ); + + return meeting.usersProp.guestPolicy; +}; + const isGuestLobbyMessageEnabled = Meteor.settings.public.app.enableGuestLobbyMessage; const getGuestLobbyMessage = () => { @@ -24,6 +33,7 @@ const setGuestLobbyMessage = (message) => makeCall('setGuestLobbyMessage', messa export default { guestUsersCall, changeGuestPolicy, + getGuestPolicy, isGuestLobbyMessageEnabled, getGuestLobbyMessage, setGuestLobbyMessage, diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 421fd097c7c4044f165e91a616dcb5f4d3490ac7..edac1b464d33e7ec5a36c477ab92a9564f1c7053 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -100,6 +100,8 @@ "app.userList.userOptions.unmuteAllDesc": "Unmutes the meeting", "app.userList.userOptions.lockViewersLabel": "Lock viewers", "app.userList.userOptions.lockViewersDesc": "Lock certain functionalities for attendees of the meeting", + "app.userList.userOptions.guestPolicyLabel": "Guest policy", + "app.userList.userOptions.guestPolicyDesc": "Change meeting guest policy setting", "app.userList.userOptions.connectionStatusLabel": "Connection status", "app.userList.userOptions.connectionStatusDesc": "View users' connection status", "app.userList.userOptions.disableCam": "Viewers' webcams are disabled", @@ -599,6 +601,12 @@ "app.lock-viewers.button.cancel": "Cancel", "app.lock-viewers.locked": "Locked", "app.lock-viewers.unlocked": "Unlocked", + "app.guest-policy.ariaTitle": "Guest policy settings modal", + "app.guest-policy.title": "Guest policy", + "app.guest-policy.description": "Change meeting guest policy setting", + "app.guest-policy.button.askModerator": "Ask moderator", + "app.guest-policy.button.alwaysAccept": "Always accept", + "app.guest-policy.button.alwaysDeny": "Always deny", "app.connection-status.ariaTitle": "Connection status modal", "app.connection-status.title": "Connection status", "app.connection-status.description": "View users' connection status",