import React, { PureComponent } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; import { Session } from 'meteor/session'; import logger from '/imports/startup/client/logger'; import Styled from './styles'; import Service from './service'; import BreakoutRoomContainer from './breakout-remaining-time/container'; import MessageFormContainer from './message-form/container'; import VideoService from '/imports/ui/components/video-provider/service'; import { PANELS, ACTIONS } from '../layout/enums'; import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'; import AudioManager from '/imports/ui/services/audio-manager'; import Settings from '/imports/ui/services/settings'; import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component'; import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; import Header from '/imports/ui/components/common/control-header/component'; const intlMessages = defineMessages({ breakoutTitle: { id: 'app.createBreakoutRoom.title', description: 'breakout title', }, breakoutAriaTitle: { id: 'app.createBreakoutRoom.ariaTitle', description: 'breakout aria title', }, breakoutDuration: { id: 'app.createBreakoutRoom.duration', description: 'breakout duration time', }, breakoutRoom: { id: 'app.createBreakoutRoom.room', description: 'breakout room', }, breakoutJoin: { id: 'app.createBreakoutRoom.join', description: 'label for join breakout room', }, breakoutJoinAudio: { id: 'app.createBreakoutRoom.joinAudio', description: 'label for option to transfer audio', }, breakoutReturnAudio: { id: 'app.createBreakoutRoom.returnAudio', description: 'label for option to return audio', }, askToJoin: { id: 'app.createBreakoutRoom.askToJoin', description: 'label for generate breakout room url', }, generatingURL: { id: 'app.createBreakoutRoom.generatingURL', description: 'label for generating breakout room url', }, endAllBreakouts: { id: 'app.createBreakoutRoom.endAllBreakouts', description: 'Button label to end all breakout rooms', }, chatTitleMsgAllRooms: { id: 'app.createBreakoutRoom.chatTitleMsgAllRooms', description: 'chat title for send message to all rooms', }, alreadyConnected: { id: 'app.createBreakoutRoom.alreadyConnected', description: 'label for the user that is already connected to breakout room', }, setTimeInMinutes: { id: 'app.createBreakoutRoom.setTimeInMinutes', description: 'Label for input to set time (minutes)', }, setTimeLabel: { id: 'app.createBreakoutRoom.setTimeLabel', description: 'Button label to set breakout rooms time', }, setTimeCancel: { id: 'app.createBreakoutRoom.setTimeCancel', description: 'Button label to cancel set breakout rooms time', }, setTimeHigherThanMeetingTimeError: { id: 'app.createBreakoutRoom.setTimeHigherThanMeetingTimeError', description: 'Label for error when new breakout rooms time would be higher than remaining time in parent meeting', }, }); let prevBreakoutData = {}; class BreakoutRoom extends PureComponent { static sortById(a, b) { if (a.userId > b.userId) { return 1; } if (a.userId < b.userId) { return -1; } return 0; } constructor(props) { super(props); this.renderBreakoutRooms = this.renderBreakoutRooms.bind(this); this.getBreakoutURL = this.getBreakoutURL.bind(this); this.hasBreakoutUrl = this.hasBreakoutUrl.bind(this); this.getBreakoutLabel = this.getBreakoutLabel.bind(this); this.renderDuration = this.renderDuration.bind(this); this.transferUserToBreakoutRoom = this.transferUserToBreakoutRoom.bind(this); this.changeSetTime = this.changeSetTime.bind(this); this.showSetTimeForm = this.showSetTimeForm.bind(this); this.resetSetTimeForm = this.resetSetTimeForm.bind(this); this.renderUserActions = this.renderUserActions.bind(this); this.returnBackToMeeeting = this.returnBackToMeeeting.bind(this); this.closePanel = this.closePanel.bind(this); this.handleClickOutsideDurationContainer = this.handleClickOutsideDurationContainer.bind(this); this.state = { requestedBreakoutId: '', waiting: false, generated: false, joinedAudioOnly: false, breakoutId: '', visibleSetTimeForm: false, visibleSetTimeHigherThanMeetingTimeError: false, newTime: 15, }; } componentDidMount() { if (this.panel) this.panel.firstChild.focus(); } componentDidUpdate() { const { getBreakoutRoomUrl, setBreakoutAudioTransferStatus, isMicrophoneUser, isReconnecting, breakoutRooms, } = this.props; const { waiting, requestedBreakoutId, joinedAudioOnly, generated, } = this.state; if (breakoutRooms.length === 0) { return this.closePanel(); } if (waiting && !generated) { const breakoutUrlData = getBreakoutRoomUrl(requestedBreakoutId); if (!breakoutUrlData) return false; if (breakoutUrlData.redirectToHtml5JoinURL !== '' && breakoutUrlData.redirectToHtml5JoinURL !== prevBreakoutData.redirectToHtml5JoinURL) { prevBreakoutData = breakoutUrlData; Session.set('lastBreakoutIdOpened', requestedBreakoutId); window.open(breakoutUrlData.redirectToHtml5JoinURL, '_blank'); _.delay(() => this.setState({ generated: true, waiting: false }), 1000); } } if (joinedAudioOnly && (!isMicrophoneUser || isReconnecting)) { this.clearJoinedAudioOnly(); setBreakoutAudioTransferStatus({ breakoutMeetingId: '', status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.DISCONNECTED, }); } return true; } getBreakoutURL(breakoutId) { const { requestJoinURL, getBreakoutRoomUrl } = this.props; const { waiting } = this.state; const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId); if (!breakoutRoomUrlData && !waiting) { this.setState( { waiting: true, generated: false, requestedBreakoutId: breakoutId, }, () => requestJoinURL(breakoutId), ); } if (breakoutRoomUrlData) { Session.set('lastBreakoutIdOpened', breakoutId); window.open(breakoutRoomUrlData.redirectToHtml5JoinURL, '_blank'); this.setState({ waiting: false, generated: false }); } return null; } hasBreakoutUrl(breakoutId) { const { getBreakoutRoomUrl } = this.props; const { requestedBreakoutId, generated } = this.state; const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId); if ((generated && requestedBreakoutId === breakoutId) || breakoutRoomUrlData) { return true; } return false; } getBreakoutLabel(breakoutId) { const { intl } = this.props; const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId) if (hasBreakoutUrl) { return intl.formatMessage(intlMessages.breakoutJoin); } return intl.formatMessage(intlMessages.askToJoin); } clearJoinedAudioOnly() { this.setState({ joinedAudioOnly: false }); } changeSetTime(event) { const newSetTime = Number.parseInt(event.target.value, 10) || 0; this.setState({ newTime: newSetTime >= 0 ? newSetTime : 0 }); } handleClickOutsideDurationContainer(e) { if (this.durationContainerRef) { const { x, right, y, bottom } = this.durationContainerRef.getBoundingClientRect(); if (e.clientX < x || e.clientX > right || e.clientY < y || e.clientY > bottom) { this.resetSetTimeForm(); } } } showSetTimeForm() { this.setState({ visibleSetTimeForm: true }); window.addEventListener('click', this.handleClickOutsideDurationContainer); } showSetTimeHigherThanMeetingTimeError(show) { this.setState({ visibleSetTimeHigherThanMeetingTimeError: show }); } resetSetTimeForm() { this.setState({ visibleSetTimeForm: false, newTime: 5 }); window.removeEventListener('click', this.handleClickOutsideDurationContainer); } transferUserToBreakoutRoom(breakoutId) { const { transferToBreakout } = this.props; transferToBreakout(breakoutId); this.setState({ joinedAudioOnly: true, breakoutId }); } returnBackToMeeeting(breakoutId) { const { transferUserToMeeting, meetingId } = this.props; transferUserToMeeting(breakoutId, meetingId); this.setState({ joinedAudioOnly: false, breakoutId }); } closePanel() { const { layoutContextDispatch } = this.props; layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, value: false, }); layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, value: PANELS.NONE, }); } renderUserActions(breakoutId, joinedUsers, shortName) { const { isMicrophoneUser, amIModerator, amIPresenter, intl, isUserInBreakoutRoom, forceExitAudio, rejoinAudio, setBreakoutAudioTransferStatus, getBreakoutAudioTransferStatus, } = this.props; const { joinedAudioOnly, breakoutId: _stateBreakoutId, requestedBreakoutId, waiting, } = this.state; const { breakoutMeetingId: currentAudioTransferBreakoutId, status, } = getBreakoutAudioTransferStatus(); const isInBreakoutAudioTransfer = status === AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED; const stateBreakoutId = _stateBreakoutId || currentAudioTransferBreakoutId; const moderatorJoinedAudio = isMicrophoneUser && amIModerator; const disable = waiting && requestedBreakoutId !== breakoutId; const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId); const dataTest = `${hasBreakoutUrl ? 'join' : 'askToJoin'}${shortName.replace(' ', '')}`; const audioAction = joinedAudioOnly || isInBreakoutAudioTransfer ? () => { setBreakoutAudioTransferStatus({ breakoutMeetingId: breakoutId, status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.RETURNING, }); this.returnBackToMeeeting(breakoutId); return logger.info({ logCode: 'breakoutroom_return_main_audio', extraInfo: { logType: 'user_action' }, }, 'Returning to main audio (breakout room audio closed)'); } : () => { setBreakoutAudioTransferStatus({ breakoutMeetingId: breakoutId, status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED, }); this.transferUserToBreakoutRoom(breakoutId); return logger.info({ logCode: 'breakoutroom_join_audio_from_main_room', extraInfo: { logType: 'user_action' }, }, 'joining breakout room audio (main room audio closed)'); }; return ( { isUserInBreakoutRoom(joinedUsers) ? ( {intl.formatMessage(intlMessages.alreadyConnected)} ) : ( { this.getBreakoutURL(breakoutId); // leave main room's audio, // and stops video and screenshare when joining a breakout room forceExitAudio(); logger.info({ logCode: 'breakoutroom_join', extraInfo: { logType: 'user_action' }, }, 'joining breakout room closed audio in the main room'); VideoService.storeDeviceIds(); VideoService.exitVideo(); if (amIPresenter) screenshareHasEnded(); Tracker.autorun((c) => { const selector = { meetingId: breakoutId, }; const query = Users.find(selector, { fields: { loggedOut: 1, extId: 1, }, }); const observeLogOut = (user) => { if (user?.loggedOut && user?.extId?.startsWith(Auth.userID)) { rejoinAudio(); c.stop(); } } query.observe({ added: observeLogOut, changed: observeLogOut, }); }); }} disabled={disable} /> ) } { moderatorJoinedAudio ? [ ('|'), ( ), ] : null } ); } renderBreakoutRooms() { const { breakoutRooms, intl, } = this.props; const { waiting, requestedBreakoutId, } = this.state; const { animations } = Settings.application; const roomItems = breakoutRooms.map((breakout) => ( {breakout.isDefaultName ? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence }) : breakout.shortName} ( {breakout.joinedUsers.length} ) {waiting && requestedBreakoutId === breakout.breakoutId ? ( {intl.formatMessage(intlMessages.generatingURL)} ) : this.renderUserActions( breakout.breakoutId, breakout.joinedUsers, breakout.shortName, )} {breakout.joinedUsers .sort(BreakoutRoom.sortById) .filter((value, idx, arr) => !(value.userId === (arr[idx + 1] || {}).userId)) .sort(Service.sortUsersByName) .map((u) => u.name) .join(', ')} )); return ( {roomItems} ); } renderDuration() { const { intl, breakoutRooms, amIModerator, isMeteorConnected, setBreakoutsTime, isNewTimeHigherThanMeetingRemaining, } = this.props; const { newTime, visibleSetTimeForm, visibleSetTimeHigherThanMeetingTimeError, } = this.state; return ( this.durationContainerRef = ref} > {amIModerator && visibleSetTimeForm ? (
    { this.showSetTimeHigherThanMeetingTimeError(false); if (isNewTimeHigherThanMeetingRemaining(newTime)) { this.showSetTimeHigherThanMeetingTimeError(true); } else if (setBreakoutsTime(newTime)) { this.resetSetTimeForm(); } }} /> {visibleSetTimeHigherThanMeetingTimeError ? ( {intl.formatMessage(intlMessages.setTimeHigherThanMeetingTimeError)} ) : null}
) : null}
); } render() { const { isMeteorConnected, intl, endAllBreakouts, amIModerator, isRTL, } = this.props; return ( this.panel = n}>
{ this.closePanel(); }, }} customRightButton={amIModerator && ( { this.closePanel(); endAllBreakouts(); }} isMeteorConnected={isMeteorConnected} amIModerator={amIModerator} isRTL={isRTL} /> )} /> {this.renderDuration()} {amIModerator ? ( ) : null } {amIModerator ? : null } {this.renderBreakoutRooms()} ); } } export default injectIntl(BreakoutRoom);