diff --git a/bbb-api-demo/src/main/webapp/demo_iframe.jsp b/bbb-api-demo/src/main/webapp/demo_iframe.jsp index 3cd02f6fda6db623efc735311ae0a3e3a8e19f5f..e8aafb61d2849581eee2c88b0f9705ab10e31b0d 100644 --- a/bbb-api-demo/src/main/webapp/demo_iframe.jsp +++ b/bbb-api-demo/src/main/webapp/demo_iframe.jsp @@ -133,10 +133,11 @@ muteButton.id = 'muteButton'; function getInitialState() { document.getElementById('client-content').contentWindow.postMessage('c_recording_status', '*'); - document.getElementById('client-content').contentWindow.postMessage('c_mute_status', '*'); + document.getElementById('client-content').contentWindow.postMessage('get_audio_joined_status', '*'); } function handleMessage(e) { + let neverJoinedAudio = true; switch (e) { case 'readyToConnect': { // get initial state @@ -160,11 +161,18 @@ function handleMessage(e) { case 'notInAudio': { muteButton.innerHTML = 'Not in audio'; document.getElementById('muteButton').disabled = true; + if (neverJoinedAudio) { + // poll every 1 sec to check if we joined audio + setTimeout(function(){ + document.getElementById('client-content').contentWindow.postMessage('get_audio_joined_status', '*'); + }, 1000);} break; } case 'joinedAudio': { + neverJoinedAudio = false; muteButton.innerHTML = ''; - document.getElementById('muteButton').disabled = false; getInitialState(); + document.getElementById('muteButton').disabled = false; + document.getElementById('client-content').contentWindow.postMessage('c_mute_status', '*'); break; } default: console.log('neither', { e }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods.js b/bigbluebutton-html5/imports/api/meetings/server/methods.js old mode 100644 new mode 100755 index 74401c1b1544f0637d7245e0574139d6ccdf3165..960ef8082efae97b4e628dc50ec470774f70c746 --- a/bigbluebutton-html5/imports/api/meetings/server/methods.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods.js @@ -2,9 +2,13 @@ import { Meteor } from 'meteor/meteor'; import endMeeting from './methods/endMeeting'; import toggleRecording from './methods/toggleRecording'; import transferUser from './methods/transferUser'; +import toggleLockSettings from './methods/toggleLockSettings'; +import toggleWebcamsOnlyForModerator from './methods/toggleWebcamsOnlyForModerator'; Meteor.methods({ endMeeting, toggleRecording, + toggleLockSettings, transferUser, + toggleWebcamsOnlyForModerator, }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js new file mode 100755 index 0000000000000000000000000000000000000000..ee59cf102e9ed1b70e138abff731bbe21d08edce --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default function toggleLockSettings(credentials, meeting) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ChangeLockSettingsInMeetingCmdMsg'; + + const { meetingId, requesterUserId } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(meeting.lockSettingsProp, { + disableCam: Boolean, + disableMic: Boolean, + disablePrivChat: Boolean, + disablePubChat: Boolean, + lockedLayout: Boolean, + lockOnJoin: Boolean, + lockOnJoinConfigurable: Boolean, + setBy: String, + }); + + const payload = { + disableCam: meeting.lockSettingsProp.disableCam, + disableMic: meeting.lockSettingsProp.disableMic, + disablePrivChat: meeting.lockSettingsProp.disablePrivChat, + disablePubChat: meeting.lockSettingsProp.disablePubChat, + lockedLayout: meeting.lockSettingsProp.lockedLayout, + lockOnJoin: meeting.lockSettingsProp.lockOnJoin, + lockOnJoinConfigurable: meeting.lockSettingsProp.lockOnJoinConfigurable, + setBy: requesterUserId, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js new file mode 100755 index 0000000000000000000000000000000000000000..75f052de76503bc36967c966e157d4cbcb6a063d --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default function toggleWebcamsOnlyForModerator(credentials, meeting) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'UpdateWebcamsOnlyForModeratorCmdMsg'; + + const { meetingId, requesterUserId } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(meeting.usersProp.webcamsOnlyForModerator, Boolean); + + const payload = { + webcamsOnlyForModerator: meeting.usersProp.webcamsOnlyForModerator, + setBy: requesterUserId, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index a8023da3626343ddc0c50c913caba9062494b8e0..f8011fd0d32225429338b6252eefd78af4307bc8 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -69,10 +69,22 @@ export default function addMeeting(meeting) { meetingId, }; + const lockSettingsProp = { + disableCam: false, + disableMic: false, + disablePrivChat: false, + disablePubChat: false, + lockOnJoin: true, + lockOnJoinConfigurable: false, + lockedLayout: false, + setBy: 'temp', + }; + const modifier = { $set: Object.assign( { meetingId }, flat(meeting, { safe: true }), + { lockSettingsProp }, ), }; diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js b/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js index 105772c23263026b5baed5bd935bf5d74512e3f9..ddd232040867e1ae3c7d21ff9dc22971b542f76b 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js @@ -1,10 +1,13 @@ import Users from '/imports/api/users'; +import PresentationPods from '/imports/api/presentation-pods'; import changeRole from '/imports/api/users/server/modifiers/changeRole'; +import assignPresenter from '../methods/assignPresenter'; -export default function handlePresenterAssigned({ body }, meetingId) { +export default function handlePresenterAssigned(credentials, meetingId) { const USER_CONFIG = Meteor.settings.public.user; const ROLE_PRESENTER = USER_CONFIG.role_presenter; + const { body } = credentials; const { presenterId, assignedBy } = body; changeRole(ROLE_PRESENTER, true, presenterId, meetingId, assignedBy); @@ -15,11 +18,34 @@ export default function handlePresenterAssigned({ body }, meetingId) { presenter: true, }; + const defaultPodSelector = { + podId: 'DEFAULT_PRESENTATION_POD', + }; + const prevPresenter = Users.findOne(selector); // no previous presenters + // The below code is responsible for set Meeting presenter to be default pod presenter as well. + // It's been handled here because right now akka-apps don't handle all cases scenarios. if (!prevPresenter) { - return true; + const currentDefaultPodPresenter = PresentationPods.findOne(defaultPodSelector); + + const { currentPresenterId } = currentDefaultPodPresenter; + + const podPresenterCredentials = { + meetingId, + requesterUserId: assignedBy, + }; + + if (currentDefaultPodPresenter.currentPresenterId !== '') { + const oldPresenter = Users.findOne({ userId: currentPresenterId }); + + if (oldPresenter.connectionStatus === 'offline') { + return assignPresenter(podPresenterCredentials, presenterId); + } + return true; + } + return assignPresenter(podPresenterCredentials, presenterId); } return changeRole(ROLE_PRESENTER, false, prevPresenter.userId, meetingId, assignedBy); diff --git a/bigbluebutton-html5/imports/startup/server/logger.js b/bigbluebutton-html5/imports/startup/server/logger.js old mode 100755 new mode 100644 diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 865c0278339cdf30936bc7ab442068cf668c3b89..bada062b60acda6b11a2bdf8a9aca362dbb639fd 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -8,6 +8,7 @@ import Resizable from 're-resizable'; import browser from 'browser-detect'; import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container'; import PollingContainer from '/imports/ui/components/polling/container'; +import PollContainer from '/imports/ui/components/poll/container'; import ToastContainer from '../toast/container'; import ModalContainer from '../modal/container'; import NotificationsBarContainer from '../notifications-bar/container'; @@ -16,7 +17,7 @@ import ChatAlertContainer from '../chat/alert/container'; import { styles } from './styles'; import UserListContainer from '../user-list/container'; import ChatContainer from '../chat/container'; -import PollContainer from '/imports/ui/components/poll/container'; + const MOBILE_MEDIA = 'only screen and (max-width: 40em)'; const USERLIST_COMPACT_WIDTH = 50; @@ -327,7 +328,7 @@ class App extends Component { render() { const { - userListIsOpen, customStyle, customStyleUrl, + userListIsOpen, customStyle, customStyleUrl, micsLocked, } = this.props; const { enableResize } = this.state; @@ -349,11 +350,11 @@ class App extends Component { </section> <PollingContainer /> <ModalContainer /> - <AudioContainer /> + {micsLocked ? null : <AudioContainer />} <ToastContainer /> <ChatAlertContainer /> - { customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null } - { customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null } + {customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null} + {customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null} </main> ); } diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 600aee257851fedf1a23fe30c9df4aa3645db74a..748cb89be8ec13593c8c45b0b900197986026cd7 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -70,6 +70,7 @@ const AppContainer = (props) => { export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => { const currentUser = Users.findOne({ userId: Auth.userID }); + const meeting = Meetings.findOne({ meetingId: Auth.meetingID }); const isMeetingBreakout = meetingIsBreakout(); if (!currentUser.approved) { @@ -117,6 +118,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) pollIsOpen: Session.get('isPollOpen') && Session.get('isUserListOpen'), customStyle: getFromUserSettings('customStyle', false), customStyleUrl: getFromUserSettings('customStyleUrl', false), + micsLocked: (currentUser.locked && meeting.lockSettingsProp.disableMic), }; })(AppContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index b2334ede83a8d269b2a0f1313bdae8309f8543db..6211484d91a0473c476140b9080374fd5fee53ab 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data'; import { withModalMounter } from '/imports/ui/components/modal/service'; import AudioManager from '/imports/ui/services/audio-manager'; import { makeCall } from '/imports/ui/services/api'; +import Users from '/imports/api/users/'; +import Meetings from '/imports/api/meetings'; +import Auth from '/imports/ui/services/auth'; import AudioControls from './component'; import AudioModalContainer from '../audio-modal/container'; import Service from '../service'; @@ -15,11 +18,12 @@ const processToggleMuteFromOutside = (e) => { makeCall('toggleSelfVoice'); break; } + case 'get_audio_joined_status': { + const audioJoinedState = AudioManager.isConnected ? 'joinedAudio' : 'notInAudio'; + this.window.parent.postMessage({ response: audioJoinedState }, '*'); + break; + } case 'c_mute_status': { - if (!AudioManager.isUsingAudio()) { - this.window.parent.postMessage({ response: 'notInAudio' }, '*'); - return; - } const muteState = AudioManager.isMuted ? 'selfMuted' : 'selfUnmuted'; this.window.parent.postMessage({ response: muteState }, '*'); break; @@ -39,6 +43,12 @@ export default withModalMounter(withTracker(({ mountModal }) => disable: Service.isConnecting() || Service.isHangingUp(), glow: Service.isTalking() && !Service.isMuted(), handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(), - handleJoinAudio: () => mountModal(<AudioModalContainer />), + handleJoinAudio: () => { + const meeting = Meetings.findOne({ meetingId: Auth.meetingID }); + const currentUser = Users.findOne({ userId: Auth.userID }); + const micsLocked = (currentUser.locked && meeting.lockSettingsProp.disableMic); + + return micsLocked ? Service.joinListenOnly() : mountModal(<AudioModalContainer />); + }, handleLeaveAudio: () => Service.exitAudio(), }))(AudioControlsContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx index 800d6409e03e0c8c38cb70244c0725cd48cc92bb..297ee48d84444c471f344c70296ae62d1131c216 100644 --- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { Meteor } from 'meteor/meteor'; import Button from '/imports/ui/components/button/component'; +import logoutRouteHandler from '/imports/utils/logoutRouteHandler'; import { styles } from './styles'; const intlMessages = defineMessages({ @@ -66,7 +67,7 @@ class ErrorScreen extends React.PureComponent { <div className={styles.content}> <Button size="sm" - onClick={() => Session.set('isMeetingEnded', true)} + onClick={logoutRouteHandler} label={intl.formatMessage(intlMessages.leave)} /> </div> diff --git a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx index 55c515d1bdb26568cc3d5fd389e999c1128c6161..f1bfd5c9e311aab365bb2d2a841a921ca4d13dd6 100644 --- a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx @@ -71,6 +71,7 @@ class JoinHandler extends Component { return resp; }; + const setLogoutURL = (url) => Auth.logoutURL = url; const setLogoURL = (resp) => { setCustomLogoUrl(resp.customLogoURL); return resp; @@ -94,11 +95,12 @@ class JoinHandler extends Component { .then(response => response.json()) .then(({ response }) => response) .then((resp) => { + setLogoutURL(resp.logoutURL); if (resp.returncode !== 'FAILED') { logger.info(`User successfully went through main.joinRouteHandler with [${resp}].`); return resolve(resp); } - const e = new Error('Session not found'); + const e = new Error('Session not found'); logger.error(`User faced [${e}] on main.joinRouteHandler. Error was:`, JSON.stringify(resp)); return reject(e); }); diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..7c89ec1f1bbd035f3a1316caded8e3081dc9efb3 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx @@ -0,0 +1,251 @@ +import React, { Component } from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import Toggle from '/imports/ui/components/switch/component'; +import cx from 'classnames'; +import ModalBase from '/imports/ui/components/modal/base/component'; +import { styles } from './styles'; + +const intlMessages = defineMessages({ + lockViewersTitle: { + id: 'app.lock-viewers.title', + description: 'lock-viewers title', + }, + closeLabel: { + id: 'app.shortcut-help.closeLabel', + description: 'label for close button', + }, + closeDesc: { + id: 'app.shortcut-help.closeDesc', + description: 'description for close button', + }, + lockViewersDescription: { + id: 'app.lock-viewers.description', + description: 'description for lock viewers feature', + }, + featuresLable: { + id: 'app.lock-viewers.featuresLable', + description: 'features label', + }, + lockStatusLabel: { + id: 'app.lock-viewers.lockStatusLabel', + description: 'description for close button', + }, + webcamLabel: { + id: 'app.lock-viewers.webcamLabel', + description: 'description for close button', + }, + otherViewersWebcamLabel: { + id: 'app.lock-viewers.otherViewersWebcamLabel', + description: 'description for close button', + }, + microphoneLable: { + id: 'app.lock-viewers.microphoneLable', + description: 'description for close button', + }, + publicChatLabel: { + id: 'app.lock-viewers.PublicChatLabel', + description: 'description for close button', + }, + privateChatLable: { + id: 'app.lock-viewers.PrivateChatLable', + description: 'description for close button', + }, + layoutLable: { + id: 'app.lock-viewers.Layout', + description: 'description for close button', + }, +}); + +class LockViewersComponent extends Component { + constructor(props) { + super(props); + const { + closeModal, + toggleLockSettings, + toggleWebcamsOnlyForModerator, + } = props; + + this.closeModal = closeModal; + this.toggleLockSettings = toggleLockSettings; + this.toggleWebcamsOnlyForModerator = toggleWebcamsOnlyForModerator; + } + + render() { + const { intl, meeting } = this.props; + + return ( + <ModalBase + overlayClassName={styles.overlay} + className={styles.modal} + onRequestClose={this.closeModal} + > + + <div className={styles.container}> + <div className={styles.header}> + <div className={styles.title}>{intl.formatMessage(intlMessages.lockViewersTitle)}</div> + <Button + data-test="modalBaseCloseButton" + className={styles.closeBtn} + label={intl.formatMessage(intlMessages.closeLabel)} + icon="close" + size="md" + hideLabel + onClick={this.closeModal} + /> + </div> + <div className={styles.description}> + {`${intl.formatMessage(intlMessages.lockViewersDescription)}`} + </div> + + <div className={styles.form}> + <header className={styles.subHeader}> + <div className={styles.bold}>{intl.formatMessage(intlMessages.featuresLable)}</div> + <div className={styles.bold}>{intl.formatMessage(intlMessages.lockStatusLabel)}</div> + </header> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.webcamLabel)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.lockSettingsProp.disableCam} + onChange={() => { + meeting.lockSettingsProp.disableCam = + !meeting.lockSettingsProp.disableCam; + this.toggleLockSettings(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.webcamLabel)} + /> + </div> + </div> + </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.otherViewersWebcamLabel)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.usersProp.webcamsOnlyForModerator} + onChange={() => { + meeting.usersProp.webcamsOnlyForModerator = + !meeting.usersProp.webcamsOnlyForModerator; + this.toggleWebcamsOnlyForModerator(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.otherViewersWebcamLabel)} + /> + </div> + </div> + </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.microphoneLable)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.lockSettingsProp.disableMic} + onChange={() => { + meeting.lockSettingsProp.disableMic = + !meeting.lockSettingsProp.disableMic; + this.toggleLockSettings(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.microphoneLable)} + /> + </div> + </div> + </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.publicChatLabel)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.lockSettingsProp.disablePubChat} + onChange={() => { + meeting.lockSettingsProp.disablePubChat = + !meeting.lockSettingsProp.disablePubChat; + this.toggleLockSettings(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.publicChatLabel)} + /> + </div> + </div> + </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.privateChatLable)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.lockSettingsProp.disablePrivChat} + onChange={() => { + meeting.lockSettingsProp.disablePrivChat = + !meeting.lockSettingsProp.disablePrivChat; + this.toggleLockSettings(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.privateChatLable)} + /> + </div> + </div> + </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <div className={styles.label}> + {intl.formatMessage(intlMessages.layoutLable)} + </div> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={meeting.lockSettingsProp.lockedLayout} + onChange={() => { + meeting.lockSettingsProp.lockedLayout = + !meeting.lockSettingsProp.lockedLayout; + this.toggleLockSettings(meeting); + }} + ariaLabel={intl.formatMessage(intlMessages.layoutLable)} + /> + </div> + </div> + </div> + </div> + </div> + </ModalBase> + ); + } +} + +export default injectIntl(LockViewersComponent); diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/container.jsx b/bigbluebutton-html5/imports/ui/components/lock-viewers/container.jsx new file mode 100755 index 0000000000000000000000000000000000000000..2ce4edcc89e00bac16de77a4dbc5c48e4f60b8ea --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/container.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import { withModalMounter } from '/imports/ui/components/modal/service'; +import { makeCall } from '/imports/ui/services/api'; +import Meetings from '/imports/api/meetings'; +import Auth from '/imports/ui/services/auth'; +import LockViewersComponent from './component'; + +const LockViewersContainer = props => <LockViewersComponent {...props} />; + +export default withModalMounter(withTracker(({ mountModal }) => ({ + closeModal() { + mountModal(null); + }, + + toggleLockSettings(meeting) { + makeCall('toggleLockSettings', meeting); + }, + + toggleWebcamsOnlyForModerator(meeting) { + makeCall('toggleWebcamsOnlyForModerator', meeting); + }, + meeting: (Meetings.findOne({ meetingId: Auth.meetingID })), +}))(LockViewersContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.scss b/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.scss new file mode 100755 index 0000000000000000000000000000000000000000..078b36e6daa2ef8be0ca1eaad6c887953eda040e --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.scss @@ -0,0 +1,118 @@ +@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 { + position: relative; + left: var(--title-position-left); + color: var(--color-gray-dark); + font-weight: bold; + font-size: var(--font-size-large); + text-align: center; +} + +.form { + display: flex; + flex-flow: column; +} + +.container { + margin: var(--modal-margin); + margin-bottom: var(--lg-padding-x); +} + +.subHeader { + display: flex; + flex-flow: row; + flex-grow: 1; + justify-content: space-between; + color: var(--color-gray-label); + font-size: var(--font-size-base); + margin-bottom: var(--title-position-left); +} + +.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) +} + +.row { + display: flex; + flex-flow: row; + flex-grow: 1; + justify-content: space-between; + margin-bottom: var(--md-padding-x); +} + +.col { + display: flex; + flex-grow: 1; + flex-basis: 0; + margin-right: var(--md-padding-x); +} + +.label { + color: var(--color-gray-label); + font-size: var(--font-size-small); + margin-bottom: var(--lg-padding-y); +} + +.formElement { + position: relative; + display: flex; + flex-flow: column; + flex-grow: 1; +} + +.pullContentRight { + display: flex; + justify-content: flex-end; + flex-flow: row; +} + +.bold { + font-weight: bold; +} + +.closeBtn { + position: relative; + background-color: var(--color-white); + left: var(--closeBtn-position-left); + bottom: var(--closeBtn-position-left); + + i { + color: var(--color-gray-light); + } + + &:focus, + &:hover { + background-color: var(--color-gray-lighter); + i { + color: var(--color-gray); + } + } +} + +.header { + margin: 0; + padding: 0; + border: none; + line-height: var(--title-position-left); + margin-bottom: var(--lg-padding-y); +} diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx index 8d91aa3c755a47091426bbdeb8654e9f8557bfa7..da34c76529daceaae9077e8b8611f75caa33f844 100755 --- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import Auth from '/imports/ui/services/auth'; import Button from '/imports/ui/components/button/component'; import getFromUserSettings from '/imports/ui/services/users-settings'; +import logoutRouteHandler from '/imports/utils/logoutRouteHandler'; import Rating from './rating/component'; import { styles } from './styles'; @@ -77,19 +78,6 @@ class MeetingEnded extends React.PureComponent { const comment = textarea.value; return comment; } - - static logoutRouteHandler() { - Auth.logout() - .then((logoutURL = window.location.origin) => { - const protocolPattern = /^((http|https):\/\/)/; - - window.location.href = - protocolPattern.test(logoutURL) ? - logoutURL : - `http://${logoutURL}`; - }); - } - constructor(props) { super(props); this.state = { @@ -116,7 +104,7 @@ class MeetingEnded extends React.PureComponent { } = this.state; if (selected <= 0) { - MeetingEnded.logoutRouteHandler(); + logoutRouteHandler(); return; } @@ -138,7 +126,7 @@ class MeetingEnded extends React.PureComponent { fetch(url, options) .finally(() => { - MeetingEnded.logoutRouteHandler(); + logoutRouteHandler(); }); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss index d8de801266d015d2bfbf6c4b70f90fdc9399518c..50e54ffaf1e03fc3259b88ea863cba5b177194ad 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss @@ -121,6 +121,11 @@ color: var(--color-gray-light); } +.userListColumn { + @extend %flex-column; + min-height: 0; +} + .enter, .appear { opacity: 0.01; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss index 6ab7b7e20fe87ce91b77de1c08f0f4b40b084027..6c671c9f3a88438f609c0b7b07d562423b6f3442 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss @@ -12,7 +12,8 @@ .container{ display: flex; align-items: center; - margin-bottom: .3rem; + margin-bottom: var(--lg-padding-y); + margin-top: var(--sm-padding-x); } .scrollableList { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index b3907e93290f68d1b38faa39d4140b235196442d..2009826c847eb655f9f4928ae792802876a89db1 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -175,7 +175,7 @@ class UserParticipants extends Component { } = this.props; return ( - <div> + <div className={styles.userListColumn}> { !compact ? <div className={styles.container}> 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 3e689940efa29dc8d15164b34155a0cea5c0f528..69c2551259fbbf05e45d55e35b4b5e32043a64a0 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 @@ -9,6 +9,7 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; 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 { styles } from './styles'; const propTypes = { @@ -19,7 +20,6 @@ const propTypes = { toggleMuteAllUsers: PropTypes.func.isRequired, toggleMuteAllUsersExceptPresenter: PropTypes.func.isRequired, toggleStatus: PropTypes.func.isRequired, - toggleLockView: PropTypes.func.isRequired, }; const intlMessages = defineMessages({ @@ -83,7 +83,8 @@ class UserOptions extends Component { } componentWillMount() { - const { intl, isMeetingMuted } = this.props; + const { intl, isMeetingMuted, mountModal } = this.props; + this.menuItems = _.compact([ (<DropdownListItem key={_.uniqueId('list-item-')} @@ -111,7 +112,7 @@ class UserOptions extends Component { icon="lock" label={intl.formatMessage(intlMessages.lockViewersLabel)} description={intl.formatMessage(intlMessages.lockViewersDesc)} - onClick={this.props.toggleLockView} + onClick={() => mountModal(<LockViewersContainer />)} />), ]); 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 59561dab0d68b6c8b63d9e3d42f5b3cd2899ce1f..cf5af07ef7fd03c58eb1b2b82f7c11597c1daffe 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 @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import logger from '/imports/startup/client/logger'; import Auth from '/imports/ui/services/auth'; import mapUser from '/imports/ui/services/user/mapUser'; import Users from '/imports/api/users/'; @@ -26,7 +25,6 @@ export default class UserOptionsContainer extends Component { this.muteMeeting = this.muteMeeting.bind(this); this.muteAllUsersExceptPresenter = this.muteAllUsersExceptPresenter.bind(this); - this.handleLockView = this.handleLockView.bind(this); this.handleClearStatus = this.handleClearStatus.bind(this); } @@ -44,11 +42,6 @@ export default class UserOptionsContainer extends Component { muteAllExceptPresenter(currentUser.userId); } - handleLockView() { - // Temporary lock method, will be changed in future PR - logger.info('handleLockView function'); - } - handleClearStatus() { const { users, setEmojiStatus } = this.props; @@ -71,7 +64,6 @@ export default class UserOptionsContainer extends Component { <UserOptions toggleMuteAllUsers={this.muteMeeting} toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter} - toggleLockView={this.handleLockView} toggleStatus={this.handleClearStatus} isMeetingMuted={this.state.meetingMuted} /> : null diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index 1c7fe511e3766b0b6d26e05705b8f1ab6ba7b07b..24095302e509c2d404bbe031ffa32d9e0732c251 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -388,7 +388,7 @@ class VideoProvider extends Component { } async createWebRTCPeer(id, shareWebcam) { - const { meetingId, sessionToken } = this.props; + const { meetingId, sessionToken, voiceBridge } = this.props; let iceServers = []; try { @@ -448,6 +448,7 @@ class VideoProvider extends Component { sdpOffer: offerSdp, cameraId: id, meetingId, + voiceBridge, }; this.sendMessage(message); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx index 12bab47d132c9406c24351b9571084d172acd7a9..eb6bbd51d1bc99f4e58ee856baa8451bd36b963e 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx @@ -14,4 +14,5 @@ export default withTracker(() => ({ sessionToken: VideoService.sessionToken(), userName: VideoService.userName(), enableVideoStats: getFromUserSettings('enableVideoStats', Meteor.settings.public.kurento.enableVideoStats), + voiceBridge: VideoService.voiceBridge(), }))(VideoProviderContainer); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 5c672116d78bf63326d99ea03bc6e6e5b77f7067..0637da6234d9000cd5f9b17a2638f49cfe97845f 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -126,6 +126,11 @@ class VideoService { return Auth.sessionToken; } + voiceBridge() { + const voiceBridge = Meetings.findOne({ meetingId: Auth.meetingID }).voiceProp.voiceConf; + return voiceBridge; + } + isConnected() { return this.isConnected; } @@ -157,4 +162,5 @@ export default { meetingId: () => videoService.meetingId(), getAllUsersVideo: () => videoService.getAllUsersVideo(), sessionToken: () => videoService.sessionToken(), + voiceBridge: () => videoService.voiceBridge(), }; diff --git a/bigbluebutton-html5/imports/utils/logoutRouteHandler.js b/bigbluebutton-html5/imports/utils/logoutRouteHandler.js new file mode 100644 index 0000000000000000000000000000000000000000..c0febc21367d1bad7ef5475b4d9df34a3526c949 --- /dev/null +++ b/bigbluebutton-html5/imports/utils/logoutRouteHandler.js @@ -0,0 +1,15 @@ +import Auth from '/imports/ui/services/auth'; + +const logoutRouteHandler = () => { + Auth.logout() + .then((logoutURL = window.location.origin) => { + const protocolPattern = /^((http|https):\/\/)/; + + window.location.href = + protocolPattern.test(logoutURL) ? + logoutURL : + `http://${logoutURL}`; + }); +}; + +export default logoutRouteHandler; diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 7e390556857e62c8b575d5c16d99af51a5184a1f..abc96fb5fd3ec269087ef70abbe9bc798fb0f0d8 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -359,6 +359,16 @@ "app.shortcut-help.closePrivateChat": "Close Private Chat", "app.shortcut-help.openActions": "Open Actions Menu", "app.shortcut-help.openStatus": "Open Status Menu", + "app.lock-viewers.title": "Lock Viewers", + "app.lock-viewers.description": "These options enable you to restrict certain features available to viewers, such as locking out their ability to use private chat. (These restrictions do no apply to moderators)", + "app.lock-viewers.featuresLable": "Feature", + "app.lock-viewers.lockStatusLabel": "Locked Status", + "app.lock-viewers.webcamLabel": "Webcam", + "app.lock-viewers.otherViewersWebcamLabel": "See other viewsers webcams", + "app.lock-viewers.microphoneLable": "Microphone", + "app.lock-viewers.PublicChatLabel": "Public Chat", + "app.lock-viewers.PrivateChatLable": "Private Chat", + "app.lock-viewers.Layout": "Layout", "app.videoPreview.cameraLabel": "Camera", "app.videoPreview.cancelLabel": "Cancel", "app.videoPreview.closeLabel": "Close", diff --git a/build_script.sh b/build_script.sh index 42265a3dba3e0322c18354c508a13986dff56ce5..a83ccde4eab531a1c81288712d1b5f4129101f64 100755 --- a/build_script.sh +++ b/build_script.sh @@ -8,8 +8,17 @@ if [[ $files = *"bigbluebutton-html5"* ]]; then meteor npm install if [ $1 = linter ] then + html5_files="" + list=$(echo $files | tr " " "\n") + for file in $list + do + if [[ $file = bigbluebutton-html5* ]] ; then + html5_files+=" $file" + fi + done + cd .. - bigbluebutton-html5/node_modules/.bin/eslint --ext .jsx,.js $files + bigbluebutton-html5/node_modules/.bin/eslint --ext .jsx,.js $html5_files elif [ $1 = acceptance_tests ] then {