diff --git a/README.md b/README.md index 45da35c3197bdaef811ca36be7856ea22b8899ba..537953b83a060c82f991fc8f6038a6520b7733e2 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,18 @@ BigBlueButton ============= BigBlueButton is an open source web conferencing system. -BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard controls), chat, and the screen. Instructors can engage remote students with polling, emojis, and breakout rooms. BigBlueButton can record and playback all content shared in a session. +BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard controls), chat, and the screen. Instructors can engage remote students with polling, emojis, multi-user whiteboard, and breakout rooms. + +Presenters can record and playback content for later sharing with others. We designed BigBlueButton for online learning (though it can be used for many [other applications](http://www.c4isrnet.com/story/military-tech/disa/2015/02/11/disa-to-save-12m-defense-collaboration-services/23238997/)). The educational use cases for BigBlueButton are - * One-to-one on-line tutoring - * Small group collaboration - * On-line classes + * Online tutoring (one-to-one) + * Flipped classrooms (recording content ahead of your session) + * Group collaboration (many-to-many) + * Online classes (one-to-many) -BigBlueButton runs on a Ubuntu 16.04 64-bit server. If you follow the [installation instructions](http://docs.bigbluebutton.org/install/install.html), we guarantee you will have BigBlueButton installed and running within 30 minutes (or your money back :-). +You can install on a Ubuntu 16.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running within 30 minutes (or your money back :-). For full technical documentation BigBlueButton -- including architecture, features, API, and GreenLight (the default front-end) -- see [http://docs.bigbluebutton.org/](http://docs.bigbluebutton.org/). diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 1cee0185f6857c851606ea9d67015362f3f6756f..6d773bac37d7956675b8446bc7b82796b6a111ea 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -10,7 +10,7 @@ } body { - position: absolute; + position: fixed; height: 100%; font-family: 'Source Sans Pro', Arial, sans-serif; font-size: 1rem; /* 16px */ diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index c9072c61105d261ad0c1d3fbb4462fc1bdbdfd53..b01f3538dbcdfb421707dfc2172dc1e88abfd621 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -8,10 +8,15 @@ function breakouts(credentials, moderator) { requesterUserId, } = credentials; Logger.info(`Publishing Breakouts for ${meetingId} ${requesterUserId}`); + if (moderator) { const presenterSelector = { - parentMeetingId: meetingId, + $or: [ + { parentMeetingId: meetingId }, + { breakoutId: meetingId }, + ], }; + return Breakouts.find(presenterSelector); } diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 4428471e9d3bafb1363607e4d010cd155f0ae3c4..5cd70b18d9afd93b17fb4f4785b0b4c7f8f41017 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -47,9 +47,9 @@ class Base extends Component { componentWillUpdate() { const { approved } = this.props; - const isLoading = this.state.loading; + const { loading } = this.state; - if (approved && isLoading) this.updateLoadingState(false); + if (approved && loading) this.updateLoadingState(false); } updateLoadingState(loading = false) { @@ -131,7 +131,8 @@ const BaseContainer = withTracker(() => { }, }; - const subscriptionsHandlers = SUBSCRIPTIONS_NAME.map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler)); + const subscriptionsHandlers = SUBSCRIPTIONS_NAME + .map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler)); const chats = GroupChat.find({ $or: [ @@ -147,7 +148,8 @@ const BaseContainer = withTracker(() => { const chatIds = chats.map(chat => chat.chatId); const groupChatMessageHandler = Meteor.subscribe('group-chat-msg', credentials, chatIds, subscriptionErrorHandler); - const User = Users.findOne({ intId: credentials.externUserID }); + const User = Users.findOne({ intId: credentials.requesterUserId }); + if (User) { const mappedUser = mapUser(User); breakoutRoomSubscriptionHandler = Meteor.subscribe('breakouts', credentials, mappedUser.isModerator, subscriptionErrorHandler); 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 0676065f445719c327c30932904a8d029895d2da..dc558fc24a6c83a79f7459c2b417bdf287e37ee6 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 @@ -20,6 +20,17 @@ const propTypes = { isUserPresenter: PropTypes.bool.isRequired, intl: intlShape.isRequired, mountModal: PropTypes.func.isRequired, + isUserModerator: PropTypes.bool.isRequired, + allowStartStopRecording: PropTypes.bool.isRequired, + isRecording: PropTypes.bool.isRequired, + record: PropTypes.func.isRequired, + toggleRecording: PropTypes.func.isRequired, + meetingIsBreakout: PropTypes.bool.isRequired, + hasBreakoutRoom: PropTypes.bool.isRequired, + createBreakoutRoom: PropTypes.func.isRequired, + meetingName: PropTypes.string.isRequired, + shortcuts: PropTypes.string.isRequired, + users: PropTypes.arrayOf(PropTypes.object).isRequired, }; const intlMessages = defineMessages({ @@ -116,20 +127,37 @@ class ActionsDropdown extends Component { isRecording, record, toggleRecording, - togglePollMenu, meetingIsBreakout, hasBreakoutRoom, } = this.props; + const { + pollBtnLabel, + pollBtnDesc, + presentationLabel, + presentationDesc, + startRecording, + stopRecording, + createBreakoutRoom, + createBreakoutRoomDesc, + } = intlMessages; + + const { + formatMessage, + } = intl; + return _.compact([ (isUserPresenter ? ( <DropdownListItem icon="user" - label={intl.formatMessage(intlMessages.pollBtnLabel)} - description={intl.formatMessage(intlMessages.pollBtnDesc)} + label={formatMessage(pollBtnLabel)} + description={formatMessage(pollBtnDesc)} key={this.pollId} - onClick={() => togglePollMenu()} + onClick={() => { + Session.set('openPanel', 'poll'); + Session.set('forcePollOpen', true); + }} /> ) : null), @@ -138,8 +166,8 @@ class ActionsDropdown extends Component { <DropdownListItem data-test="uploadPresentation" icon="presentation" - label={intl.formatMessage(intlMessages.presentationLabel)} - description={intl.formatMessage(intlMessages.presentationDesc)} + label={formatMessage(presentationLabel)} + description={formatMessage(presentationDesc)} key={this.presentationItemId} onClick={this.handlePresentationClick} /> @@ -149,10 +177,10 @@ class ActionsDropdown extends Component { ? ( <DropdownListItem icon="record" - label={intl.formatMessage(isRecording - ? intlMessages.stopRecording : intlMessages.startRecording)} - description={intl.formatMessage(isRecording - ? intlMessages.stopRecording : intlMessages.startRecording)} + label={formatMessage(isRecording + ? stopRecording : startRecording)} + description={formatMessage(isRecording + ? stopRecording : startRecording)} key={this.recordId} onClick={toggleRecording} /> @@ -162,8 +190,8 @@ class ActionsDropdown extends Component { ? ( <DropdownListItem icon="rooms" - label={intl.formatMessage(intlMessages.createBreakoutRoom)} - description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)} + label={formatMessage(createBreakoutRoom)} + description={formatMessage(createBreakoutRoomDesc)} key={this.createBreakoutRoomId} onClick={this.handleCreateBreakoutRoomClick} /> @@ -173,7 +201,8 @@ class ActionsDropdown extends Component { } handlePresentationClick() { - this.props.mountModal(<PresentationUploaderContainer />); + const { mountModal } = this.props; + mountModal(<PresentationUploaderContainer />); } handleCreateBreakoutRoomClick() { @@ -189,7 +218,8 @@ class ActionsDropdown extends Component { createBreakoutRoom={createBreakoutRoom} meetingName={meetingName} users={users} - />); + />, + ); } render() { diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 57d39bf1f0c7cf8c9dc6d4993f58ef3c680211b3..213d75e779ca7b0e66b14922e403dfb01398341d 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -18,7 +18,6 @@ class ActionsBar extends React.PureComponent { isUserModerator, recordSettingsList, toggleRecording, - togglePollMenu, screenSharingCheck, enableVideo, createBreakoutRoom, @@ -48,7 +47,6 @@ class ActionsBar extends React.PureComponent { isRecording, record, toggleRecording, - togglePollMenu, createBreakoutRoom, meetingIsBreakout, hasBreakoutRoom, @@ -57,21 +55,27 @@ class ActionsBar extends React.PureComponent { }} /> </div> - <div className={isUserPresenter ? cx(styles.centerWithActions, actionBarClasses) : styles.center}> + <div + className={ + isUserPresenter ? cx(styles.centerWithActions, actionBarClasses) : styles.center + } + > <AudioControlsContainer /> - {enableVideo ? - <JoinVideoOptionsContainer - handleJoinVideo={handleJoinVideo} - handleCloseVideo={handleExitVideo} - /> + {enableVideo + ? ( + <JoinVideoOptionsContainer + handleJoinVideo={handleJoinVideo} + handleCloseVideo={handleExitVideo} + /> + ) : null} <DesktopShare {...{ - handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, - isUserPresenter, - screenSharingCheck, - }} + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, + isUserPresenter, + screenSharingCheck, + }} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index ea5d7d6897023a9d1bfaa90b16e24c1a75615ec8..c3c393cf22bc2cc2a5c2990b3e105693f06492fd 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -1,6 +1,5 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; -import { Session } from 'meteor/session'; import getFromUserSettings from '/imports/ui/services/users-settings'; import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; @@ -12,23 +11,6 @@ import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/ const ActionsBarContainer = props => <ActionsBar {...props} />; export default withTracker(() => { - const togglePollMenu = () => { - const showPoll = Session.equals('isPollOpen', false) || !Session.get('isPollOpen'); - - const show = () => { - Session.set('isUserListOpen', true); - Session.set('isPollOpen', true); - Session.set('forcePollOpen', true); - }; - - const hide = () => Session.set('isPollOpen', false); - - Session.set('isChatOpen', false); - Session.set('breakoutRoomIsOpen', false); - - return showPoll ? show() : hide(); - }; - Meetings.find({ meetingId: Auth.meetingID }).observeChanges({ changed: (id, fields) => { if (fields.recordProp && fields.recordProp.recording) { @@ -57,7 +39,6 @@ export default withTracker(() => { meetingIsBreakout: Service.meetingIsBreakout(), hasBreakoutRoom: Service.hasBreakoutRoom(), meetingName: Service.meetingName(), - togglePollMenu, users: Service.users(), }; })(ActionsBarContainer); diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 6a1fb08bfb6a87821561bccd81e37800477e43e6..a4b497ffa8fd612349af093b05f8b0289d610dac 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -3,12 +3,9 @@ import PropTypes from 'prop-types'; import { throttle } from 'lodash'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import Modal from 'react-modal'; -import cx from 'classnames'; -import Resizable from 're-resizable'; import browser from 'browser-detect'; -import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container'; +import PanelManager from '/imports/ui/components/panel-manager/component'; import PollingContainer from '/imports/ui/components/polling/container'; -import PollContainer from '/imports/ui/components/poll/container'; import logger from '/imports/startup/client/logger'; import ToastContainer from '../toast/container'; import ModalContainer from '../modal/container'; @@ -16,12 +13,8 @@ import NotificationsBarContainer from '../notifications-bar/container'; import AudioContainer from '../audio/container'; import ChatAlertContainer from '../chat/alert/container'; import { styles } from './styles'; -import UserListContainer from '../user-list/container'; -import ChatContainer from '../chat/container'; - const MOBILE_MEDIA = 'only screen and (max-width: 40em)'; -const USERLIST_COMPACT_WIDTH = 50; const intlMessages = defineMessages({ userListLabel: { @@ -51,7 +44,6 @@ const propTypes = { closedCaption: PropTypes.element, userListIsOpen: PropTypes.bool.isRequired, chatIsOpen: PropTypes.bool.isRequired, - pollIsOpen: PropTypes.bool.isRequired, locale: PropTypes.string, intl: intlShape.isRequired, }; @@ -71,7 +63,6 @@ class App extends Component { super(); this.state = { - compactUserList: false, enableResize: !window.matchMedia(MOBILE_MEDIA).matches, }; @@ -79,11 +70,11 @@ class App extends Component { } componentDidMount() { - const { locale } = this.props; + const { locale, fontSize } = this.props; Modal.setAppElement('#app'); document.getElementsByTagName('html')[0].lang = locale; - document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize; + document.getElementsByTagName('html')[0].style.fontSize = fontSize; const BROWSER_RESULTS = browser(); const body = document.getElementsByTagName('body')[0]; @@ -112,15 +103,17 @@ class App extends Component { this.setState({ enableResize: shouldEnableResize }); } - renderPoll() { - const { pollIsOpen } = this.props; - - if (!pollIsOpen) return null; + renderPanel() { + const { enableResize } = this.state; + const { openPanel } = this.props; return ( - <div className={styles.poll}> - <PollContainer /> - </div> + <PanelManager + {...{ + openPanel, + enableResize, + }} + /> ); } @@ -160,136 +153,6 @@ class App extends Component { ); } - renderUserList() { - const { - intl, chatIsOpen, userListIsOpen, - } = this.props; - - const { compactUserList } = this.state; - - if (!userListIsOpen) return null; - - const userListStyle = {}; - userListStyle[styles.compact] = compactUserList; - // userList = React.cloneElement(userList, { - // compact: compactUserList, // TODO 4767 - // }); - - return ( - <div - className={cx(styles.userList, userListStyle)} - aria-label={intl.formatMessage(intlMessages.userListLabel)} - aria-hidden={chatIsOpen} - > - <UserListContainer /> - </div> - ); - } - - renderBreakoutRoom() { - const { hasBreakoutRooms, breakoutRoomIsOpen } = this.props; - - if (!breakoutRoomIsOpen) return null; - if (!hasBreakoutRooms) return null; - return ( - <div className={styles.breakoutRoom}> - <BreakoutRoomContainer /> - </div> - ); - } - - renderUserListResizable() { - const { userListIsOpen } = this.props; - - // Variables for resizing user-list. - const USERLIST_MIN_WIDTH_PX = 150; - const USERLIST_MAX_WIDTH_PX = 240; - const USERLIST_DEFAULT_WIDTH_RELATIVE = 18; - - // decide whether using pixel or percentage unit as a default width for userList - const USERLIST_DEFAULT_WIDTH = (window.innerWidth * (USERLIST_DEFAULT_WIDTH_RELATIVE / 100.0)) < USERLIST_MAX_WIDTH_PX ? `${USERLIST_DEFAULT_WIDTH_RELATIVE}%` : USERLIST_MAX_WIDTH_PX; - - if (!userListIsOpen) return null; - - const resizableEnableOptions = { - top: false, - right: true, - bottom: false, - left: false, - topRight: false, - bottomRight: false, - bottomLeft: false, - topLeft: false, - }; - - return ( - <Resizable - defaultSize={{ width: USERLIST_DEFAULT_WIDTH }} - minWidth={USERLIST_MIN_WIDTH_PX} - maxWidth={USERLIST_MAX_WIDTH_PX} - ref={(node) => { this.resizableUserList = node; }} - enable={resizableEnableOptions} - onResize={(e, direction, ref) => { - const { compactUserList } = this.state; - const shouldBeCompact = ref.clientWidth <= USERLIST_COMPACT_WIDTH; - if (compactUserList === shouldBeCompact) return; - this.setState({ compactUserList: shouldBeCompact }); - }} - > - {this.renderUserList()} - </Resizable> - ); - } - - renderChat() { - const { intl, chatIsOpen } = this.props; - - if (!chatIsOpen) return null; - - return ( - <section - className={styles.chat} - aria-label={intl.formatMessage(intlMessages.chatLabel)} - > - <ChatContainer /> - </section> - ); - } - - renderChatResizable() { - const { chatIsOpen } = this.props; - - // Variables for resizing chat. - const CHAT_MIN_WIDTH = '10%'; - const CHAT_MAX_WIDTH = '25%'; - const CHAT_DEFAULT_WIDTH = '15%'; - - if (!chatIsOpen) return null; - - const resizableEnableOptions = { - top: false, - right: true, - bottom: false, - left: false, - topRight: false, - bottomRight: false, - bottomLeft: false, - topLeft: false, - }; - - return ( - <Resizable - defaultSize={{ width: CHAT_DEFAULT_WIDTH }} - minWidth={CHAT_MIN_WIDTH} - maxWidth={CHAT_MAX_WIDTH} - ref={(node) => { this.resizableChat = node; }} - enable={resizableEnableOptions} - > - {this.renderChat()} - </Resizable> - ); - } - renderMedia() { const { media, intl, chatIsOpen, userListIsOpen, @@ -329,9 +192,8 @@ class App extends Component { render() { const { - userListIsOpen, customStyle, customStyleUrl, micsLocked, + customStyle, customStyleUrl, micsLocked, } = this.props; - const { enableResize } = this.state; return ( <main className={styles.main}> @@ -342,11 +204,7 @@ class App extends Component { {this.renderMedia()} {this.renderActionsBar()} </div> - {enableResize ? this.renderUserListResizable() : this.renderUserList()} - {userListIsOpen && enableResize ? <div className={styles.userlistPad} /> : null} - {enableResize ? this.renderChatResizable() : this.renderChat()} - {this.renderPoll()} - {this.renderBreakoutRoom()} + {this.renderPanel()} {this.renderSidebar()} </section> <PollingContainer /> diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 3e1af53e04e10874a23bc3e7ddb816e23d1a16c5..a289c016da41c7b4223d0759631d5548842497b8 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -67,7 +67,6 @@ const AppContainer = (props) => { ); }; - export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => { const currentUser = Users.findOne({ userId: Auth.userID }); const currentUserIsLocked = mapUser(currentUser).isLocked; @@ -111,12 +110,12 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) closedCaption: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null, fontSize: getFontSize(), hasBreakoutRooms: getBreakoutRooms().length > 0, - userListIsOpen: Session.get('isUserListOpen'), - breakoutRoomIsOpen: Session.get('breakoutRoomIsOpen') && Session.get('isUserListOpen'), - chatIsOpen: Session.get('isChatOpen') && Session.get('isUserListOpen'), - pollIsOpen: Session.get('isPollOpen') && Session.get('isUserListOpen'), customStyle: getFromUserSettings('customStyle', false), customStyleUrl: getFromUserSettings('customStyleUrl', false), + breakoutRoomIsOpen: Session.equals('openPanel', 'breakoutroom'), + chatIsOpen: Session.equals('openPanel', 'chat'), + openPanel: Session.get('openPanel'), + userListIsOpen: !Session.equals('openPanel', ''), micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic), }; })(AppContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/app/styles.scss b/bigbluebutton-html5/imports/ui/components/app/styles.scss index 089e4bf729c01531f965f267fb9607819ef62868..f5ad8b2ed2b0bc6f8bab61586e8fb61ba68c57ad 100755 --- a/bigbluebutton-html5/imports/ui/components/app/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/app/styles.scss @@ -12,9 +12,8 @@ } .main { - position: fixed; + position: relative; height: 100%; - width: 100%; display: flex; flex-direction: column; } diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js index 9158ea9d373006c61e678152d7f8264f6d502198..5f6f1bebd33cc94dc0f3659af7ed87a742f7fd13 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js @@ -25,9 +25,11 @@ const breakoutRoomUser = (breakoutId) => { return breakoutUser; }; +const closeBreakoutPanel = () => Session.set('openPanel', 'userlist'); const endAllBreakouts = () => { makeCall('endAllBreakouts'); + closeBreakoutPanel(); }; const requestJoinURL = (breakoutId) => { @@ -63,9 +65,6 @@ const isModerator = () => { return mappedUser.isModerator; }; - -const closeBreakoutPanel = () => Session.set('breakoutRoomIsOpen', false); - export default { findBreakouts, endAllBreakouts, diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx index ba73f249ff3953d15c29377717dfdb71de8261cf..562f7a0db1155fd3e34795370683a641ee6adb04 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx @@ -23,10 +23,9 @@ class ChatPushAlert extends PureComponent { aria-label={message} tabIndex={0} onClick={() => { - Session.set('isUserListOpen', true); - Session.set('isChatOpen', true); - Session.set('idChatOpen', chatId); - }} + Session.set('openPanel', 'chat'); + Session.set('idChatOpen', chatId); + }} > { message } </div> diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index 03c00b88a9877a73e9337feb967621f5df038660..1554d7f32f147cc3bc1e121afe333b383a2c39c3 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -56,7 +56,7 @@ const Chat = (props) => { > <Button onClick={() => { - Session.set('isChatOpen', false); + Session.set('openPanel', 'userlist'); }} aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })} accessKey={HIDE_CHAT_AK} @@ -66,23 +66,23 @@ const Chat = (props) => { /> </div> { - chatID !== 'public' ? - <Button - className={styles.closeBtn} - icon="close" - size="md" - hideLabel - onClick={() => { - actions.handleClosePrivateChat(chatID); - Session.set('isChatOpen', false); - Session.set('idChatOpen', ''); - }} - aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} - label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} - accessKey={CLOSE_CHAT_AK} - /> - : - <ChatDropdown /> + chatID !== 'public' + ? ( + <Button + className={styles.closeBtn} + icon="close" + size="md" + hideLabel + onClick={() => { + actions.handleClosePrivateChat(chatID); + Session.set('openPanel', 'userlist'); + }} + aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} + label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} + accessKey={CLOSE_CHAT_AK} + /> + ) + : <ChatDropdown /> } </header> <MessageList @@ -122,6 +122,7 @@ const propTypes = { PropTypes.object, ])).isRequired).isRequired, scrollPosition: PropTypes.number, + shortcuts: PropTypes.string.isRequired, hasUnreadMessages: PropTypes.bool.isRequired, lastReadMessageTime: PropTypes.number.isRequired, partnerIsLoggedOut: PropTypes.bool.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx index 353f27bd8b673295c971c266da8d511297009967..b62f599fd42421c845b361c0b31c99a2c461e276 100644 --- a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { Session } from 'meteor/session'; +import PropTypes from 'prop-types'; import Auth from '/imports/ui/services/auth'; import { setCustomLogoUrl } from '/imports/ui/components/user-list/service'; import { makeCall } from '/imports/ui/services/api'; @@ -7,6 +8,9 @@ import deviceInfo from '/imports/utils/deviceInfo'; import logger from '/imports/startup/client/logger'; import LoadingScreen from '/imports/ui/components/loading-screen/component'; +const propTypes = { + children: PropTypes.element.isRequired, +}; class JoinHandler extends Component { static setError(codeError) { @@ -99,7 +103,6 @@ class JoinHandler extends Component { }; // use enter api to get params for the client const url = `/bigbluebutton/api/enter?sessionToken=${sessionToken}`; - const fetchContent = await fetch(url, { credentials: 'same-origin' }); const parseToJson = await fetchContent.json(); const { response } = parseToJson; @@ -109,7 +112,11 @@ class JoinHandler extends Component { await setCustomData(response); setLogoURL(response); logUserInfo(); - Session.set('isUserListOpen', deviceInfo.type().isPhone); + + Session.set('openPanel', 'chat'); + Session.set('idChatOpen', ''); + if (deviceInfo.type().isPhone) Session.set('openPanel', ''); + logger.info(`User successfully went through main.joinRouteHandler with [${JSON.stringify(response)}].`); } else { const e = new Error('Session not found'); @@ -128,3 +135,5 @@ class JoinHandler extends Component { } export default JoinHandler; + +JoinHandler.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/media/component.jsx b/bigbluebutton-html5/imports/ui/components/media/component.jsx index 2e5faf840e1ca19d4ebca0807b8cffd7397c2b07..853c31d73f7a9a5a14393884942fa9a75e1b77c2 100644 --- a/bigbluebutton-html5/imports/ui/components/media/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/component.jsx @@ -24,7 +24,7 @@ export default class Media extends Component { render() { const { - swapLayout, floatingOverlay, hideOverlay, disableVideo, + swapLayout, floatingOverlay, hideOverlay, disableVideo, children, } = this.props; const contentClassName = cx({ @@ -40,7 +40,7 @@ export default class Media extends Component { return ( <div className={styles.container}> <div className={!swapLayout ? contentClassName : overlayClassName}> - {this.props.children} + {children} </div> <div className={!swapLayout ? overlayClassName : contentClassName}> { !disableVideo ? <VideoProviderContainer /> : null } diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 38625dc6e115e05c4ca3feaef2f09ef2c7bd6e20..8def7900d95e00f915b497050f2c334ccbe78adb 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; +import { Session } from 'meteor/session'; import _ from 'lodash'; import cx from 'classnames'; import Auth from '/imports/ui/services/auth'; @@ -48,7 +49,7 @@ const intlMessages = defineMessages({ const propTypes = { presentationTitle: PropTypes.string, hasUnreadMessages: PropTypes.bool, - beingRecorded: PropTypes.object, + beingRecorded: PropTypes.bool, shortcuts: PropTypes.string, }; @@ -59,10 +60,12 @@ const defaultProps = { shortcuts: '', }; -const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(<BreakoutJoinConfirmation - breakout={breakout} - breakoutName={breakoutName} -/>); +const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal( + <BreakoutJoinConfirmation + breakout={breakout} + breakoutName={breakoutName} + />, +); const closeBreakoutJoinConfirmation = mountModal => mountModal(null); @@ -85,6 +88,10 @@ class NavBar extends PureComponent { mountModal, } = this.props; + const { + didSendBreakoutInvite, + } = this.state; + const hadBreakouts = oldProps.breakouts.length; const hasBreakouts = breakouts.length; @@ -101,18 +108,23 @@ class NavBar extends PureComponent { if (!userOnMeeting) return; - if (!this.state.didSendBreakoutInvite && !isBreakoutRoom) { + if (!didSendBreakoutInvite && !isBreakoutRoom) { this.inviteUserToBreakout(breakout); } }); - if (!breakouts.length && this.state.didSendBreakoutInvite) { + if (!breakouts.length && didSendBreakoutInvite) { this.setState({ didSendBreakoutInvite: false }); } } handleToggleUserList() { - this.props.toggleUserList(); + Session.set( + 'openPanel', + Session.get('openPanel') !== '' + ? '' + : 'userlist', + ); } inviteUserToBreakout(breakout) { @@ -132,6 +144,10 @@ class NavBar extends PureComponent { presentationTitle, } = this.props; + const { + isActionsOpen, + } = this.state; + if (isBreakoutRoom || !breakouts.length) { return ( <h1 className={styles.presentationTitle}>{presentationTitle}</h1> @@ -140,7 +156,7 @@ class NavBar extends PureComponent { const breakoutItems = breakouts.map(breakout => this.renderBreakoutItem(breakout)); return ( - <Dropdown isOpen={this.state.isActionsOpen}> + <Dropdown isOpen={isActionsOpen}> <DropdownTrigger> <h1 className={cx(styles.presentationTitle, styles.dropdownBreakout)}> {presentationTitle} @@ -170,7 +186,9 @@ class NavBar extends PureComponent { <DropdownListItem key={_.uniqueId('action-header')} label={breakoutName} - onClick={openBreakoutJoinConfirmation.bind(this, breakout, breakoutName, mountModal)} + onClick={ + openBreakoutJoinConfirmation.bind(this, breakout, breakoutName, mountModal) + } /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx b/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ec52e29e5dc9f4fe7f83e92b31948d4fa5fef50e --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/panel-manager/component.jsx @@ -0,0 +1,198 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container'; +import UserListContainer from '/imports/ui/components/user-list/container'; +import ChatContainer from '/imports/ui/components/chat/container'; +import PollContainer from '/imports/ui/components/poll/container'; +import { defineMessages, injectIntl } from 'react-intl'; +import Resizable from 're-resizable'; +import { styles } from '/imports/ui/components/app/styles'; +import _ from 'lodash'; + +const intlMessages = defineMessages({ + chatLabel: { + id: 'app.chat.label', + description: 'Aria-label for Chat Section', + }, + userListLabel: { + id: 'app.userList.label', + description: 'Aria-label for Userlist Nav', + }, +}); + +const propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + enableResize: PropTypes.bool.isRequired, + openPanel: PropTypes.string.isRequired, +}; + +class PanelManager extends Component { + constructor() { + super(); + + this.padKey = _.uniqueId('resize-pad-'); + this.userlistKey = _.uniqueId('userlist-'); + this.breakoutroomKey = _.uniqueId('breakoutroom-'); + this.chatKey = _.uniqueId('chat-'); + this.pollKey = _.uniqueId('poll-'); + } + + renderUserList() { + const { + intl, + enableResize, + } = this.props; + + return ( + <div + className={styles.userList} + aria-label={intl.formatMessage(intlMessages.userListLabel)} + key={enableResize ? null : this.userlistKey} + > + <UserListContainer /> + </div> + ); + } + + renderUserListResizable() { + // Variables for resizing user-list. + const USERLIST_MIN_WIDTH_PX = 150; + const USERLIST_MAX_WIDTH_PX = 240; + const USERLIST_DEFAULT_WIDTH_RELATIVE = 18; + + // decide whether using pixel or percentage unit as a default width for userList + const USERLIST_DEFAULT_WIDTH = (window.innerWidth * (USERLIST_DEFAULT_WIDTH_RELATIVE / 100.0)) < USERLIST_MAX_WIDTH_PX ? `${USERLIST_DEFAULT_WIDTH_RELATIVE}%` : USERLIST_MAX_WIDTH_PX; + + const resizableEnableOptions = { + top: false, + right: true, + bottom: false, + left: false, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }; + + return ( + <Resizable + defaultSize={{ width: USERLIST_DEFAULT_WIDTH }} + minWidth={USERLIST_MIN_WIDTH_PX} + maxWidth={USERLIST_MAX_WIDTH_PX} + ref={(node) => { this.resizableUserList = node; }} + className={styles.resizableUserList} + enable={resizableEnableOptions} + key={this.userlistKey} + > + {this.renderUserList()} + </Resizable> + ); + } + + renderChat() { + const { intl, enableResize } = this.props; + + return ( + <section + className={styles.chat} + aria-label={intl.formatMessage(intlMessages.chatLabel)} + key={enableResize ? null : this.chatKey} + > + <ChatContainer /> + </section> + ); + } + + renderChatResizable() { + // Variables for resizing chat. + const CHAT_MIN_WIDTH = '10%'; + const CHAT_MAX_WIDTH = '25%'; + const CHAT_DEFAULT_WIDTH = '15%'; + + const resizableEnableOptions = { + top: false, + right: true, + bottom: false, + left: false, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }; + + return ( + <Resizable + defaultSize={{ width: CHAT_DEFAULT_WIDTH }} + minWidth={CHAT_MIN_WIDTH} + maxWidth={CHAT_MAX_WIDTH} + ref={(node) => { this.resizableChat = node; }} + className={styles.resizableChat} + enable={resizableEnableOptions} + key={this.chatKey} + > + {this.renderChat()} + </Resizable> + ); + } + + renderPoll() { + return ( + <div className={styles.poll} key={this.pollKey}> + <PollContainer /> + </div> + ); + } + + renderBreakoutRoom() { + return ( + <div className={styles.breakoutRoom} key={this.breakoutroomKey}> + <BreakoutRoomContainer /> + </div> + ); + } + + render() { + const { enableResize, openPanel } = this.props; + if (openPanel === '') return null; + + const panels = [this.renderUserList()]; + const resizablePanels = [ + this.renderUserListResizable(), + <div className={styles.userlistPad} key={this.padKey} />, + ]; + + if (openPanel === 'chat') { + if (enableResize) { + resizablePanels.push(this.renderChatResizable()); + } else { + panels.push(this.renderChat()); + } + } + + if (openPanel === 'poll') { + if (enableResize) { + resizablePanels.push(this.renderPoll()); + } else { + panels.push(this.renderPoll()); + } + } + + if (openPanel === 'breakoutroom') { + if (enableResize) { + resizablePanels.push(this.renderBreakoutRoom()); + } else { + panels.push(this.renderBreakoutRoom()); + } + } + + return enableResize + ? resizablePanels + : panels; + } +} + +export default injectIntl(PanelManager); + +PanelManager.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index e4377ba24c51252564914ed2c8a829a757e0b085..722d8c37be2a0c33e4e5d0326d30e6246e7e7527 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -92,6 +92,16 @@ class Poll extends Component { this.handleBackClick = this.handleBackClick.bind(this); } + componentDidUpdate() { + const { currentUser } = this.props; + + if (!currentUser.presenter) { + Session.set('openPanel', 'userlist'); + Session.set('forcePollOpen', false); + } + } + + handleInputChange(index, event) { // This regex will replace any instance of 2 or more consecutive white spaces // with a single white space character. @@ -100,21 +110,15 @@ class Poll extends Component { this.setState({ customPollValues: this.inputEditor }); } - renderInputFields() { - const { intl } = this.props; - const items = []; - - items = _.range(1, MAX_CUSTOM_FIELDS + 1).map((ele, index) => ( - <input - key={`custom-poll-${index}`} - placeholder={intl.formatMessage(intlMessages.customPlaceholder)} - className={styles.input} - onChange={event => this.handleInputChange(index, event)} - defaultValue={this.state.customPollValues[index]} - /> - )); + handleBackClick() { + const { stopPoll } = this.props; - return items; + stopPoll(); + this.inputEditor = []; + this.setState({ + isPolling: false, + customPollValues: this.inputEditor, + }, document.activeElement.blur()); } toggleCustomFields() { @@ -133,7 +137,8 @@ class Poll extends Component { const label = intl.formatMessage( // regex removes the - to match the message id - intlMessages[type.replace(/-/g, '').toLowerCase()]); + intlMessages[type.replace(/-/g, '').toLowerCase()], + ); return ( <Button @@ -142,8 +147,8 @@ class Poll extends Component { className={styles.pollBtn} key={_.uniqueId('quick-poll-')} onClick={() => { - this.setState({ isPolling: true }, () => startPoll(type)); - }} + this.setState({ isPolling: true }, () => startPoll(type)); + }} />); }); @@ -172,15 +177,25 @@ class Poll extends Component { ); } - handleBackClick() { - const { stopPoll } = this.props; + renderInputFields() { + const { intl } = this.props; + const { customPollValues } = this.state; + let items = []; - stopPoll(); - this.inputEditor = []; - this.setState({ - isPolling: false, - customPollValues: this.inputEditor, - }, document.activeElement.blur()); + items = _.range(1, MAX_CUSTOM_FIELDS + 1).map((ele, index) => { + const id = index; + return ( + <input + key={`custom-poll-${id}`} + placeholder={intl.formatMessage(intlMessages.customPlaceholder)} + className={styles.input} + onChange={event => this.handleInputChange(id, event)} + defaultValue={customPollValues[id]} + /> + ); + }); + + return items; } renderActivePollOptions() { @@ -235,9 +250,13 @@ class Poll extends Component { render() { const { - intl, stopPoll, currentPoll, + intl, stopPoll, currentPoll, currentUser, } = this.props; + const { isPolling } = this.state; + + if (!currentUser.presenter) return null; + return ( <div> <header className={styles.header}> @@ -248,22 +267,19 @@ class Poll extends Component { aria-label={intl.formatMessage(intlMessages.hidePollDesc)} className={styles.hideBtn} onClick={() => { - Session.set('isPollOpen', false); - Session.set('forcePollOpen', true); - Session.set('isUserListOpen', true); + Session.set('openPanel', 'userlist'); }} /> <Button label={intl.formatMessage(intlMessages.closeLabel)} onClick={() => { - if (currentPoll) { - stopPoll(); - } - Session.set('isPollOpen', false); - Session.set('forcePollOpen', false); - Session.set('isUserListOpen', true); - }} + if (currentPoll) { + stopPoll(); + } + Session.set('openPanel', 'userlist'); + Session.set('forcePollOpen', false); + }} className={styles.closeBtn} icon="close" size="sm" @@ -272,8 +288,8 @@ class Poll extends Component { </header> { - this.state.isPolling || !this.state.isPolling && currentPoll - ? this.renderActivePollOptions() : this.renderPollOptions() + (isPolling || (!isPolling && currentPoll)) + ? this.renderActivePollOptions() : this.renderPollOptions() } </div> ); diff --git a/bigbluebutton-html5/imports/ui/components/poll/container.jsx b/bigbluebutton-html5/imports/ui/components/poll/container.jsx index 619a45c47116eaf0a441517a1197cd3cb3217d75..65dae7e018061905323ec51960488c53a4133ac9 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/container.jsx @@ -1,23 +1,13 @@ import React from 'react'; import { makeCall } from '/imports/ui/services/api'; import { withTracker } from 'meteor/react-meteor-data'; -import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; import Presentations from '/imports/api/presentations'; import PresentationAreaService from '/imports/ui/components/presentation/service'; import Poll from './component'; import Service from './service'; -const PollContainer = ({ ...props }) => { - const currentUser = Users.findOne({ userId: Auth.userID }); - if (currentUser.presenter) { - return (<Poll {...props} />); - } - Session.set('isPollOpen', false); - Session.set('forcePollOpen', false); - Session.set('isUserListOpen', true); - return null; -}; +const PollContainer = ({ ...props }) => <Poll {...props} />; export default withTracker(({ }) => { Meteor.subscribe('current-poll', Auth.meetingID); diff --git a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx index aa614df8ce4654808de5f8ef84499a6d821ff460..dee6d7e1c8fd43d13d46d18c41db2abe182cc676 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx @@ -87,17 +87,19 @@ class LiveResult extends Component { answers.map((obj) => { const pct = Math.round(obj.numVotes / numRespondents * 100); - return pollStats.push(<div className={styles.main} key={_.uniqueId('stats-')}> - <div className={styles.left}> - {obj.key} - </div> - <div className={styles.center}> - {obj.numVotes} - </div> - <div className={styles.right}> - {`${isNaN(pct) ? 0 : pct}%`} - </div> - </div>); + return pollStats.push( + <div className={styles.main} key={_.uniqueId('stats-')}> + <div className={styles.left}> + {obj.key} + </div> + <div className={styles.center}> + {obj.numVotes} + </div> + <div className={styles.right}> + {`${Number.isNaN(pct) ? 0 : pct}%`} + </div> + </div>, + ); }); return pollStats; @@ -117,8 +119,7 @@ class LiveResult extends Component { onClick={() => { publishPoll(); stopPoll(); - Session.set('isUserListOpen', true); - Session.set('isPollOpen', false); + Session.set('openPanel', 'userlist'); Session.set('forcePollOpen', false); }} label={intl.formatMessage(intlMessages.publishLabel)} @@ -134,8 +135,12 @@ class LiveResult extends Component { className={styles.btn} /> <div className={styles.container}> - <div className={styles.usersHeading}>{intl.formatMessage(intlMessages.usersTitle)}</div> - <div className={styles.responseHeading}>{intl.formatMessage(intlMessages.responsesTitle)}</div> + <div className={styles.usersHeading}> + {intl.formatMessage(intlMessages.usersTitle)} + </div> + <div className={styles.responseHeading}> + {intl.formatMessage(intlMessages.responsesTitle)} + </div> {this.renderAnswers()} </div> </div> @@ -149,4 +154,9 @@ LiveResult.propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, + getUser: PropTypes.func.isRequired, + currentPoll: PropTypes.arrayOf(Object).isRequired, + publishPoll: PropTypes.func.isRequired, + stopPoll: PropTypes.func.isRequired, + handleBackClick: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index b4cb9c1a48c6d9404a01bd66c1d4cf2ae2b283e6..8c7886e493d75949bd8246b8b14b4b5740201b35 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -45,9 +45,13 @@ const defaultProps = { shortcuts: '', }; -const toggleChatOpen = () => { - Session.set('isChatOpen', !Session.get('isChatOpen')); - Session.set('breakoutRoomIsOpen', false); +const handleClickToggleChat = (id) => { + Session.set( + 'openPanel', + Session.get('openPanel') === 'chat' && Session.get('idChatOpen') === id + ? 'userlist' : 'chat', + ); + Session.set('idChatOpen', id); }; const ChatListItem = (props) => { @@ -73,15 +77,7 @@ const ChatListItem = (props) => { aria-expanded={isCurrentChat} tabIndex={tabIndex} accessKey={isPublicChat(chat) ? TOGGLE_CHAT_PUB_AK : null} - onClick={() => { - toggleChatOpen(); - Session.set('idChatOpen', chat.id); - - if (Session.equals('isPollOpen', true)) { - Session.set('isPollOpen', false); - Session.set('forcePollOpen', true); - } - }} + onClick={() => handleClickToggleChat(chat.id)} id="chat-toggle-button" aria-label={isPublicChat(chat) ? intl.formatMessage(intlMessages.titlePublic) : chat.name} > diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index dbae3223c6cf128e43ac7f5119f273706d1541d9..ad5c6a5de483a3a9b3c9486d70ec35356700700d 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -41,11 +41,11 @@ const getCustomLogoUrl = () => Storage.getItem(CUSTOM_LOGO_URL_KEY); const sortUsersByName = (a, b) => { if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1; - } else if (a.name.toLowerCase() > b.name.toLowerCase()) { + } if (a.name.toLowerCase() > b.name.toLowerCase()) { return 1; - } else if (a.id.toLowerCase() > b.id.toLowerCase()) { + } if (a.id.toLowerCase() > b.id.toLowerCase()) { return -1; - } else if (a.id.toLowerCase() < b.id.toLowerCase()) { + } if (a.id.toLowerCase() < b.id.toLowerCase()) { return 1; } @@ -62,12 +62,12 @@ const sortUsersByEmoji = (a, b) => { if (emojiA && emojiB && (emojiA !== 'none' && emojiB !== 'none')) { if (a.emoji.changedAt < b.emoji.changedAt) { return -1; - } else if (a.emoji.changedAt > b.emoji.changedAt) { + } if (a.emoji.changedAt > b.emoji.changedAt) { return 1; } - } else if (emojiA && emojiA !== 'none') { + } if (emojiA && emojiA !== 'none') { return -1; - } else if (emojiB && emojiB !== 'none') { + } if (emojiB && emojiB !== 'none') { return 1; } return 0; @@ -76,9 +76,9 @@ const sortUsersByEmoji = (a, b) => { const sortUsersByModerator = (a, b) => { if (a.isModerator && b.isModerator) { return sortUsersByEmoji(a, b); - } else if (a.isModerator) { + } if (a.isModerator) { return -1; - } else if (b.isModerator) { + } if (b.isModerator) { return 1; } @@ -88,9 +88,9 @@ const sortUsersByModerator = (a, b) => { const sortUsersByPhoneUser = (a, b) => { if (!a.isPhoneUser && !b.isPhoneUser) { return 0; - } else if (!a.isPhoneUser) { + } if (!a.isPhoneUser) { return -1; - } else if (!b.isPhoneUser) { + } if (!b.isPhoneUser) { return 1; } @@ -101,7 +101,7 @@ const sortUsersByPhoneUser = (a, b) => { const sortUsersByCurrent = (a, b) => { if (a.isCurrent) { return -1; - } else if (b.isCurrent) { + } if (b.isCurrent) { return 1; } @@ -133,11 +133,11 @@ const sortUsers = (a, b) => { const sortChatsByName = (a, b) => { if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1; - } else if (a.name.toLowerCase() > b.name.toLowerCase()) { + } if (a.name.toLowerCase() > b.name.toLowerCase()) { return 1; - } else if (a.id.toLowerCase() > b.id.toLowerCase()) { + } if (a.id.toLowerCase() > b.id.toLowerCase()) { return -1; - } else if (a.id.toLowerCase() < b.id.toLowerCase()) { + } if (a.id.toLowerCase() < b.id.toLowerCase()) { return 1; } @@ -147,9 +147,9 @@ const sortChatsByName = (a, b) => { const sortChatsByIcon = (a, b) => { if (a.icon && b.icon) { return sortChatsByName(a, b); - } else if (a.icon) { + } if (a.icon) { return -1; - } else if (b.icon) { + } if (b.icon) { return 1; } @@ -436,6 +436,11 @@ const getGroupChatPrivate = (sender, receiver) => { } }; +const isUserModerator = (userId) => { + const u = Users.findOne({ userId }); + return u ? u.moderator : false; +}; + export default { setEmojiStatus, assignPresenter, @@ -457,7 +462,7 @@ export default { getCustomLogoUrl, getGroupChatPrivate, hasBreakoutRoom, + isUserModerator, getEmojiList: () => EMOJI_STATUSES, getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, }; - diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx index 5638e6ce9d2fb05eb41a9c1dca4a13e163d305ce..57eeb2d6f715c6aaf17e94d90cf5e7cfb23a2241 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { Session } from 'meteor/session'; import Icon from '/imports/ui/components/icon/component'; @@ -11,10 +12,12 @@ const intlMessages = defineMessages({ }, }); const toggleBreakoutPanel = () => { - const breakoutPanelState = Session.get('breakoutRoomIsOpen'); - Session.set('breakoutRoomIsOpen', !breakoutPanelState); - Session.set('isChatOpen', false); - Session.set('isPollOpen', false); + Session.set( + 'openPanel', + Session.get('openPanel') === 'breakoutroom' + ? 'userlist' + : 'breakoutroom', + ); }; const BreakoutRoomItem = ({ @@ -24,13 +27,17 @@ const BreakoutRoomItem = ({ if (hasBreakoutRoom) { return ( <div role="button" onClick={toggleBreakoutPanel}> - <h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()}</h2> + <h2 className={styles.smallTitle}> + {intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()} + </h2> <div className={styles.BreakoutRoomsItem}> <div className={styles.BreakoutRoomsContents}> - <div className={styles.BreakoutRoomsIcon} > + <div className={styles.BreakoutRoomsIcon}> <Icon iconName="rooms" /> </div> - <span className={styles.BreakoutRoomsText}>{intl.formatMessage(intlMessages.breakoutTitle)}</span> + <span className={styles.BreakoutRoomsText}> + {intl.formatMessage(intlMessages.breakoutTitle)} + </span> </div> </div> </div> @@ -40,3 +47,10 @@ const BreakoutRoomItem = ({ }; export default injectIntl(BreakoutRoomItem); + +BreakoutRoomItem.propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + hasBreakoutRoom: PropTypes.bool.isRequired, +}; 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 a5b0c31f514c8495e368bf6462b4f7d3666325a8..193691654ba29e33082ed3677e8fa633e045f210 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 @@ -72,12 +72,13 @@ class UserParticipants extends Component { } componentDidMount() { - if (!this.props.compact) { + const { compact, roving, users } = this.props; + if (!compact) { this.refScrollContainer.addEventListener( 'keydown', - event => this.props.roving( + event => roving( event, - this.props.users.length, + users.length, this.changeState, ), ); @@ -91,12 +92,13 @@ class UserParticipants extends Component { } componentDidUpdate(prevProps, prevState) { - if (this.state.index === -1) { + const { index } = this.state; + if (index === -1) { return; } - if (this.state.index !== prevState.index) { - this.focusUserItem(this.state.index); + if (index !== prevState.index) { + this.focusUserItem(index); } } @@ -129,44 +131,43 @@ class UserParticipants extends Component { const { meetingId } = meeting; - return users.map(u => - ( - <CSSTransition - classNames={listTransition} - appear - enter - exit - timeout={0} - component="div" - className={cx(styles.participantsList)} - key={u} - > - <div ref={(node) => { this.userRefs[index += 1] = node; }}> - <UserListItemContainer - {...{ - currentUser, - compact, - isBreakoutRoom, - meetingId, - getAvailableActions, - normalizeEmojiName, - isMeetingLocked, - handleEmojiChange, - getEmojiList, - getEmoji, - setEmojiStatus, - assignPresenter, - removeUser, - toggleVoice, - changeRole, - getGroupChatPrivate, - }} - userId={u} - getScrollContainerRef={this.getScrollContainerRef} - /> - </div> - </CSSTransition> - )); + return users.map(u => ( + <CSSTransition + classNames={listTransition} + appear + enter + exit + timeout={0} + component="div" + className={cx(styles.participantsList)} + key={u} + > + <div ref={(node) => { this.userRefs[index += 1] = node; }}> + <UserListItemContainer + {...{ + currentUser, + compact, + isBreakoutRoom, + meetingId, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + handleEmojiChange, + getEmojiList, + getEmoji, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + getGroupChatPrivate, + }} + userId={u} + getScrollContainerRef={this.getScrollContainerRef} + /> + </div> + </CSSTransition> + )); } focusUserItem(index) { @@ -181,28 +182,40 @@ class UserParticipants extends Component { render() { const { - intl, users, compact, setEmojiStatus, muteAllUsers, meeting, muteAllExceptPresenter, + intl, + users, + compact, + setEmojiStatus, + muteAllUsers, + meeting, + muteAllExceptPresenter, + currentUser, } = this.props; return ( <div className={styles.userListColumn}> { - !compact ? - <div className={styles.container}> - <h2 className={styles.smallTitle}> - {intl.formatMessage(intlMessages.usersTitle)} - ({users.length}) - - </h2> - <UserOptionsContainer {...{ - users, - muteAllUsers, - muteAllExceptPresenter, - setEmojiStatus, - meeting, - }} - /> - </div> + !compact + ? ( + <div className={styles.container}> + <h2 className={styles.smallTitle}> + {intl.formatMessage(intlMessages.usersTitle)} + ( + {users.length} +) + + </h2> + <UserOptionsContainer {...{ + users, + muteAllUsers, + muteAllExceptPresenter, + setEmojiStatus, + meeting, + currentUser, + }} + /> + </div> + ) : <hr className={styles.separator} /> } <div diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index 7fbcb87020aae5906e12d3eb94e1c2c1fcbc2032..d1dd3e02b15f6729fdc2022c973f124dca726930 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -131,38 +131,52 @@ class UserDropdown extends PureComponent { this.seperator = _.uniqueId('action-separator-'); } - componentDidUpdate(prevProps, prevState) { - if (!this.state.isActionsOpen && this.state.showNestedOptions) { + componentDidUpdate() { + const { isActionsOpen, showNestedOptions } = this.state; + + if (!isActionsOpen && showNestedOptions) { return this.resetMenuState(); } - this.checkDropdownDirection(); + return this.checkDropdownDirection(); } - makeDropdownItem(key, label, onClick, icon = null, iconRight = null) { - return ( - <DropdownListItem - {...{ - key, - label, - onClick, - icon, - iconRight, - }} - className={key === this.props.getEmoji ? styles.emojiSelected : null} - data-test={key} - /> - ); + onActionsShow() { + const { getScrollContainerRef } = this.props; + const dropdown = this.getDropdownMenuParent(); + const scrollContainer = getScrollContainerRef(); + + if (dropdown && scrollContainer) { + const dropdownTrigger = dropdown.children[0]; + const list = findDOMNode(this.list); + const children = [].slice.call(list.children); + children.find(child => child.getAttribute('role') === 'menuitem').focus(); + + this.setState({ + isActionsOpen: true, + dropdownVisible: false, + dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + dropdownDirection: 'top', + }); + + scrollContainer.addEventListener('scroll', this.handleScroll, false); + } } - resetMenuState() { - return this.setState({ + onActionsHide(callback) { + const { getScrollContainerRef } = this.props; + + this.setState({ isActionsOpen: false, - dropdownOffset: 0, - dropdownDirection: 'top', dropdownVisible: false, - showNestedOptions: false, }); + + const scrollContainer = getScrollContainerRef(); + scrollContainer.removeEventListener('scroll', this.handleScroll, false); + + if (callback) { + return callback; + } } getUsersActions() { @@ -182,6 +196,8 @@ class UserDropdown extends PureComponent { changeRole, } = this.props; + const { showNestedOptions } = this.state; + const actionPermissions = getAvailableActions(currentUser, user, isBreakoutRoom); const actions = []; @@ -197,7 +213,7 @@ class UserDropdown extends PureComponent { allowedToChangeStatus, } = actionPermissions; - if (this.state.showNestedOptions) { + if (showNestedOptions) { if (allowedToChangeStatus) { actions.push(this.makeDropdownItem( 'back', @@ -236,12 +252,8 @@ class UserDropdown extends PureComponent { intl.formatMessage(messages.ChatLabel), () => { getGroupChatPrivate(currentUser, user); - if (Session.equals('isPollOpen', true)) { - Session.set('isPollOpen', false); - Session.set('forcePollOpen', true); - } + Session.set('openPanel', 'chat'); Session.set('idChatOpen', user.id); - Session.set('isChatOpen', true); }, 'chat', )); @@ -313,44 +325,37 @@ class UserDropdown extends PureComponent { return actions; } - onActionsShow() { - const dropdown = this.getDropdownMenuParent(); - const scrollContainer = this.props.getScrollContainerRef(); - - if (dropdown && scrollContainer) { - const dropdownTrigger = dropdown.children[0]; - const list = findDOMNode(this.list); - const children = [].slice.call(list.children); - children.find(child => child.getAttribute('role') === 'menuitem').focus(); - - this.setState({ - isActionsOpen: true, - dropdownVisible: false, - dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, - dropdownDirection: 'top', - }); + getDropdownMenuParent() { + return findDOMNode(this.dropdown); + } - scrollContainer.addEventListener('scroll', this.handleScroll, false); - } + makeDropdownItem(key, label, onClick, icon = null, iconRight = null) { + const { getEmoji } = this.props; + return ( + <DropdownListItem + {...{ + key, + label, + onClick, + icon, + iconRight, + }} + className={key === getEmoji ? styles.emojiSelected : null} + data-test={key} + /> + ); } - onActionsHide(callback) { - this.setState({ + resetMenuState() { + return this.setState({ isActionsOpen: false, + dropdownOffset: 0, + dropdownDirection: 'top', dropdownVisible: false, + showNestedOptions: false, }); - - const scrollContainer = this.props.getScrollContainerRef(); - scrollContainer.removeEventListener('scroll', this.handleScroll, false); - - if (callback) { - return callback; - } } - getDropdownMenuParent() { - return findDOMNode(this.dropdown); - } handleScroll() { this.setState({ isActionsOpen: false }); @@ -360,12 +365,13 @@ class UserDropdown extends PureComponent { * Check if the dropdown is visible, if so, check if should be draw on top or bottom direction. */ checkDropdownDirection() { + const { getScrollContainerRef } = this.props; if (this.isDropdownActivedByUser()) { const dropdown = this.getDropdownMenuParent(); const dropdownTrigger = dropdown.children[0]; const dropdownContent = dropdown.children[1]; - const scrollContainer = this.props.getScrollContainerRef(); + const scrollContainer = getScrollContainerRef(); const nextState = { dropdownVisible: true, @@ -377,7 +383,7 @@ class UserDropdown extends PureComponent { ); if (!isDropdownVisible) { - const offsetPageTop = ((dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop); + const offsetPageTop = (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop; nextState.dropdownOffset = window.innerHeight - offsetPageTop; nextState.dropdownDirection = 'bottom'; @@ -449,8 +455,8 @@ class UserDropdown extends PureComponent { const userItemContentsStyle = {}; userItemContentsStyle[styles.dropdown] = true; - userItemContentsStyle[styles.userListItem] = !this.state.isActionsOpen; - userItemContentsStyle[styles.usertListItemWithMenu] = this.state.isActionsOpen; + userItemContentsStyle[styles.userListItem] = !isActionsOpen; + userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen; const you = (user.isCurrent) ? intl.formatMessage(messages.you) : ''; @@ -503,7 +509,7 @@ class UserDropdown extends PureComponent { return ( <Dropdown ref={(ref) => { this.dropdown = ref; }} - isOpen={this.state.isActionsOpen} + isOpen={isActionsOpen} onShow={this.onActionsShow} onHide={this.onActionsHide} className={userItemContentsStyle} 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 6e26614b16501532a602db95d0fe9cc8b4a738fe..0db0a225fdb68c2733af006b26b875f7e7561395 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 @@ -20,6 +20,7 @@ const propTypes = { toggleMuteAllUsers: PropTypes.func.isRequired, toggleMuteAllUsersExceptPresenter: PropTypes.func.isRequired, toggleStatus: PropTypes.func.isRequired, + mountModal: PropTypes.func.isRequired, }; const intlMessages = defineMessages({ @@ -83,7 +84,14 @@ class UserOptions extends PureComponent { } componentWillMount() { - const { intl, isMeetingMuted, mountModal } = this.props; + const { + intl, + isMeetingMuted, + mountModal, + toggleStatus, + toggleMuteAllUsers, + toggleMuteAllUsersExceptPresenter, + } = this.props; this.menuItems = _.compact([ (<DropdownListItem @@ -91,21 +99,21 @@ class UserOptions extends PureComponent { icon="clear_status" label={intl.formatMessage(intlMessages.clearAllLabel)} description={intl.formatMessage(intlMessages.clearAllDesc)} - onClick={this.props.toggleStatus} + onClick={toggleStatus} />), (<DropdownListItem key={_.uniqueId('list-item-')} icon="mute" label={intl.formatMessage(intlMessages.muteAllLabel)} description={intl.formatMessage(intlMessages.muteAllDesc)} - onClick={this.props.toggleMuteAllUsers} + onClick={toggleMuteAllUsers} />), (<DropdownListItem key={_.uniqueId('list-item-')} icon="mute" label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} - onClick={this.props.toggleMuteAllUsersExceptPresenter} + onClick={toggleMuteAllUsersExceptPresenter} />), (<DropdownListItem key={_.uniqueId('list-item-')} @@ -122,7 +130,8 @@ class UserOptions extends PureComponent { } componentDidUpdate(prevProps) { - if (prevProps.isMeetingMuted !== this.props.isMeetingMuted) { + const { isMeetingMuted } = this.props; + if (prevProps.isMeetingMuted !== isMeetingMuted) { this.alterMenu(); } } @@ -140,16 +149,23 @@ class UserOptions extends PureComponent { } alterMenu() { - const { intl, isMeetingMuted } = this.props; + const { + intl, + isMeetingMuted, + toggleMuteAllUsers, + toggleMuteAllUsersExceptPresenter, + } = this.props; if (isMeetingMuted) { - const menuButton = (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="unmute" - label={intl.formatMessage(intlMessages.unmuteAllLabel)} - description={intl.formatMessage(intlMessages.unmuteAllDesc)} - onClick={this.props.toggleMuteAllUsers} - />); + const menuButton = ( + <DropdownListItem + key={_.uniqueId('list-item-')} + icon="unmute" + label={intl.formatMessage(intlMessages.unmuteAllLabel)} + description={intl.formatMessage(intlMessages.unmuteAllDesc)} + onClick={toggleMuteAllUsers} + /> + ); this.menuItems.splice(1, 2, menuButton); } else { const muteMeetingButtons = [(<DropdownListItem @@ -157,13 +173,13 @@ class UserOptions extends PureComponent { icon="mute" label={intl.formatMessage(intlMessages.muteAllLabel)} description={intl.formatMessage(intlMessages.muteAllDesc)} - onClick={this.props.toggleMuteAllUsers} + onClick={toggleMuteAllUsers} />), (<DropdownListItem key={_.uniqueId('list-item-')} icon="mute" label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} - onClick={this.props.toggleMuteAllUsersExceptPresenter} + onClick={toggleMuteAllUsersExceptPresenter} />)]; this.menuItems.splice(1, 1, muteMeetingButtons[0], muteMeetingButtons[1]); @@ -172,12 +188,13 @@ class UserOptions extends PureComponent { render() { const { intl } = this.props; + const { isUserOptionsOpen } = this.state; return ( <Dropdown ref={(ref) => { this.dropdown = ref; }} autoFocus={false} - isOpen={this.state.isUserOptionsOpen} + isOpen={isUserOptionsOpen} onShow={this.onActionsShow} onHide={this.onActionsHide} className={styles.dropdown} 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 70cffbac78e99a60a3659300add2dc7af1c1444a..24d71cfeff48042756b4a29637520d2aa5f2f5af 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,8 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import Auth from '/imports/ui/services/auth'; -import mapUser from '/imports/ui/services/user/mapUser'; -import Users from '/imports/api/users/'; import UserOptions from './component'; @@ -31,43 +29,37 @@ export default class UserOptionsContainer extends PureComponent { muteMeeting() { const { muteAllUsers } = this.props; - const currentUser = Users.findOne({ userId: Auth.userID }); - - muteAllUsers(currentUser.userId); + muteAllUsers(Auth.userID); } muteAllUsersExceptPresenter() { const { muteAllExceptPresenter } = this.props; - const currentUser = Users.findOne({ userId: Auth.userID }); - - muteAllExceptPresenter(currentUser.userId); + muteAllExceptPresenter(Auth.userID); } handleClearStatus() { const { users, setEmojiStatus } = this.props; - users.forEach((user) => { - if (user.emoji.status !== 'none') { - setEmojiStatus(user.id, 'none'); - } + users.forEach((id) => { + setEmojiStatus(id, 'none'); }); } render() { - const currentUser = Users.findOne({ userId: Auth.userID }); - const currentUserIsModerator = mapUser(currentUser).isModerator; - const { meeting } = this.props; + const { currentUser } = this.props; + const currentUserIsModerator = currentUser.isModerator; - this.state.meetingMuted = meeting.voiceProp.muteOnStart; + const { meetingMuted } = this.state; return ( - currentUserIsModerator ? - <UserOptions - toggleMuteAllUsers={this.muteMeeting} - toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter} - toggleStatus={this.handleClearStatus} - isMeetingMuted={this.state.meetingMuted} - /> : null + currentUserIsModerator + ? ( + <UserOptions + toggleMuteAllUsers={this.muteMeeting} + toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter} + toggleStatus={this.handleClearStatus} + isMeetingMuted={meetingMuted} + />) : null ); } } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx index f0776f82536b937f6f9ffec1c3ef4d8856b4c6c0..c0754d72f86ae995d1d9b22798429cc1d2897826 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import _ from 'lodash'; +import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import Icon from '/imports/ui/components/icon/component'; import { Session } from 'meteor/session'; @@ -14,6 +14,15 @@ const intlMessages = defineMessages({ class UserPolls extends PureComponent { render() { + const handleClickTogglePoll = () => { + Session.set( + 'openPanel', + Session.get('openPanel') === 'poll' + ? 'userlist' + : 'poll', + ); + }; + const { intl, isPresenter, @@ -36,17 +45,10 @@ class UserPolls extends PureComponent { role="button" tabIndex={0} className={styles.pollLink} - onClick={() => { - Session.set('isChatOpen', false); - Session.set('breakoutRoomIsOpen', false); - - return Session.equals('isPollOpen', true) - ? Session.set('isPollOpen', false) - : Session.set('isPollOpen', true); - }} + onClick={handleClickTogglePoll} > <Icon iconName="polling" className={styles.icon} /> - <span className={styles.label} >{intl.formatMessage(intlMessages.pollLabel)}</span> + <span className={styles.label}>{intl.formatMessage(intlMessages.pollLabel)}</span> </div> </div> </div> @@ -55,3 +57,12 @@ class UserPolls extends PureComponent { } export default injectIntl(UserPolls); + +UserPolls.propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + isPresenter: PropTypes.bool.isRequired, + pollIsOpen: PropTypes.bool.isRequired, + forcePollOpen: PropTypes.bool.isRequired, +}; diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx index afcbab35d91f317622f5274a71b8f4005ad75540..2dbb9517d8318f4ab035381cd8855894cf1bdedb 100644 --- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx @@ -49,9 +49,25 @@ const intlMessages = defineMessages({ id: 'app.videoPreview.webcamNotFoundLabel', description: 'Webcam not found label', }, - sharingError: { - id: 'app.video.sharingError', - description: 'Error on sharing webcam', + permissionError: { + id: 'app.video.permissionError', + description: 'Error message for webcam permission', + }, + NotFoundError: { + id: 'app.video.notFoundError', + description: 'error message when can not get webcam video', + }, + NotAllowedError: { + id: 'app.video.notAllowed', + description: 'error message when webcam had permission denied', + }, + NotSupportedError: { + id: 'app.video.notSupportedError', + description: 'error message when origin do not have ssl valid', + }, + NotReadableError: { + id: 'app.video.notReadableError', + description: 'error message When the webcam is being used by other software', }, }); @@ -91,6 +107,16 @@ class VideoPreview extends Component { } } + handlegUMError(error) { + const { + intl, + } = this.props; + const errorMessage = intlMessages[error.name] + || intlMessages.permissionError; + notify(intl.formatMessage(errorMessage), 'error', 'video'); + logger.error(error); + } + handleSelectWebcam(event) { const { intl, @@ -108,8 +134,7 @@ class VideoPreview extends Component { this.video.srcObject = stream; this.deviceStream = stream; }).catch((error) => { - notify(intl.formatMessage(intlMessages.sharingError), 'error', 'video'); - logger.error(error); + this.handlegUMError(error); }); } @@ -156,8 +181,9 @@ class VideoPreview extends Component { this.setState({ availableWebcams: webcams }); } }); - }).catch(() => { + }).catch((error) => { this.setState({ isStartSharingDisabled: true }); + this.handlegUMError(error); }); } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index e00a6df51d591da9e62bf1f62fceec1466ae22e8..c33607a68abd55449e9e9c875a703fa27c87ca8e 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -17,10 +17,6 @@ const intlClientErrors = defineMessages({ id: 'app.video.iceCandidateError', description: 'Error message for ice candidate fail', }, - sharingError: { - id: 'app.video.sharingError', - description: 'Error on sharing webcam', - }, chromeExtensionError: { id: 'app.video.chromeExtensionError', description: 'Error message for Chrome Extension not installed', @@ -53,6 +49,10 @@ const intlClientErrors = defineMessages({ id: 'app.video.iceConnectionStateError', description: 'Error message for ice connection state being failed', }, + mediaFlowTimeout: { + id: 'app.video.mediaFlowTimeout1020', + description: 'Error message when media could not go through the server within the specified period', + }, }); const intlSFUErrors = defineMessages({ @@ -62,7 +62,7 @@ const intlSFUErrors = defineMessages({ }, 2001: { id: 'app.sfu.mediaServerOffline2001', - description: 'error message when kurento is offline', + description: 'error message when SFU is offline', }, 2002: { id: 'app.sfu.mediaServerNoResources2002', @@ -80,6 +80,10 @@ const intlSFUErrors = defineMessages({ id: 'app.sfu.serverIceStateFailed2022', description: 'Error message fired when the server endpoint transitioned to a FAILED ICE state', }, + 2200: { + id: 'app.sfu.mediaGenericError2200', + description: 'Error message fired when the SFU component generated a generic error', + }, 2202: { id: 'app.sfu.invalidSdp2202', description: 'Error message fired when the clients provides an invalid SDP', @@ -116,6 +120,9 @@ class VideoProvider extends Component { this.videoTags = {}; this.sharedWebcam = false; + this.createVideoTag = this.createVideoTag.bind(this); + this.getStats = this.getStats.bind(this); + this.stopGettingStats = this.stopGettingStats.bind(this); this.onWsOpen = this.onWsOpen.bind(this); this.onWsClose = this.onWsClose.bind(this); this.onWsMessage = this.onWsMessage.bind(this); @@ -129,46 +136,10 @@ class VideoProvider extends Component { this.customGetStats = this.customGetStats.bind(this); } - logger(type, message, options = {}) { - const { userId, userName } = this.props; - const topic = options.topic || 'video'; - - logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`); - } - - _sendPauseStream(id, role, state) { - this.sendMessage({ - cameraId: id, - id: 'pause', - type: 'video', - role, - state, - }); - } - - pauseViewers() { - this.logger('debug', 'Calling pause in viewer streams'); - - Object.keys(this.webRtcPeers).forEach((id) => { - if (this.props.userId !== id && this.webRtcPeers[id].started) { - this._sendPauseStream(id, 'viewer', true); - } - }); - } - - unpauseViewers() { - this.logger('debug', 'Calling un-pause in viewer streams'); - - Object.keys(this.webRtcPeers).forEach((id) => { - if (id !== this.props.userId && this.webRtcPeers[id].started) { - this._sendPauseStream(id, 'viewer', false); - } - }); - } componentWillMount() { - this.ws.addEventListener('open', this.onWsOpen); - this.ws.addEventListener('close', this.onWsClose); + this.ws.onopen = this.onWsOpen; + this.ws.onclose = this.onWsClose; window.addEventListener('online', this.openWs); window.addEventListener('offline', this.onWsClose); @@ -177,7 +148,7 @@ class VideoProvider extends Component { componentDidMount() { document.addEventListener('joinVideo', this.shareWebcam); // TODO find a better way to do this document.addEventListener('exitVideo', this.unshareWebcam); - this.ws.addEventListener('message', this.onWsMessage); + this.ws.onmessage = this.onWsMessage; window.addEventListener('beforeunload', this.unshareWebcam); this.visibility.onVisible(this.unpauseViewers); @@ -199,9 +170,9 @@ class VideoProvider extends Component { document.removeEventListener('joinVideo', this.shareWebcam); document.removeEventListener('exitVideo', this.unshareWebcam); - this.ws.removeEventListener('message', this.onWsMessage); - this.ws.removeEventListener('open', this.onWsOpen); - this.ws.removeEventListener('close', this.onWsClose); + this.ws.onmessage = null; + this.ws.onopen = null; + this.ws.onclose = null; window.removeEventListener('online', this.openWs); window.removeEventListener('offline', this.onWsClose); @@ -222,35 +193,6 @@ class VideoProvider extends Component { this.ws.close(); } - onWsOpen() { - this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' }); - - // -- Resend queued messages that happened when socket was not connected - while (this.wsQueue.length > 0) { - this.sendMessage(this.wsQueue.pop()); - } - - this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL); - - this.setState({ socketOpen: true }); - } - - onWsClose(error) { - this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' }); - - this.stopWebRTCPeer(this.props.userId); - clearInterval(this.pingInterval); - - this.setState({ socketOpen: false }); - } - - ping() { - const message = { - id: 'ping', - }; - this.sendMessage(message); - } - onWsMessage(msg) { const parsedMessage = JSON.parse(msg.data); @@ -274,7 +216,6 @@ class VideoProvider extends Component { break; case 'pong': - this.logger('debug', 'Received pong from server', { topic: 'ws' }); break; case 'error': @@ -284,6 +225,100 @@ class VideoProvider extends Component { } } + onWsClose() { + const { + intl, + } = this.props; + + this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' }); + + clearInterval(this.pingInterval); + + if (this.sharedWebcam) { + this.unshareWebcam(); + } + // Notify user that the SFU component has gone offline + this.notifyError(intl.formatMessage(intlSFUErrors[2001])); + + this.setState({ socketOpen: false }); + } + + onWsOpen() { + this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' }); + + // -- Resend queued messages that happened when socket was not connected + while (this.wsQueue.length > 0) { + this.sendMessage(this.wsQueue.pop()); + } + + this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL); + + this.setState({ socketOpen: true }); + } + + getStats(id, video, callback) { + const peer = this.webRtcPeers[id]; + + const hasLocalStream = peer && peer.started === true + && peer.peerConnection.getLocalStreams().length > 0; + const hasRemoteStream = peer && peer.started === true + && peer.peerConnection.getRemoteStreams().length > 0; + + if (hasLocalStream) { + this.monitorTrackStart(peer.peerConnection, + peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], true, callback); + } else if (hasRemoteStream) { + this.monitorTrackStart(peer.peerConnection, + peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], false, callback); + } + } + + logger(type, message, options = {}) { + const { userId, userName } = this.props; + const topic = options.topic || 'video'; + + logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`); + } + + _sendPauseStream(id, role, state) { + this.sendMessage({ + cameraId: id, + id: 'pause', + type: 'video', + role, + state, + }); + } + + pauseViewers() { + const { userId } = this.props; + this.logger('debug', 'Calling pause in viewer streams'); + + Object.keys(this.webRtcPeers).forEach((id) => { + if (userId !== id && this.webRtcPeers[id].started) { + this._sendPauseStream(id, 'viewer', true); + } + }); + } + + unpauseViewers() { + const { userId } = this.props; + this.logger('debug', 'Calling un-pause in viewer streams'); + + Object.keys(this.webRtcPeers).forEach((id) => { + if (id !== userId && this.webRtcPeers[id].started) { + this._sendPauseStream(id, 'viewer', false); + } + }); + } + + ping() { + const message = { + id: 'ping', + }; + this.sendMessage(message); + } + sendMessage(message) { const { ws } = this; @@ -347,7 +382,7 @@ class VideoProvider extends Component { } } - stopWebRTCPeer(id) { + stopWebRTCPeer(id, restarting = false) { this.logger('info', 'Stopping webcam', { cameraId: id }); const { userId } = this.props; const shareWebcam = id === userId; @@ -369,9 +404,16 @@ class VideoProvider extends Component { cameraId: id, }); - // Clear the shared camera fail timeout when destroying - clearTimeout(this.restartTimeout[id]); - delete this.restartTimeout[id]; + // Clear the shared camera media flow timeout when destroying it + if (!restarting) { + if (this.restartTimeout[id]) { + clearTimeout(this.restartTimeout[id]); + } + + if (this.restartTimer[id]) { + delete this.restartTimer[id]; + } + } this.destroyWebRTCPeer(id); } @@ -393,6 +435,11 @@ class VideoProvider extends Component { const { meetingId, sessionToken, voiceBridge } = this.props; let iceServers = []; + // Check if there's connectivity to the SFU component + if (!this.connectedToMediaServer()) { + return this._webRTCOnError(2001, id, shareWebcam); + } + // Check if the peer is already being processed if (this.webRtcPeers[id]) { return; @@ -467,23 +514,25 @@ class VideoProvider extends Component { }); }); if (this.webRtcPeers[id].peerConnection) { - this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = - this._getOnIceConnectionStateChangeCallback(id); + this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id); } } } - _getWebRTCStartTimeout(id, shareWebcam, peer) { - const { intl } = this.props; + _getWebRTCStartTimeout(id, shareWebcam) { + const { intl, userId } = this.props; return () => { this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, { cameraId: id }); - if (this.props.userId === id) { - this.notifyError(intl.formatMessage(intlClientErrors.sharingError)); - this.stopWebRTCPeer(id); + if (userId === id) { + this.notifyError(intl.formatMessage(intlClientErrors.mediaFlowTimeout)); + this.stopWebRTCPeer(id, false); } else { - this.stopWebRTCPeer(id); + // Subscribers try to reconnect according to their timers if media could + // not reach the server. That's why we pass the restarting flag as true + // to the stop procedure as to not destroy the timers + this.stopWebRTCPeer(id, true); this.createWebRTCPeer(id, shareWebcam); // Increment reconnect interval @@ -508,19 +557,13 @@ class VideoProvider extends Component { } } - _webRTCOnError(error, id, shareWebcam) { + _webRTCOnError(error, id) { const { intl } = this.props; this.logger('error', ' WebRTC peerObj create error', id); - this.logger('error', error, id); const errorMessage = intlClientErrors[error.name] - || intlClientErrors.permissionError; + || intlSFUErrors[error] || intlClientErrors.permissionError; this.notifyError(intl.formatMessage(errorMessage)); - /* This notification error is displayed considering kurento-utils - * returned the error 'The request is not allowed by the user agent - * or the platform in the current context.', but there are other - * errors that could be returned. */ - this.stopWebRTCPeer(id); return this.logger('error', errorMessage, { cameraId: id }); @@ -555,7 +598,7 @@ class VideoProvider extends Component { const { intl } = this.props; const peer = this.webRtcPeers[id]; - return (event) => { + return () => { const connectionState = peer.peerConnection.iceConnectionState; if (connectionState === 'failed' || connectionState === 'closed') { // prevent the same error from being detected multiple times @@ -569,6 +612,7 @@ class VideoProvider extends Component { } attachVideoStream(id) { + const { userId } = this.props; const video = this.videoTags[id]; if (video == null) { this.logger('warn', 'Peer', id, 'has not been started yet'); @@ -580,7 +624,7 @@ class VideoProvider extends Component { return; // Skip if the stream is already attached } - const isCurrent = id === this.props.userId; + const isCurrent = id === userId; const peer = this.webRtcPeers[id]; const attachVideoStreamHelper = () => { @@ -615,8 +659,9 @@ class VideoProvider extends Component { } } - customGetStats(peer, mediaStreamTrack, callback, interval) { - const statsState = this.state.stats; + customGetStats(peer, mediaStreamTrack, callback) { + const { stats } = this.state; + const statsState = stats; let promise; try { promise = peer.getStats(mediaStreamTrack); @@ -627,9 +672,9 @@ class VideoProvider extends Component { let videoInOrOutbound = {}; results.forEach((res) => { if (res.type === 'ssrc' || res.type === 'inbound-rtp' || res.type === 'outbound-rtp') { - res.packetsSent = parseInt(res.packetsSent); - res.packetsLost = parseInt(res.packetsLost) || 0; - res.packetsReceived = parseInt(res.packetsReceived); + res.packetsSent = parseInt(res.packetsSent, 10); + res.packetsLost = parseInt(res.packetsLost, 10) || 0; + res.packetsReceived = parseInt(res.packetsReceived, 10); if ((isNaN(res.packetsSent) && res.packetsReceived === 0) || (res.type === 'outbound-rtp' && res.isRemote)) { @@ -637,11 +682,11 @@ class VideoProvider extends Component { } if (res.googFrameWidthReceived) { - res.width = parseInt(res.googFrameWidthReceived); - res.height = parseInt(res.googFrameHeightReceived); + res.width = parseInt(res.googFrameWidthReceived, 10); + res.height = parseInt(res.googFrameHeightReceived, 10); } else if (res.googFrameWidthSent) { - res.width = parseInt(res.googFrameWidthSent); - res.height = parseInt(res.googFrameHeightSent); + res.width = parseInt(res.googFrameWidthSent, 10); + res.height = parseInt(res.googFrameHeightSent, 10); } // Extra fields available on Chrome @@ -692,11 +737,14 @@ class VideoProvider extends Component { const videoKbitsReceivedPerSecond = (videoIntervalBytesReceived * 8) / videoReceivedInterval; const videoKbitsSentPerSecond = (videoIntervalBytesSent * 8) / videoSentInterval; - const videoPacketDuration = (videoIntervalPacketsSent / videoSentInterval) * 1000; - let videoLostPercentage, - videoLostRecentPercentage, - videoBitrate; + let videoLostPercentage; + + + let videoLostRecentPercentage; + + + let videoBitrate; if (videoStats.packetsReceived > 0) { // Remote video videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived) * 100)) || 0).toFixed(1); videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0); @@ -775,24 +823,13 @@ class VideoProvider extends Component { } } - getStats(id, video, callback) { - const peer = this.webRtcPeers[id]; - - const hasLocalStream = peer && peer.started === true && peer.peerConnection.getLocalStreams().length > 0; - const hasRemoteStream = peer && peer.started === true && peer.peerConnection.getRemoteStreams().length > 0; - - if (hasLocalStream) { - this.monitorTrackStart(peer.peerConnection, peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], true, callback); - } else if (hasRemoteStream) { - this.monitorTrackStart(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], false, callback); - } - } - stopGettingStats(id) { const peer = this.webRtcPeers[id]; - const hasLocalStream = peer && peer.started === true && peer.peerConnection.getLocalStreams().length > 0; - const hasRemoteStream = peer && peer.started === true && peer.peerConnection.getRemoteStreams().length > 0; + const hasLocalStream = peer && peer.started === true + && peer.peerConnection.getLocalStreams().length > 0; + const hasRemoteStream = peer && peer.started === true + && peer.peerConnection.getRemoteStreams().length > 0; if (hasLocalStream) { this.monitorTrackStop(peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0].id); @@ -811,9 +848,9 @@ class VideoProvider extends Component { handlePlayStart(message) { const id = message.cameraId; const peer = this.webRtcPeers[id]; - const videoTag = this.videoTags[id]; if (peer) { + const { userId } = this.props; this.logger('info', 'Handle play start for camera', { cameraId: id }); // Clear camera shared timeout when camera succesfully starts @@ -827,7 +864,7 @@ class VideoProvider extends Component { this.attachVideoStream(id); } - if (id === this.props.userId) { + if (id === userId) { VideoService.sendUserShareWebcam(id); VideoService.joinedVideo(); } @@ -844,7 +881,7 @@ class VideoProvider extends Component { if (message.streamId === userId) { this.unshareWebcam(); this.notifyError(intl.formatMessage(intlSFUErrors[code] - || intlClientErrors.sharingError)); + || intlSFUErrors[2200])); } else { this.stopWebRTCPeer(message.cameraId); } @@ -857,36 +894,34 @@ class VideoProvider extends Component { } shareWebcam() { - const { intl } = this.props; - if (this.connectedToMediaServer()) { this.logger('info', 'Sharing webcam'); this.sharedWebcam = true; VideoService.joiningVideo(); - } else { - this.logger('debug', 'Error on sharing webcam'); - this.notifyError(intl.formatMessage(intlClientErrors.sharingError)); } } unshareWebcam() { + const { userId } = this.props; this.logger('info', 'Unsharing webcam'); - VideoService.sendUserUnshareWebcam(this.props.userId); + VideoService.sendUserUnshareWebcam(userId); VideoService.exitedVideo(); this.sharedWebcam = false; } render() { - if (!this.state.socketOpen) return null; + const { socketOpen } = this.state; + if (!socketOpen) return null; + const { users, enableVideoStats } = this.props; return ( <VideoList - users={this.props.users} - onMount={this.createVideoTag.bind(this)} - getStats={this.getStats.bind(this)} - stopGettingStats={this.stopGettingStats.bind(this)} - enableVideoStats={this.props.enableVideoStats} + users={users} + onMount={this.createVideoTag} + getStats={this.getStats} + stopGettingStats={this.stopGettingStats} + enableVideoStats={enableVideoStats} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx index eb6bbd51d1bc99f4e58ee856baa8451bd36b963e..2fea564c46643a04980b66aa7d7a0082dc38369a 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx @@ -4,8 +4,10 @@ import getFromUserSettings from '/imports/ui/services/users-settings'; import VideoProvider from './component'; import VideoService from './service'; -const VideoProviderContainer = ({ children, ...props }) => - (!props.users.length ? null : <VideoProvider {...props}>{children}</VideoProvider>); +const VideoProviderContainer = ({ children, ...props }) => { + const { users } = props; + return (!users.length ? null : <VideoProvider {...props}>{children}</VideoProvider>); +}; export default withTracker(() => ({ meetingId: VideoService.meetingId(), diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 9e3f0f33b74d1af92b06baac615844311098204d..99d532da9411c3811418e7e3f5d8acaf00ac04a8 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -5,7 +5,6 @@ import Meetings from '/imports/api/meetings/'; import Users from '/imports/api/users/'; import mapUser from '/imports/ui/services/user/mapUser'; import UserListService from '/imports/ui/components/user-list/service'; -import SessionStorage from '/imports/ui/services/storage/session'; class VideoService { constructor() { @@ -100,8 +99,8 @@ class VideoService { } webcamOnlyModerator() { - const m = Meetings.findOne({ meetingId: Auth.meetingID }); - return m.usersProp.webcamsOnlyForModerator; + const m = Meetings.findOne({ meetingId: Auth.meetingID }) || {}; + return m.usersProp ? m.usersProp.webcamsOnlyForModerator : false; } isLocked() { @@ -127,8 +126,8 @@ class VideoService { } voiceBridge() { - const voiceBridge = Meetings.findOne({ meetingId: Auth.meetingID }).voiceProp.voiceConf; - return voiceBridge; + const m = Meetings.findOne({ meetingId: Auth.meetingID }) || {}; + return m.voiceProp ? m.voiceProp.voiceConf : null; } isConnected() { diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 602cf490788642546036650b98c600d5e87c50ae..885ad442ed3e1bd91c38c26e2cd1b65be58d628c 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -387,6 +387,7 @@ "app.video.notAllowed": "Missing permission for share webcam, please make sure your browser permissions", "app.video.notSupportedError": "Can share webcam video only with safe sources, make sure your SSL certificate is valid", "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ", + "app.video.mediaFlowTimeout1020": "Error 1020: media could not reach the server", "app.video.swapCam": "Swap", "app.video.swapCamDesc": "swap the direction of webcams", "app.video.videoMenu": "Video menu", @@ -414,6 +415,7 @@ "app.sfu.mediaServerRequestTimeout2003": "Error 2003: Media server requests are timing out", "app.sfu.serverIceGatheringFailed2021": "Error 2021: Media server cannot gather ICE candidates", "app.sfu.serverIceGatheringFailed2022": "Error 2022: Media server ICE connection failed", + "app.sfu.mediaGenericError2200": "Error 2200: Media server failed to process request", "app.sfu.invalidSdp2202":"Error 2202: Client generated an invalid SDP", "app.sfu.noAvailableCodec2203": "Error 2203: Server could not find an appropriate codec", "app.meeting.endNotification.ok.label": "OK", diff --git a/labs/api/meetings-vx/.gitignore b/labs/api/meetings-vx/.gitignore deleted file mode 100644 index 3c3629e647f5ddf82548912e337bea9826b434af..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/labs/api/meetings-vx/Cakefile b/labs/api/meetings-vx/Cakefile deleted file mode 100755 index f71c67698ab0068f2da4789affbcb80f19950e1d..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/Cakefile +++ /dev/null @@ -1,65 +0,0 @@ -fs = require 'fs' -{print} = require 'util' -{spawn, exec} = require 'child_process' -glob = require 'glob' - -REPORTER = "min" - -config = {} -config.binPath = './node_modules/.bin/' - -# cake test # run all tests -# cake -f test/lib/file.coffee test # run the files passed -# cake -b test # run all tests and stop at first failure -option '-f', '--file [FILE*]', 'input file(s)' -option '-b', '--bail', 'bail' -task 'test', 'Run the test suite', (options) -> - process.env.NODE_ENV = "test" - testFiles = [ - - ] - testOpts = [ - '--require', 'coffee-script/register', - '--compilers', 'coffee:coffee-script/register', - '--require', 'should', - '--colors', - '--ignore-leaks', - '--timeout', '15000', - '--reporter', 'spec' - ] - if options.bail? and options.bail - testOpts = testOpts.concat('-b') - - if options.file? - if _.isArray(options.file) - files = testFiles.concat(options.file) - else - files = testFiles.concat([options.file]) - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - - else - glob 'test/**/*.coffee', (error, files) -> - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - -# Internal methods - -# Spawns an application with `options` and calls `onExit` -# when it finishes. -run = (bin, options, onExit) -> - bin = config.binPath + bin - console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "") - cmd = spawn bin, options - cmd.stdout.on 'data', (data) -> print data.toString() - cmd.stderr.on 'data', (data) -> print data.toString() - cmd.on 'exit', (code) -> - console.log 'done.' - onExit?(code, options) - -# Returns a string with the current time to print out. -timeNow = -> - today = new Date() - today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() \ No newline at end of file diff --git a/labs/api/meetings-vx/README.md b/labs/api/meetings-vx/README.md deleted file mode 100644 index 17d99532d724a542d2d87348aa400e4dc16d709f..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/README.md +++ /dev/null @@ -1,41 +0,0 @@ -exploringHapi -============= - -This was used as a playground for attempts to validate URL parameters -and to calculate and compare checksum - -Keywords: hapi, joi, OAuth, checksum, hmac_sha1 - -Instructions: -============= -from Terminal: -$ coffee index.coffee -Listening on http://x.x.x.x:PORT - -go to the browser, open an MCONF API-MATE window -modify the "server"(id="input-custom-server-url") field to http://x.x.x.x:PORT -click on the link for creating a meeting ("create ...") - -In the Terminal window you should see something like: -the checksum from url is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a -YAY! They match! - -or - -the checksum from url is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is -dkfjhdkjfhlkafhdfklahfkfhfjhkgfeq349492a - -The browser window will display -"everything is fine" if the parameter validation was successful -or Error if it was not - - -LOGGING - # To use for CLI - npm install -g bunyan - -https://github.com/trentm/node-bunyan - diff --git a/labs/api/meetings-vx/index.coffee b/labs/api/meetings-vx/index.coffee deleted file mode 100755 index 151b12f6bac00fb4ff238640e3f8ea0727621f94..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/index.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Hapi = require("hapi") -pack = require './package' -routes = require './lib/routes' -bunyan = require 'bunyan' - -log = bunyan.createLogger({name: 'myapp'}); -log.info('hi') -log.warn({lang: 'fr'}, 'au revoir') - -server = Hapi.createServer("0.0.0.0", parseInt(process.env.PORT, 10) or 4000) - -server.start(() -> - log.info(['start'], pack.name + ' - web interface: ' + server.info.uri); -) - -server.route routes.routes - diff --git a/labs/api/meetings-vx/lib/handlers.coffee b/labs/api/meetings-vx/lib/handlers.coffee deleted file mode 100755 index 9e2fb0fdd392cf4ac899cee2f4b95e1d52157267..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/lib/handlers.coffee +++ /dev/null @@ -1,30 +0,0 @@ -hapi = require 'hapi' -Joi = require 'joi' -util = require './util' -sha1 = require 'js-sha1' - -sharedSecret = '8cd8ef52e8e101574e400365b55e11a6' - -index = (req, resp) -> - resp "Hello World!" - -createHandler = (req, resp) -> - console.log("CREATE: " + req.originalUrl ) - checksum = req.query.checksum - console.log("checksum = [" + checksum + "]") - - query = util.removeChecksumFromQuery(req.query) - - baseString = util.buildCreateBaseString(query) - ourChecksum = util.calculateChecksum("create", baseString, sharedSecret) - - console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum - - if checksum isnt ourChecksum - resp "Fail!" - else - resp "everything is fine" - - -exports.index = index -exports.create = createHandler diff --git a/labs/api/meetings-vx/lib/routes.coffee b/labs/api/meetings-vx/lib/routes.coffee deleted file mode 100755 index d2cc3cb0114a6d49bb9289f7e9a17aa142cc8782..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/lib/routes.coffee +++ /dev/null @@ -1,33 +0,0 @@ -hapi = require 'hapi' -handlers = require './handlers' -Joi = require 'joi' - -createValidation = - attendeePW: Joi.string().max(20).required() - checksum: Joi.string().required() - meetingID: Joi.string().min(3).max(30).required() - moderatorPW: Joi.string().required() - name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/) - record: Joi.boolean() - voiceBridge: Joi.string() - welcome: Joi.string() - -routes = [{ - method: 'GET', - path: '/', - config: { - handler: handlers.index - } - }, { - method: "GET", - path: "/bigbluebutton/api/create", - config: { - handler: handlers.create, - validate: { - query: createValidation - } - } - }]; - - -exports.routes = routes; \ No newline at end of file diff --git a/labs/api/meetings-vx/lib/util.coffee b/labs/api/meetings-vx/lib/util.coffee deleted file mode 100755 index 05101168f785ac53d287531a1ec927453d0e099b..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/lib/util.coffee +++ /dev/null @@ -1,32 +0,0 @@ -sha1 = require 'js-sha1' - - - -removeChecksumFromQuery = (query) -> - for own propName of query - console.log(propName + "=" + query[propName]) - delete query['checksum'] - query - -buildCreateBaseString = (query) -> - baseString = "" - for own propName of query - propVal = query[propName] - if (propName == "welcome") - propVal = encodeURIComponent(query.welcome).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A") - baseString += propName + "=" + propVal + "&" - console.log(propName + "=" + query[propName]) - - console.log("baseString=[" + baseString.slice(0, -1) + "]") - - baseString.slice(0, -1) - -calculateChecksum = (method, baseString, sharedSecret) -> - qStr = method + baseString + sharedSecret - console.log("[" + qStr + "]") - sha1(qStr) - - -exports.removeChecksumFromQuery = removeChecksumFromQuery -exports.buildCreateBaseString = buildCreateBaseString -exports.calculateChecksum = calculateChecksum \ No newline at end of file diff --git a/labs/api/meetings-vx/package.json b/labs/api/meetings-vx/package.json deleted file mode 100644 index 3350d153695c57ca58fcc392637c834970f8164a..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "exploringHapi", - "version": "0.0.2", - "private": true, - "scripts": { - "start": "coffee index.coffee" - }, - "dependencies": { - "hapi": "2.6.0", - "joi": "2.7.0", - "oauth-signature": "1.1.3", - "coffee-script": "1.7.1", - "js-sha1": "0.1.1", - "bunyan": "0.22.2", - "glob": "3.2.6" - }, - "devDependencies": { - "coffee-script": "1.7.1", - "mocha": "1.18.2", - "should": "3.3.1", - "glob": "3.2.6", - "chai": "1.9.x" - } -} diff --git a/labs/api/meetings-vx/test/test_helper.coffee b/labs/api/meetings-vx/test/test_helper.coffee deleted file mode 100755 index 6636bc60e10fab9b98464c4cbde0829f7ccb505e..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/test/test_helper.coffee +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/labs/api/meetings-vx/test/testc.coffee b/labs/api/meetings-vx/test/testc.coffee deleted file mode 100644 index 49cae906b6c949f7f5c91dd486e5551138ff8781..0000000000000000000000000000000000000000 --- a/labs/api/meetings-vx/test/testc.coffee +++ /dev/null @@ -1,31 +0,0 @@ -assert = require("assert") -oauth = require("oauth-signature") - -describe "Array", -> - - describe '#indexOf()', -> - - it 'should return -1 when the value is not present', -> - assert.equal(-1, [1,2,3].indexOf(5)) - - it "should calc checksum", -> - httpMethod = 'GET' - url = 'http://photos.example.net/photos' - parameters = { - oauth_consumer_key : 'dpf43f3p2l4k3l03', - oauth_token : 'nnch734d00sl2jdk', - oauth_nonce : 'kllo9940pd9333jh', - oauth_timestamp : '1191242096', - oauth_signature_method : 'HMAC-SHA1', - oauth_version : '1.0', - file : 'vacation.jpg', - size : 'original' - } - consumerSecret = 'kd94hf93k423kf44' - tokenSecret = 'pfkkdhi9sl3r4s00' - encodedSignature = oauth.generate(httpMethod, url, parameters, consumerSecret, tokenSecret); - console.log(encodedSignature) - assert.equal(encodedSignature, "tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D") - - - diff --git a/labs/api/meetings/.gitignore b/labs/api/meetings/.gitignore deleted file mode 100644 index 3c3629e647f5ddf82548912e337bea9826b434af..0000000000000000000000000000000000000000 --- a/labs/api/meetings/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/labs/api/meetings/Cakefile b/labs/api/meetings/Cakefile deleted file mode 100755 index f71c67698ab0068f2da4789affbcb80f19950e1d..0000000000000000000000000000000000000000 --- a/labs/api/meetings/Cakefile +++ /dev/null @@ -1,65 +0,0 @@ -fs = require 'fs' -{print} = require 'util' -{spawn, exec} = require 'child_process' -glob = require 'glob' - -REPORTER = "min" - -config = {} -config.binPath = './node_modules/.bin/' - -# cake test # run all tests -# cake -f test/lib/file.coffee test # run the files passed -# cake -b test # run all tests and stop at first failure -option '-f', '--file [FILE*]', 'input file(s)' -option '-b', '--bail', 'bail' -task 'test', 'Run the test suite', (options) -> - process.env.NODE_ENV = "test" - testFiles = [ - - ] - testOpts = [ - '--require', 'coffee-script/register', - '--compilers', 'coffee:coffee-script/register', - '--require', 'should', - '--colors', - '--ignore-leaks', - '--timeout', '15000', - '--reporter', 'spec' - ] - if options.bail? and options.bail - testOpts = testOpts.concat('-b') - - if options.file? - if _.isArray(options.file) - files = testFiles.concat(options.file) - else - files = testFiles.concat([options.file]) - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - - else - glob 'test/**/*.coffee', (error, files) -> - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - -# Internal methods - -# Spawns an application with `options` and calls `onExit` -# when it finishes. -run = (bin, options, onExit) -> - bin = config.binPath + bin - console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "") - cmd = spawn bin, options - cmd.stdout.on 'data', (data) -> print data.toString() - cmd.stderr.on 'data', (data) -> print data.toString() - cmd.on 'exit', (code) -> - console.log 'done.' - onExit?(code, options) - -# Returns a string with the current time to print out. -timeNow = -> - today = new Date() - today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() \ No newline at end of file diff --git a/labs/api/meetings/README.md b/labs/api/meetings/README.md deleted file mode 100644 index 17d99532d724a542d2d87348aa400e4dc16d709f..0000000000000000000000000000000000000000 --- a/labs/api/meetings/README.md +++ /dev/null @@ -1,41 +0,0 @@ -exploringHapi -============= - -This was used as a playground for attempts to validate URL parameters -and to calculate and compare checksum - -Keywords: hapi, joi, OAuth, checksum, hmac_sha1 - -Instructions: -============= -from Terminal: -$ coffee index.coffee -Listening on http://x.x.x.x:PORT - -go to the browser, open an MCONF API-MATE window -modify the "server"(id="input-custom-server-url") field to http://x.x.x.x:PORT -click on the link for creating a meeting ("create ...") - -In the Terminal window you should see something like: -the checksum from url is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a -YAY! They match! - -or - -the checksum from url is -e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is -dkfjhdkjfhlkafhdfklahfkfhfjhkgfeq349492a - -The browser window will display -"everything is fine" if the parameter validation was successful -or Error if it was not - - -LOGGING - # To use for CLI - npm install -g bunyan - -https://github.com/trentm/node-bunyan - diff --git a/labs/api/meetings/index.coffee b/labs/api/meetings/index.coffee deleted file mode 100755 index 151b12f6bac00fb4ff238640e3f8ea0727621f94..0000000000000000000000000000000000000000 --- a/labs/api/meetings/index.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Hapi = require("hapi") -pack = require './package' -routes = require './lib/routes' -bunyan = require 'bunyan' - -log = bunyan.createLogger({name: 'myapp'}); -log.info('hi') -log.warn({lang: 'fr'}, 'au revoir') - -server = Hapi.createServer("0.0.0.0", parseInt(process.env.PORT, 10) or 4000) - -server.start(() -> - log.info(['start'], pack.name + ' - web interface: ' + server.info.uri); -) - -server.route routes.routes - diff --git a/labs/api/meetings/lib/handlers.coffee b/labs/api/meetings/lib/handlers.coffee deleted file mode 100755 index 9e2fb0fdd392cf4ac899cee2f4b95e1d52157267..0000000000000000000000000000000000000000 --- a/labs/api/meetings/lib/handlers.coffee +++ /dev/null @@ -1,30 +0,0 @@ -hapi = require 'hapi' -Joi = require 'joi' -util = require './util' -sha1 = require 'js-sha1' - -sharedSecret = '8cd8ef52e8e101574e400365b55e11a6' - -index = (req, resp) -> - resp "Hello World!" - -createHandler = (req, resp) -> - console.log("CREATE: " + req.originalUrl ) - checksum = req.query.checksum - console.log("checksum = [" + checksum + "]") - - query = util.removeChecksumFromQuery(req.query) - - baseString = util.buildCreateBaseString(query) - ourChecksum = util.calculateChecksum("create", baseString, sharedSecret) - - console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum - - if checksum isnt ourChecksum - resp "Fail!" - else - resp "everything is fine" - - -exports.index = index -exports.create = createHandler diff --git a/labs/api/meetings/lib/routes.coffee b/labs/api/meetings/lib/routes.coffee deleted file mode 100755 index d2cc3cb0114a6d49bb9289f7e9a17aa142cc8782..0000000000000000000000000000000000000000 --- a/labs/api/meetings/lib/routes.coffee +++ /dev/null @@ -1,33 +0,0 @@ -hapi = require 'hapi' -handlers = require './handlers' -Joi = require 'joi' - -createValidation = - attendeePW: Joi.string().max(20).required() - checksum: Joi.string().required() - meetingID: Joi.string().min(3).max(30).required() - moderatorPW: Joi.string().required() - name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/) - record: Joi.boolean() - voiceBridge: Joi.string() - welcome: Joi.string() - -routes = [{ - method: 'GET', - path: '/', - config: { - handler: handlers.index - } - }, { - method: "GET", - path: "/bigbluebutton/api/create", - config: { - handler: handlers.create, - validate: { - query: createValidation - } - } - }]; - - -exports.routes = routes; \ No newline at end of file diff --git a/labs/api/meetings/lib/util.coffee b/labs/api/meetings/lib/util.coffee deleted file mode 100755 index 05101168f785ac53d287531a1ec927453d0e099b..0000000000000000000000000000000000000000 --- a/labs/api/meetings/lib/util.coffee +++ /dev/null @@ -1,32 +0,0 @@ -sha1 = require 'js-sha1' - - - -removeChecksumFromQuery = (query) -> - for own propName of query - console.log(propName + "=" + query[propName]) - delete query['checksum'] - query - -buildCreateBaseString = (query) -> - baseString = "" - for own propName of query - propVal = query[propName] - if (propName == "welcome") - propVal = encodeURIComponent(query.welcome).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A") - baseString += propName + "=" + propVal + "&" - console.log(propName + "=" + query[propName]) - - console.log("baseString=[" + baseString.slice(0, -1) + "]") - - baseString.slice(0, -1) - -calculateChecksum = (method, baseString, sharedSecret) -> - qStr = method + baseString + sharedSecret - console.log("[" + qStr + "]") - sha1(qStr) - - -exports.removeChecksumFromQuery = removeChecksumFromQuery -exports.buildCreateBaseString = buildCreateBaseString -exports.calculateChecksum = calculateChecksum \ No newline at end of file diff --git a/labs/api/meetings/package.json b/labs/api/meetings/package.json deleted file mode 100644 index d98ce6d3d9067df8f039a06d699f7645b5e004bd..0000000000000000000000000000000000000000 --- a/labs/api/meetings/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "meetingApi", - "version": "0.0.2", - "private": true, - "scripts": { - "start": "coffee index.coffee" - }, - "dependencies": { - "hapi": "2.6.0", - "joi": "2.7.0", - "coffee-script": "1.7.1", - "js-sha1": "0.1.1", - "bunyan": "0.22.2", - "glob": "3.2.6" - }, - "devDependencies": { - "coffee-script": "1.7.1", - "mocha": "1.18.2", - "should": "3.3.1", - "glob": "3.2.6", - "chai": "1.9.x" - } -} diff --git a/labs/api/meetings/test/routetests.coffee b/labs/api/meetings/test/routetests.coffee deleted file mode 100644 index 7e099d6c27b671a2c3a73cccc72b2851d4572569..0000000000000000000000000000000000000000 --- a/labs/api/meetings/test/routetests.coffee +++ /dev/null @@ -1,39 +0,0 @@ -hapi = require('hapi') -assert = require('assert') -chai = require('chai') -assert = chai.assert -routes = require('../lib/routes') - - -# integration tests for API endpoint - - -# setup server with firing up - use inject instead -server = new hapi.Server() -server.route(routes.routes) - - -# parseurls endpoint test -describe 'add endpoint', -> - - it 'add - should add two numbers together', -> - server.inject({method: 'PUT', url: '/sum/add/5/5'}, (res) -> - assert.deepEqual({'equals': 10}, JSON.parse(res.payload)) - done() - ) - - it 'add - should error if a string is passed', (done) -> - server.inject({method: 'PUT', url: '/sum/add/100/x'}, (res) -> - assert.deepEqual({ - 'statusCode': 400, - 'error': 'Bad Request', - 'message': 'the value of b must be a number', - 'validation': { - 'source': 'path', - 'keys': [ - 'b' - ] - } - }, JSON.parse(res.payload)) - done() - ) \ No newline at end of file diff --git a/labs/api/meetings/test/test_helper.coffee b/labs/api/meetings/test/test_helper.coffee deleted file mode 100755 index 6636bc60e10fab9b98464c4cbde0829f7ccb505e..0000000000000000000000000000000000000000 --- a/labs/api/meetings/test/test_helper.coffee +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/labs/api/meetings/test/testc.coffee b/labs/api/meetings/test/testc.coffee deleted file mode 100755 index dcb98bc2dfd2515bb99868c8cfe86423500aefbf..0000000000000000000000000000000000000000 --- a/labs/api/meetings/test/testc.coffee +++ /dev/null @@ -1,11 +0,0 @@ -assert = require("assert") - -describe "Array", -> - - describe '#indexOf()', -> - - it 'should return -1 when the value is not present', -> - assert.equal(-1, [1,2,3].indexOf(5)) - - - diff --git a/labs/api/recordings/.gitignore b/labs/api/recordings/.gitignore deleted file mode 100644 index 3db93c472c5aeda4d1029cabb51f5061193100c8..0000000000000000000000000000000000000000 --- a/labs/api/recordings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -log/*.log diff --git a/labs/api/recordings/Cakefile b/labs/api/recordings/Cakefile deleted file mode 100755 index f71c67698ab0068f2da4789affbcb80f19950e1d..0000000000000000000000000000000000000000 --- a/labs/api/recordings/Cakefile +++ /dev/null @@ -1,65 +0,0 @@ -fs = require 'fs' -{print} = require 'util' -{spawn, exec} = require 'child_process' -glob = require 'glob' - -REPORTER = "min" - -config = {} -config.binPath = './node_modules/.bin/' - -# cake test # run all tests -# cake -f test/lib/file.coffee test # run the files passed -# cake -b test # run all tests and stop at first failure -option '-f', '--file [FILE*]', 'input file(s)' -option '-b', '--bail', 'bail' -task 'test', 'Run the test suite', (options) -> - process.env.NODE_ENV = "test" - testFiles = [ - - ] - testOpts = [ - '--require', 'coffee-script/register', - '--compilers', 'coffee:coffee-script/register', - '--require', 'should', - '--colors', - '--ignore-leaks', - '--timeout', '15000', - '--reporter', 'spec' - ] - if options.bail? and options.bail - testOpts = testOpts.concat('-b') - - if options.file? - if _.isArray(options.file) - files = testFiles.concat(options.file) - else - files = testFiles.concat([options.file]) - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - - else - glob 'test/**/*.coffee', (error, files) -> - for opt in testOpts.reverse() - files.unshift opt - run 'mocha', files - -# Internal methods - -# Spawns an application with `options` and calls `onExit` -# when it finishes. -run = (bin, options, onExit) -> - bin = config.binPath + bin - console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "") - cmd = spawn bin, options - cmd.stdout.on 'data', (data) -> print data.toString() - cmd.stderr.on 'data', (data) -> print data.toString() - cmd.on 'exit', (code) -> - console.log 'done.' - onExit?(code, options) - -# Returns a string with the current time to print out. -timeNow = -> - today = new Date() - today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() \ No newline at end of file diff --git a/labs/api/recordings/README.md b/labs/api/recordings/README.md deleted file mode 100644 index 8c2d7b726912da78867178b9d9f54db737053343..0000000000000000000000000000000000000000 --- a/labs/api/recordings/README.md +++ /dev/null @@ -1,43 +0,0 @@ -recordingsWatcher -============= -This app is used to watch the file tree for recording files changes -in the directories -/var/bigbluebutton/published -and -/var/bigbluebutton/unpublished - - -For each recording modified, we push into Redis: -key = bbb:recordings:<meetingID> -value = a set of JSON strings -{"format": "<format>", "timestamp": "<timestamp>"} - - -For example: - -bbb:recordings:fbdbde6fd7b6499723a101c4c962f03843b4879c -[{"format": "presentation", "timestamp": "1396623833035"}, {"format": "capture", "timestamp": "1396623833045"}] - - -Instructions: -============= -from Terminal: -$ coffee index.coffee - -in another Terminal: -$ curl localhost:4000/recordings?meetingid=fbdbde6fd7b6499723a101c4c962f03843b48 -returns an array of stringified json recordings (see above for the structure of the JSON) - -if there are no recordings for the given meetingID, the message -"No recordings for meetingid=some_random_string" appears - - -Running Tests -============= -while the application is running // $ coffee index.coffee -open another console and enter: -$ cake test - -or -$ ./node_modules/.bin/mocha --require coffee-script/register --compilers coffee:coffee-script/register --require should --colors --ignore-leaks --timeout 15000 --reporter spec test/routetests.coffee -(where test/routetests.coffee is the collecion of tests you want to execute) \ No newline at end of file diff --git a/labs/api/recordings/config.coffee b/labs/api/recordings/config.coffee deleted file mode 100755 index 14b2435fd08f821d3dd14603f940c93d7266a65e..0000000000000000000000000000000000000000 --- a/labs/api/recordings/config.coffee +++ /dev/null @@ -1,13 +0,0 @@ -# # Global configurations file - -config = {} - -# Logging -config.log = {} - -config.log.path = if process.env.NODE_ENV == "production" - "/var/log/bigbluebutton/recording-api.log" -else - "./log/recording-api-dev.log" - -module.exports = config \ No newline at end of file diff --git a/labs/api/recordings/index.coffee b/labs/api/recordings/index.coffee deleted file mode 100755 index 5c25ab2b31c381a69e16876133fb17aae085425d..0000000000000000000000000000000000000000 --- a/labs/api/recordings/index.coffee +++ /dev/null @@ -1,17 +0,0 @@ -hapi = require 'hapi' - -log = require './lib/logger' -pack = require './package' -recWatcher = require './lib/recording-dir-watcher' -routes = require './lib/routes' - -server = hapi.createServer("0.0.0.0", - parseInt(process.env.PORT, 10) or 4000) - -server.start(() -> - log.info(['start'], pack.name + ' - web interface: ' + server.info.uri) -) - -server.route(routes.routes) - -recWatcher.watch() diff --git a/labs/api/recordings/lib/handlers.coffee b/labs/api/recordings/lib/handlers.coffee deleted file mode 100755 index 3320d66df2cf2792f4ab69882a0761a47fadea66..0000000000000000000000000000000000000000 --- a/labs/api/recordings/lib/handlers.coffee +++ /dev/null @@ -1,38 +0,0 @@ -util = require './util' -recWatcher = require './recording-dir-watcher' - -sharedSecret = '8cd8ef52e8e101574e400365b55e11a6' - -index = (req, resp) -> - resp "Hello World!" - -createHandler = (req, resp) -> - console.log("CREATE: " + req.originalUrl ) - checksum = req.query.checksum - console.log("checksum = [" + checksum + "]") - - query = util.removeChecksumFromQuery(req.query) - - baseString = util.buildCreateBaseString(query) - ourChecksum = util.calculateChecksum("create", baseString, sharedSecret) - - console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum - - if checksum isnt ourChecksum - resp "Fail!" - else - resp "everything is fine" - -getRecordings = (req, resp) -> - requestedMeetingID = req.query.meetingid - console.log("recordings for: " + requestedMeetingID) - - recWatcher.getRecordingsArray requestedMeetingID, (array) -> - if array?.length > 0 - resp JSON.stringify(array) - else - resp "No recordings for meetingid=#{requestedMeetingID}\n" - -exports.index = index -exports.create = createHandler -exports.recordings = getRecordings diff --git a/labs/api/recordings/lib/logger.coffee b/labs/api/recordings/lib/logger.coffee deleted file mode 100755 index 94569cbddeb10e9ff16bed2cfb88e32ca5ecba44..0000000000000000000000000000000000000000 --- a/labs/api/recordings/lib/logger.coffee +++ /dev/null @@ -1,19 +0,0 @@ -bunyan = require 'bunyan' - -config = require '../config' - -logger = bunyan.createLogger({ - name: 'bbbnode' - streams: [ - { - level: 'debug' - stream: process.stdout - }, - { - level: 'info' - path: config.log.path - } - ] -}) - -module.exports = logger diff --git a/labs/api/recordings/lib/recording-dir-watcher.coffee b/labs/api/recordings/lib/recording-dir-watcher.coffee deleted file mode 100755 index 30640f53012e81d274cb1873554c30272cefc622..0000000000000000000000000000000000000000 --- a/labs/api/recordings/lib/recording-dir-watcher.coffee +++ /dev/null @@ -1,57 +0,0 @@ -## -## Watches the recording dirs for new recordings -## - -chokidar = require 'chokidar' -redis = require 'redis' - -log = require './logger' - - -client = redis.createClient() - -baseKey = 'bbb:recordings:' - -watch = -> - #clear the keys first - keys = client.keys(baseKey.concat('*')) - client.del(keys) - - #start watching - chokidar.watch('/var/bigbluebutton/published', {ignored: /[\/\\]\./}).on 'all', (event, path) -> - somethingChanged(event,path) - chokidar.watch('/var/bigbluebutton/unpublished', {ignored: /[\/\\]\./}).on 'all', (event, path) -> - somethingChanged(event,path) - - -somethingChanged = (event, path) -> - uri = path.split('/') - - if uri[5]? #excludes the parent directories being added - pathArray = path.substring(path.lastIndexOf('/')+1).split('-') - meetingID = pathArray[0] - timestamp = pathArray[1] - - thisKey = baseKey.concat(meetingID) - - json = { - "format": uri[4] - "timestamp": timestamp - } - - log.info(event, path) - str = JSON.stringify(json) - - client.sadd(thisKey, str) - -getRecordingsArray = (meetingID, callback) -> - thisKey = baseKey.concat(meetingID) - - client.smembers thisKey, (err, members) -> - if err - console.log "Error: #{err}" - else - callback members - -exports.watch = watch -exports.getRecordingsArray = getRecordingsArray \ No newline at end of file diff --git a/labs/api/recordings/lib/routes.coffee b/labs/api/recordings/lib/routes.coffee deleted file mode 100755 index d24cf7118977aa2210112e399823e668e45ccece..0000000000000000000000000000000000000000 --- a/labs/api/recordings/lib/routes.coffee +++ /dev/null @@ -1,44 +0,0 @@ -Joi = require 'joi' - -handlers = require './handlers' - -createValidation = - attendeePW: Joi.string().max(20).required() - checksum: Joi.string().required() - meetingID: Joi.string().min(3).max(30).required() - moderatorPW: Joi.string().required() - name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/) - record: Joi.boolean() - voiceBridge: Joi.string() - welcome: Joi.string() - -recordingsValidation = - meetingid: Joi.string().min(3).max(45).required() - -routes = [{ - method: 'GET' - path: '/' - config: { - handler: handlers.index - } - }, { - method: "GET" - path: "/bigbluebutton/api/create" - config: { - handler: handlers.create - validate: { - query: createValidation - } - } - }, { - method: "GET" - path: "/recordings" - config: { - handler: handlers.recordings - validate: { - query: recordingsValidation - } - } - }] - -exports.routes = routes \ No newline at end of file diff --git a/labs/api/recordings/lib/util.coffee b/labs/api/recordings/lib/util.coffee deleted file mode 100644 index 13de702b329240b5e65816677b77a8afcae97292..0000000000000000000000000000000000000000 --- a/labs/api/recordings/lib/util.coffee +++ /dev/null @@ -1,12 +0,0 @@ -parser1 = require 'xml2json' -parser2 = require 'json2xml' - -xml2json = (xmlStr) -> - parser1.toJson(xmlStr) - -json2xml = (jsonObj) -> - #parser2(jsonObj) - parser1.toXml(jsonObj) - -exports.xml2json = xml2json -exports.json2xml = json2xml diff --git a/labs/api/recordings/log/.gitkeep b/labs/api/recordings/log/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/labs/api/recordings/package.json b/labs/api/recordings/package.json deleted file mode 100644 index 64b90255007128dec5a42400a9604397bff01f61..0000000000000000000000000000000000000000 --- a/labs/api/recordings/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "recordingsWatcher", - "version": "0.0.2", - "private": true, - "scripts": { - "start": "coffee index.coffee" - }, - "dependencies": { - "chokidar": "0.8.2", - "redis": "0.10.1", - "hiredis": "0.1.16", - "hapi": "2.6.0", - "joi": "2.7.0", - "coffee-script": "1.7.1", - "js-sha1": "0.1.1", - "bunyan": "0.22.2", - "json2xml": "0.1.1", - "xml2json": "0.4.0", - "easyxml": "0.0.5", - "glob": "3.2.6" - }, - "devDependencies": { - "coffee-script": "1.7.1", - "mocha": "1.18.2", - "should": "3.3.1", - "glob": "3.2.6", - "chai": "1.9.x" - } -} diff --git a/labs/api/recordings/test/routetests.coffee b/labs/api/recordings/test/routetests.coffee deleted file mode 100644 index d8d9187e85e58b549d536c07931886fe2fb41f87..0000000000000000000000000000000000000000 --- a/labs/api/recordings/test/routetests.coffee +++ /dev/null @@ -1,86 +0,0 @@ -assert = require('chai').assert -hapi = require('hapi') - -routes = require('../lib/routes') - -# integration tests for API endpoint - - -# setup server with firing up - use inject instead -server = new hapi.Server() -server.route(routes.routes) - - -# parseurls endpoint test -describe 'checking recordings', -> - - it 'recordings for a given meetingid', -> - server.inject({method: 'GET', url: '192.168.0.203:4000/recordings?meetingid=fbdbde6fd7b6499723a101c4c962f03843b4879c'}, (res) -> - #console.log "json:" + res.payload - array = [ - { - 'format': 'presentation' - 'timestamp':'1396619572523' - }, { - 'format': 'capture' - 'timestamp':'1396623833044' - }, { - 'format': 'presentation' - 'timestamp':'1396620788271' - }, { - 'format': 'presentation' - 'timestamp':'1396622260421' - }, { - 'format': 'capture' - 'timestamp':'1396623833035' - }, { - 'format': 'capture' - 'timestamp':'1396623830000' - }, { - 'format': 'capture' - 'timestamp':'1396619572523' - }, { - 'format': 'capture' - 'timestamp':'1396622260421' - }, { - 'format': 'capture' - 'timestamp':'1396620788271' - }, { - 'format': 'presentation' - 'timestamp':'1396623833035' - }, { - 'format': 'capture' - 'timestamp':'1396623831111' - } - ] - - parsedOnce = JSON.parse(res.payload) - index = 0 - while index < parsedOnce.length - assert.deepEqual(JSON.stringify(array[index]), parsedOnce[index]) - index++ - #console.log index - ) - ###it 'add - should add two numbers together', -> - server.inject({method: 'PUT', url: '/sum/add/5/5'}, (res) -> - console.log "json:" +JSON.stringify(res.payload) - assert.deepEqual({'equals': 10}, JSON.parse(res.payload)) - done() - )### - - ###it 'add - should error if a string is passed', (done) -> - server.inject({method: 'PUT', url: '/sum/add/100/1'}, (res) -> - console.log "json:" +JSON.stringify(res) - assert.deepEqual({ - 'statusCode': 400 - 'error': 'Bad Request' - 'message': 'the value of b must be a number' - 'validation': { - 'source': 'path' - 'keys': [ - 'b' - ] - } - }, JSON.parse(res.payload)) - done() - )### \ No newline at end of file diff --git a/labs/api/recordings/test/test_helper.coffee b/labs/api/recordings/test/test_helper.coffee deleted file mode 100755 index 6636bc60e10fab9b98464c4cbde0829f7ccb505e..0000000000000000000000000000000000000000 --- a/labs/api/recordings/test/test_helper.coffee +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/labs/api/recordings/test/testc.coffee b/labs/api/recordings/test/testc.coffee deleted file mode 100755 index dcb98bc2dfd2515bb99868c8cfe86423500aefbf..0000000000000000000000000000000000000000 --- a/labs/api/recordings/test/testc.coffee +++ /dev/null @@ -1,11 +0,0 @@ -assert = require("assert") - -describe "Array", -> - - describe '#indexOf()', -> - - it 'should return -1 when the value is not present', -> - assert.equal(-1, [1,2,3].indexOf(5)) - - - diff --git a/labs/api/recordings/test/utiltests.coffee b/labs/api/recordings/test/utiltests.coffee deleted file mode 100644 index 16f811fc36df55d93d30e87de83a99fc7509a18a..0000000000000000000000000000000000000000 --- a/labs/api/recordings/test/utiltests.coffee +++ /dev/null @@ -1,58 +0,0 @@ -assert = require("assert") - -util = require '../lib/util' - -sampleXml = """ - <recording> - <id>6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</id> - <state>available</state> - <published>true</published> - <start_time>1398363223514</start_time> - <end_time>1398363348994</end_time> - <playback> - <format>presentation</format> - <link>http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</link> - <processing_time>5429</processing_time> - <duration>101014</duration> - <extension> - <custom>... Any XML element, to be passed through into playback format element.</custom> - </extension> - </playback> - <meta> - <meetingId>English 101</meetingId> - <meetingName>English 101</meetingName> - <description>Test recording</description> - <title>English 101</title> - </meta> - </recording> -""" - -jsonResult = { - "recording": { - "id": "6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956", - "state": "available", - "published": true, - "start_time": 1398363223514, - "end_time": 1398363348994, - "playback": { - "format": "presentation", - "link": "http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956", - "processing_time": 5429, - "duration": 101014, - "extension": { - "custom": "... Any XML element, to be passed through into playback format element." - } - }, - "meta": { - "meetingId": "English 101", - "meetingName": "English 101", - "description": "Test recording", - "title": "English 101" - } - } -} - -describe "util", -> - describe 'xml2json()', -> - it 'should return a json string', -> - assert.deepEqual(jsonResult, JSON.parse(util.xml2json(sampleXml))) diff --git a/labs/api/recordings/testjson2xml.coffee b/labs/api/recordings/testjson2xml.coffee deleted file mode 100644 index 5d77beb87843d98c2fdcac90f84c6ccd0c989d5c..0000000000000000000000000000000000000000 --- a/labs/api/recordings/testjson2xml.coffee +++ /dev/null @@ -1,34 +0,0 @@ -util = require './lib/util' - -sampleXml = """ - <recording> - <id>6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</id> - <state>available</state> - <published>true</published> - <start_time>1398363223514</start_time> - <end_time>1398363348994</end_time> - <playback> - <format>presentation</format> - <link>http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</link> - <processing_time>5429</processing_time> - <duration>101014</duration> - <extension> - <custom>... Any XML element, to be passed through into playback format element.</custom> - </extension> - </playback> - <meta> - <meetingId>English 101</meetingId> - <meetingName>English 101</meetingName> - <description>Test recording</description> - <title>English 101</title> - </meta> - </recording> -""" - -jsonObj = util.xml2json( sampleXml ) - -console.log(jsonObj) - -jstr = util.json2xml(JSON.parse(jsonObj)) - -console.log(jstr) \ No newline at end of file diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index 7e57f74f61fdb9b43d12d8b6b0e973c05a636209..9df1b520eefa2de93e379ff973c697c11debf447 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -7,11 +7,11 @@ GEM java_properties (0.0.4) jwt (2.1.0) mini_portile2 (2.3.0) - nokogiri (1.8.1) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) open4 (1.3.4) redis (4.0.1) - rubyzip (1.2.1) + rubyzip (1.2.2) trollop (2.1.2) PLATFORMS