diff --git a/bigbluebutton-html5/.meteor/packages b/bigbluebutton-html5/.meteor/packages index 11b172a223eca58b35c0597f82f3c1d469d1e5da..42da808b79a7155506816a58bc4fdf607ded6af2 100644 --- a/bigbluebutton-html5/.meteor/packages +++ b/bigbluebutton-html5/.meteor/packages @@ -16,9 +16,10 @@ cfs:power-queue cfs:reactive-list cfs:micro-queue reactive-var@1.0.10 -ecmascript@0.5.8 +ecmascript@0.5.9-beta.7 react-meteor-data -standard-minifier-css@1.2.0 +standard-minifier-css@1.2.1 standard-minifier-js@1.2.0 -nathantreid:css-modules +nathantreid:css-modules@2.3.0-beta.2 shell-server@0.2.1 +http diff --git a/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js b/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js index 70c4bd3386f23d5f546fa7d65a6731d94071469c..f8938f80a4e28828090ea1f6ead4c0ab3ef227fc 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js @@ -1,136 +1,13 @@ import RedisPubSub from '/imports/startup/server/redis'; -import Logger from '/imports/startup/server/logger'; -import { XMLHttpRequest } from 'xmlhttprequest'; -import xml2js from 'xml2js'; -const xmlParser = new xml2js.Parser(); - -import Breakouts from '/imports/api/breakouts'; - -RedisPubSub.on('CreateBreakoutRoomRequest', ({ payload }) => { - const selector = { - breakoutMeetingId: payload.breakoutMeetingId, - }; - const modifier = payload; - - const cb = (err, numChanged) => { - if (err) { - return Logger.error(`Adding breakout to collection: ${err}`); - } - - const { - insertedId, - } = numChanged; - if (insertedId) { - return Logger.info(`Added breakout id=${payload.breakoutMeetingId}`); - } - - return Logger.info(`Upserted breakout id=${payload.breakoutMeetingId}`); - }; - - Breakouts.upsert(selector, modifier, cb); -}); - -RedisPubSub.on('BreakoutRoomStarted', ({ payload }) => { - const selector = { - breakoutMeetingId: payload.meetingId, - }; - - modifier = { - $set: { - users: [], - externalMeetingId: payload.externalMeetingId, - }, - }; - - const cb = (err, numChanged) => { - if (err) { - return Logger.error(`Adding breakout to collection: ${err}`); - } - - const { - insertedId, - } = numChanged; - if (insertedId) { - return Logger.info(`Added breakout id=${payload.meetingId}`); - } - - return Logger.info(`Upserted breakout id=${payload.meetingId}`); - }; - - Breakouts.upsert(selector, modifier, cb); -}); - -RedisPubSub.on('BreakoutRoomJoinURL', ({ payload }) => { - const REDIS_CONFIG = Meteor.settings.redis; - - const { - meetingId, - joinURL, - } = payload; - - let urlParams = {}; - joinURL.split('?')[1].split('&').map(s => { - const p = s.split('='); - urlParams[p[0]] = p[1]; - }); - - const selector = { - externalMeetingId: urlParams.meetingID, - }; - - let breakout = Breakouts.findOne(selector); - - if (urlParams.redirect !== 'false') { - const MessageContent = { - breakoutMeetingId: breakout.externalMeetingId, - meetingId: breakout.parentMeetingId, - redirect: false, - userId: payload.userId, - }; - - const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users; - const eventName = 'RequestBreakoutJoinURL'; - - RedisPubSub.publish(CHANNEL, eventName, MessageContent); - } else { - const res = Meteor.http.call('get', joinURL); - xmlParser.parseString(res.content, (err, parsedXML) => { - breakout = Breakouts.findOne(selector); - - const { response } = parsedXML; - let users = breakout.users; - - let user = { - userId: payload.userId, - urlParams: { - meetingId: response.meeting_id[0], - userId: response.user_id[0], - authToken: response.auth_token[0], - }, - }; - - const userExists = users.find(u => user.userId === u.userId); - - if (userExists) { - return; - } - - const modifier = { - $push: { - users: user, - }, - }; - - Breakouts.upsert(selector, modifier); - }); - } -}); - -RedisPubSub.on('UpdateBreakoutUsers', ({ payload }) => console.info('UPDT', payload)); - -RedisPubSub.on('BreakoutRoomClosed', ({ payload }) => { - Breakouts.remove({ - breakoutMeetingId: payload.meetingId, - }); -}); +import createBreakout from './handlers/createBreakout'; +import breakoutStarted from './handlers/breakoutStarted'; +import breakoutJoinURL from './handlers/breakoutJoinURL'; +import updateTimeRemaining from './handlers/updateTimeRemaining'; +import breakoutClosed from './handlers/breakoutClosed'; + +RedisPubSub.on('CreateBreakoutRoomRequest', createBreakout); +RedisPubSub.on('BreakoutRoomStarted', breakoutStarted); +RedisPubSub.on('BreakoutRoomJoinURL', breakoutJoinURL); +RedisPubSub.on('BreakoutRoomsTimeRemainingUpdate', updateTimeRemaining); +RedisPubSub.on('BreakoutRoomClosed', breakoutClosed); diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutClosed.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutClosed.js new file mode 100644 index 0000000000000000000000000000000000000000..0c5ac376678ca9a1893b478a2ae9d8c9f95ea38a --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutClosed.js @@ -0,0 +1,5 @@ +import clearBreakouts from '../modifiers/clearBreakouts'; + +export default function breakoutClosed({ payload }) { + return clearBreakouts(payload.meetingId); +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutJoinURL.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutJoinURL.js new file mode 100644 index 0000000000000000000000000000000000000000..4e651955c12d1c541e5a2a65711ea1660ecccaf1 --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutJoinURL.js @@ -0,0 +1,93 @@ +import Breakouts from '/imports/api/breakouts'; +import Logger from '/imports/startup/server/logger'; +import RedisPubSub from '/imports/startup/server/redis'; +import { XMLHttpRequest } from 'xmlhttprequest'; +import xml2js from 'xml2js'; +const xmlParser = new xml2js.Parser(); + +const getUrlParams = url => { + let urlParams = {}; + url.split('?')[1].split('&').map(s => { + const p = s.split('='); + urlParams[p[0]] = p[1]; + }); + + return urlParams; +}; + +export default function breakoutJoinURL({ payload }) { + const REDIS_CONFIG = Meteor.settings.redis; + + const { + meetingId, + joinURL, + } = payload; + + const urlParams = getUrlParams(joinURL); + + const selector = { + externalMeetingId: urlParams.meetingID, + }; + + let breakout = Breakouts.findOne(selector); + + if (urlParams.redirect !== 'false') { + const MessageContent = { + breakoutMeetingId: breakout.externalMeetingId, + meetingId: breakout.parentMeetingId, + redirect: false, + userId: payload.userId, + }; + + const CHANNEL = REDIS_CONFIG.channels.toBBBApps.users; + const eventName = 'RequestBreakoutJoinURL'; + + return RedisPubSub.publish(CHANNEL, eventName, MessageContent); + } else { + const res = Meteor.http.call('get', joinURL); + xmlParser.parseString(res.content, (err, parsedXML) => { + breakout = Breakouts.findOne(selector); + + const { response } = parsedXML; + let users = breakout.users; + + let user = { + userId: payload.userId, + urlParams: { + meetingId: response.meeting_id[0], + userId: response.user_id[0], + authToken: response.auth_token[0], + }, + }; + + const userExists = users.find(u => user.userId === u.userId); + + if (userExists) { + return; + } + + const modifier = { + $push: { + users: user, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding breakout to collection: ${err}`); + } + + const { + insertedId, + } = numChanged; + if (insertedId) { + return Logger.info(`Added breakout id=${urlParams.meetingID}`); + } + + return Logger.info(`Upserted breakout id=${urlParams.meetingID}`); + }; + + return Breakouts.upsert(selector, modifier, cb); + }); + } +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js new file mode 100644 index 0000000000000000000000000000000000000000..4951a8d97fc69f2a768bff0e601cc0f87c2e088b --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js @@ -0,0 +1,33 @@ +import Breakouts from '/imports/api/breakouts'; +import Logger from '/imports/startup/server/logger'; + +export default function ({ payload }) { + const selector = { + breakoutMeetingId: payload.meetingId, + }; + + modifier = { + $set: { + users: [], + timeRemaining: Number(payload.timeRemaining), + externalMeetingId: payload.externalMeetingId, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding breakout to collection: ${err}`); + } + + const { + insertedId, + } = numChanged; + if (insertedId) { + return Logger.info(`Added breakout id=${payload.meetingId}`); + } + + return Logger.info(`Upserted breakout id=${payload.meetingId}`); + }; + + Breakouts.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/createBreakout.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/createBreakout.js new file mode 100644 index 0000000000000000000000000000000000000000..8989f80d19303e25f3a7a2853532ff7abfd4abc2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/createBreakout.js @@ -0,0 +1,26 @@ +import Breakouts from '/imports/api/breakouts'; +import Logger from '/imports/startup/server/logger'; + +export default function({ payload }) { + const selector = { + breakoutMeetingId: payload.breakoutMeetingId, + }; + const modifier = payload; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding breakout to collection: ${err}`); + } + + const { + insertedId, + } = numChanged; + if (insertedId) { + return Logger.info(`Added breakout id=${payload.breakoutMeetingId}`); + } + + return Logger.info(`Upserted breakout id=${payload.breakoutMeetingId}`); + }; + + Breakouts.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/updateTimeRemaining.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/updateTimeRemaining.js new file mode 100644 index 0000000000000000000000000000000000000000..08fbd958ca2a13dc488b76090bdd8667979b50dc --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/updateTimeRemaining.js @@ -0,0 +1,17 @@ +import Breakouts from '/imports/api/breakouts'; +import Logger from '/imports/startup/server/logger'; + +export default function updateTimeRemaining({ payload }) { + console.log('updateTimeRemaining', payload); + const selector = { + parentMeetingId: payload.meetingId, + }; + + const modifier = { + $set: { + timeRemaining: payload.timeRemaining, + }, + }; + + Breakouts.update(selector, modifier, (err, numChanged) => console.log(err, numChanged)); +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/modifiers/clearBreakouts.js b/bigbluebutton-html5/imports/api/breakouts/server/modifiers/clearBreakouts.js new file mode 100644 index 0000000000000000000000000000000000000000..d83ff7bb9dc677931d49c89dcd7c7885715cf6d1 --- /dev/null +++ b/bigbluebutton-html5/imports/api/breakouts/server/modifiers/clearBreakouts.js @@ -0,0 +1,8 @@ +import Breakouts from '/imports/api/breakouts'; +import Logger from '/imports/startup/server/logger'; + +export default function (meetingId) { + return Breakouts.remove({ + // breakoutMeetingId: meetingId, + }, Logger.info(`Cleared Breakouts (${meetingId})`)); +} diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index 8a51a0acbc4dda151bcb231c03a969479f8069b9..2f4095eb991aaef3b101718200ad7642d025a746 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -5,8 +5,11 @@ import Logger from '/imports/startup/server/logger'; import { isAllowedTo } from '/imports/startup/server/userPermissions'; Meteor.publish('breakouts', (credentials) => { - Logger.info(`PUBLISHIIIIIIING breakouts for ${credentials}`); + Logger.info(`PUBLISHIIIIIIING breakouts for ${credentials.meetingId}`); return Breakouts.find({ - parentMeetingId: credentials.meetingId, + $or: [ + { parentMeetingId: credentials.meetingId }, + { breakoutMeetingId: credentials.meetingId }, + ], }); }); diff --git a/bigbluebutton-html5/imports/locales/en.json b/bigbluebutton-html5/imports/locales/en.json index 7ccb70a3bce69744919395e38f1b3a59d69a867f..2e3e17395579cf9f87784945b91339bedfb64c0d 100755 --- a/bigbluebutton-html5/imports/locales/en.json +++ b/bigbluebutton-html5/imports/locales/en.json @@ -65,7 +65,7 @@ "app.actionsBar.emojiMenu.clearLabel": "Clear", "app.actionsBar.emojiMenu.clearDesc": "Clear your status", "app.breakoutJoinConfirmation.title": "Join Breakout Room", - "app.breakoutJoinConfirmation.message": "Do you want to join this Breakout Room?", + "app.breakoutJoinConfirmation.message": "Do you want to join", "app.breakoutJoinConfirmation.confirmLabel": "Join", "app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room", "app.breakoutJoinConfirmation.dismissLabel": "Cancel", diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx index eb7a0a1bacac35b80d3258c087fea9054e855cb7..88913ebbfdc6f691dc217dce9abec646a50b8596 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx @@ -6,12 +6,12 @@ import Modal from '/imports/ui/components/modal/component'; const intlMessages = defineMessages({ title: { - id: 'app.breakoutJoinConfirmation.leaveConfirmation.title', + id: 'app.breakoutJoinConfirmation.title', defaultMessage: 'Join Breakout Room', }, message: { id: 'app.breakoutJoinConfirmation.message', - defaultMessage: 'Do you want to join this Breakout Room??', + defaultMessage: 'Do you want to join', }, confirmLabel: { id: 'app.breakoutJoinConfirmation.confirmLabel', @@ -46,7 +46,7 @@ class LeaveConfirmation extends Component { } render() { - const { intl } = this.props; + const { intl, breakoutName } = this.props; return ( <Modal title={intl.formatMessage(intlMessages.title)} @@ -59,7 +59,7 @@ class LeaveConfirmation extends Component { label: intl.formatMessage(intlMessages.dismissLabel), description: intl.formatMessage(intlMessages.dismissDesc), }}> - {intl.formatMessage(intlMessages.message)} + {`${intl.formatMessage(intlMessages.message)} ${breakoutName}?`} </Modal> ); } diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index a1a516ff6879178e17ab7a7cb6ccc4e032754dbd..2661f80f6f1ba32120b4542ef6b78e011fe9e513 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -28,8 +28,10 @@ const defaultProps = { beingRecorded: false, }; -const openBreakoutJoinConfirmation = breakoutURL => - showModal(<BreakoutJoinConfirmation breakoutURL={breakoutURL}/>); +const openBreakoutJoinConfirmation = (breakoutURL, breakoutName) => + showModal(<BreakoutJoinConfirmation + breakoutURL={breakoutURL} + breakoutName={breakoutName}/>); class NavBar extends Component { constructor(props) { @@ -37,6 +39,8 @@ class NavBar extends Component { this.state = { isActionsOpen: false, + didSendBreakoutInvite: false, + timeRemaining: null, }; this.handleToggleUserList = this.handleToggleUserList.bind(this); @@ -46,6 +50,12 @@ class NavBar extends Component { this.props.toggleUserList(); } + inviteUserToBreakout(breakout, breakoutURL) { + this.setState({ didSendBreakoutInvite: true }, () => { + openBreakoutJoinConfirmation.call(this, breakoutURL, breakout.name); + }); + } + render() { const { hasUnreadMessages, beingRecorded } = this.props; @@ -78,11 +88,13 @@ class NavBar extends Component { } renderPresentationTitle() { - const { presentationTitle } = this.props; - let { breakouts } = this.props; + const remainingTime = this.props.timeRemaining; + const presentationTitle = this.props.presentationTitle; + let breakouts = this.props.breakouts; + const meetingId = Auth.getCredentials().meetingId; + const currentUserId = Auth.getCredentials().requesterUserId; document.title = presentationTitle; - const currentUserId = Auth.getCredentials().requesterUserId; breakouts = breakouts.filter(breakout => { if (!breakout.users) { return false; @@ -116,32 +128,57 @@ class NavBar extends Component { ); } + componentDidUpdate() { + const { breakouts } = this.props; + const currentUserId = Auth.getCredentials().requesterUserId; + + breakouts.map(breakout => { + if (!breakout.users) { + return; + } else if (!breakout.users.find(user => user.userId === currentUserId)) { + return; + } + + const breakoutURL = this.getBreakoutJoinUrl(breakout); + if (!this.state.didSendBreakoutInvite) { + this.inviteUserToBreakout(breakout, breakoutURL); + } + }); + + if (!breakouts.length && this.state.didSendBreakoutInvite) { + this.setState({ didSendBreakoutInvite: false }); + } + } + renderBreakoutItem(breakout) { const breakoutName = breakout.name; + const breakoutURL = this.getBreakoutJoinUrl(breakout); - openBreakoutJoinConfirmation(this.getBreakoutJoinUrl(breakout)); return ( <DropdownListItem className={styles.actionsHeader} key={_.uniqueId('action-header')} label={breakoutName} - onClick={openBreakoutJoinConfirmation.bind(this, this.getBreakoutJoinUrl(breakout))} + onClick={openBreakoutJoinConfirmation.bind(this, breakoutURL, breakout.name)} defaultMessage={'batata'}/> ); } getBreakoutJoinUrl(breakout) { const currentUserId = Auth.getCredentials().requesterUserId; - const users = breakout.users.find(user => user.userId === currentUserId); - if (users) { - const urlParams = users.urlParams; - return [ - window.origin, - 'html5client/join', - urlParams.meetingId, - urlParams.userId, - urlParams.authToken, - ].join('/'); + + if (breakout.users) { + const users = breakout.users.find(user => user.userId === currentUserId); + if (users) { + const urlParams = users.urlParams; + return [ + window.origin, + 'html5client/join', + urlParams.meetingId, + urlParams.userId, + urlParams.authToken, + ].join('/'); + } } } } diff --git a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx index 72681edea3c1d70ff1dab404cd13850c67079d56..47c88f4b76d1d4b065b65e5bc232053b41cb8a6a 100644 --- a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx @@ -3,9 +3,20 @@ import { createContainer } from 'meteor/react-meteor-data'; import React, { Component, PropTypes } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'underscore'; +import NavBarService from '../nav-bar/service'; +import Auth from '/imports/ui/services/auth'; import NotificationsBar from './component'; +const humanizeSeconds = time => { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + return [ + minutes, + seconds, + ].map(x => (x < 10) ? `0${x}` : x).join(':'); +}; + // the connection is up and running const STATUS_CONNECTED = 'connected'; @@ -84,6 +95,31 @@ const startCounter = (sec) => { }, 1000); }; +// breakout +let timeRemaining = 0; +const timeRemainingDep = new Tracker.Dependency; + +const getTimeRemaining = () => { + timeRemainingDep.depend(); + return timeRemaining; +}; + +const setTimeRemaining = (sec = 0) => { + if (sec !== timeRemaining) { + timeRemaining = sec; + timeRemainingDep.changed(); + } +}; + +let timeRemainingInterval = null; +const startTimeRemainingCounter = (sec) => { + clearInterval(timeRemainingInterval); + setTimeRemaining(sec); + timeRemainingInterval = setInterval(() => { + setTimeRemaining(getTimeRemaining() - 1); + }, 1000); +}; + export default injectIntl(createContainer(({ intl }) => { const { status, connected, retryCount, retryTime } = Meteor.status(); let data = {}; @@ -109,5 +145,22 @@ export default injectIntl(createContainer(({ intl }) => { } } + const meetingId = Auth.meetingID; + const breakouts = NavBarService.getBreakouts(); + if (breakouts) { + const currentBreakout = breakouts.find(b => b.breakoutMeetingId === meetingId); + if (currentBreakout) { + roomRemainingTime = currentBreakout.timeRemaining; + if (!timeRemainingInterval && roomRemainingTime) { + startTimeRemainingCounter(roomRemainingTime); + } + } + } + + if (getTimeRemaining()) { + data.color = 'primary'; + data.message = `Breakout Room Time Remaining: ${humanizeSeconds(getTimeRemaining())}`; + } + return data; }, NotificationsBarContainer));