import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; import cx from 'classnames'; import deviceInfo from '/imports/utils/deviceInfo'; import Button from '/imports/ui/components/button/component'; import { Session } from 'meteor/session'; import Modal from '/imports/ui/components/modal/fullscreen/component'; import { withModalMounter } from '/imports/ui/components/modal/service'; import HoldButton from '/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component'; import SortList from './sort-user-list/component'; import styles from './styles'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; const intlMessages = defineMessages({ breakoutRoomTitle: { id: 'app.createBreakoutRoom.title', description: 'modal title', }, breakoutRoomDesc: { id: 'app.createBreakoutRoom.modalDesc', description: 'modal description', }, confirmButton: { id: 'app.createBreakoutRoom.confirm', description: 'confirm button label', }, dismissLabel: { id: 'app.presentationUploder.dismissLabel', description: 'used in the button that close modal', }, numberOfRooms: { id: 'app.createBreakoutRoom.numberOfRooms', description: 'number of rooms label', }, duration: { id: 'app.createBreakoutRoom.durationInMinutes', description: 'duration time label', }, randomlyAssign: { id: 'app.createBreakoutRoom.randomlyAssign', description: 'randomly assign label', }, roomName: { id: 'app.createBreakoutRoom.roomName', description: 'room intl to name the breakout meetings', }, freeJoinLabel: { id: 'app.createBreakoutRoom.freeJoin', description: 'free join label', }, roomLabel: { id: 'app.createBreakoutRoom.room', description: 'Room label', }, leastOneWarnBreakout: { id: 'app.createBreakoutRoom.leastOneWarnBreakout', description: 'warn message label', }, notAssigned: { id: 'app.createBreakoutRoom.notAssigned', description: 'Not assigned label', }, breakoutRoomLabel: { id: 'app.createBreakoutRoom.breakoutRoomLabel', description: 'breakout room label', }, addParticipantLabel: { id: 'app.createBreakoutRoom.addParticipantLabel', description: 'add Participant label', }, nextLabel: { id: 'app.createBreakoutRoom.nextLabel', description: 'Next label', }, backLabel: { id: 'app.audio.backLabel', description: 'Back label', }, invitationTitle: { id: 'app.invitation.title', description: 'isInvitationto breakout title', }, invitationConfirm: { id: 'app.invitation.confirm', description: 'Invitation to breakout confirm button label', }, minusRoomTime: { id: 'app.createBreakoutRoom.minusRoomTime', description: 'aria label for btn to decrease room time', }, addRoomTime: { id: 'app.createBreakoutRoom.addRoomTime', description: 'aria label for btn to increase room time', }, record: { id: 'app.createBreakoutRoom.record', description: 'label for checkbox to allow record', }, roomTime: { id: 'app.createBreakoutRoom.roomTime', description: 'used to provide current room time for aria label', }, numberOfRoomsIsValid: { id: 'app.createBreakoutRoom.numberOfRoomsError', description: 'Label an error message', }, you: { id: 'app.userList.you', description: 'Text for identifying your user', }, }); const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit; const MIN_BREAKOUT_ROOMS = 2; const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS; const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, isInvitation: PropTypes.bool.isRequired, isMe: PropTypes.func.isRequired, meetingName: PropTypes.string.isRequired, users: PropTypes.arrayOf(PropTypes.object).isRequired, createBreakoutRoom: PropTypes.func.isRequired, getUsersNotAssigned: PropTypes.func.isRequired, getBreakouts: PropTypes.func.isRequired, sendInvitation: PropTypes.func.isRequired, mountModal: PropTypes.func.isRequired, isBreakoutRecordable: PropTypes.bool.isRequired, }; class BreakoutRoom extends PureComponent { constructor(props) { super(props); this.changeNumberOfRooms = this.changeNumberOfRooms.bind(this); this.changeDurationTime = this.changeDurationTime.bind(this); this.changeUserRoom = this.changeUserRoom.bind(this); this.increaseDurationTime = this.increaseDurationTime.bind(this); this.decreaseDurationTime = this.decreaseDurationTime.bind(this); this.onCreateBreakouts = this.onCreateBreakouts.bind(this); this.setRoomUsers = this.setRoomUsers.bind(this); this.setFreeJoin = this.setFreeJoin.bind(this); this.getUserByRoom = this.getUserByRoom.bind(this); this.onAssignRandomly = this.onAssignRandomly.bind(this); this.onInviteBreakout = this.onInviteBreakout.bind(this); this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this); this.renderRoomsGrid = this.renderRoomsGrid.bind(this); this.renderBreakoutForm = this.renderBreakoutForm.bind(this); this.renderCheckboxes = this.renderCheckboxes.bind(this); this.renderRoomSortList = this.renderRoomSortList.bind(this); this.renderDesktop = this.renderDesktop.bind(this); this.renderMobile = this.renderMobile.bind(this); this.renderButtonSetLevel = this.renderButtonSetLevel.bind(this); this.renderSelectUserScreen = this.renderSelectUserScreen.bind(this); this.renderTitle = this.renderTitle.bind(this); this.handleDismiss = this.handleDismiss.bind(this); this.setInvitationConfig = this.setInvitationConfig.bind(this); this.setRecord = this.setRecord.bind(this); this.blurDurationTime = this.blurDurationTime.bind(this); this.removeRoomUsers = this.removeRoomUsers.bind(this); this.renderErrorMessages = this.renderErrorMessages.bind(this); this.renderJoinedUsers = this.renderJoinedUsers.bind(this); this.state = { numberOfRooms: MIN_BREAKOUT_ROOMS, seletedId: '', users: [], durationTime: 15, freeJoin: false, formFillLevel: 1, roomSelected: 0, preventClosing: true, valid: true, record: false, numberOfRoomsIsValid: true, breakoutJoinedUsers: null, }; this.btnLevelId = _.uniqueId('btn-set-level-'); this.handleMoveEvent = this.handleMoveEvent.bind(this); this.handleShiftUser = this.handleShiftUser.bind(this); } componentDidMount() { const { isInvitation, breakoutJoinedUsers } = this.props; this.setRoomUsers(); if (isInvitation) { this.setInvitationConfig(); } if (isInvitation) { this.setState({ breakoutJoinedUsers, }); } } handleShiftUser(activeListSibling) { const { users } = this.state; if (activeListSibling) { const text = activeListSibling.getElementsByTagName('p')[0].innerText; const roomNumber = text.match(/\d/g).join(""); users.forEach(u => { const { childNodes } = document.activeElement; if (!childNodes[childNodes.length - 1]) return; if (u.userId === childNodes[childNodes.length - 1].id) { u.room = text.substr(text.length - 1).includes(')') ? 0 : parseInt(roomNumber); } }) } } handleMoveEvent(event) { if (this.listOfUsers) { const { parentElement } = document.activeElement; if (event.key.includes('ArrowRight')) this.handleShiftUser(parentElement.nextSibling); if (event.key.includes('ArrowLeft')) this.handleShiftUser(parentElement.previousSibling); this.setRoomUsers(); } } componentWillUnmount() { if (this.listOfUsers) { for(let i = 0; i < this.listOfUsers.children.length; i++) { const roomList = this.listOfUsers.children[i].getElementsByTagName('div')[0]; roomList.removeEventListener('keydown', this.handleMoveEvent, true); } } } componentDidUpdate(prevProps, prevstate) { if (this.listOfUsers) { for(let i = 0; i < this.listOfUsers.children.length; i++) { const roomList = this.listOfUsers.children[i].getElementsByTagName('div')[0]; roomList.addEventListener('keydown', this.handleMoveEvent, true); } } const { numberOfRooms } = this.state; const { users } = this.props; const { users: prevUsers } = prevProps; if (numberOfRooms < prevstate.numberOfRooms) { this.resetUserWhenRoomsChange(numberOfRooms); } const usersCount = users.length; const prevUsersCount = prevUsers.length; if (usersCount > prevUsersCount) { this.setRoomUsers(); } if (usersCount < prevUsersCount) { this.removeRoomUsers(); } } onCreateBreakouts() { const { createBreakoutRoom, meetingName, intl, } = this.props; const { users, freeJoin, record, numberOfRoomsIsValid, } = this.state; if (users.length === this.getUserByRoom(0).length && !freeJoin) { this.setState({ valid: false }); return; } if (!numberOfRoomsIsValid) { return; } this.setState({ preventClosing: false }); const { numberOfRooms, durationTime } = this.state; const rooms = _.range(1, numberOfRooms + 1).map(value => ({ users: this.getUserByRoom(value).map(u => u.userId), name: intl.formatMessage(intlMessages.roomName, { 0: meetingName, 1: value, }), freeJoin, sequence: value, })); createBreakoutRoom(rooms, durationTime, record); Session.set('isUserListOpen', true); } onInviteBreakout() { const { getBreakouts, sendInvitation } = this.props; const { users } = this.state; const breakouts = getBreakouts(); if (users.length === this.getUserByRoom(0).length) { this.setState({ valid: false }); return; } breakouts.forEach((breakout) => { const { breakoutId } = breakout; const breakoutUsers = this.getUserByRoom(breakout.sequence); breakoutUsers.forEach(user => sendInvitation(breakoutId, user.userId)); }); this.setState({ preventClosing: false }); } onAssignRandomly() { const { numberOfRooms } = this.state; const { users } = this.state; // We only want to assign viewers so filter out the moderators. We also want to get // all users each run so that clicking the button again will reshuffle const viewers = users.filter(user => !user.isModerator); // We want to keep assigning users until all viewers have been assigned a room while (viewers.length > 0) { // We cycle through the rooms picking one user for each room so that the rooms // will have an equal number of people in each one for (let i = 1; i <= numberOfRooms && viewers.length > 0; i += 1) { // Select a random user for the room const userIdx = Math.floor(Math.random() * (viewers.length)); this.changeUserRoom(viewers[userIdx].userId, i); // Remove the picked user so they aren't selected again viewers.splice(userIdx, 1); } } } setInvitationConfig() { const { getBreakouts } = this.props; this.setState({ numberOfRooms: getBreakouts().length, formFillLevel: 2, }); } setRoomUsers() { const { users, getUsersNotAssigned } = this.props; const { users: stateUsers } = this.state; const stateUsersId = stateUsers.map(user => user.userId); const roomUsers = getUsersNotAssigned(users) .filter(user => !stateUsersId.includes(user.userId)) .map(user => ({ userId: user.userId, userName: user.name, isModerator: user.role === ROLE_MODERATOR, room: 0, })); this.setState({ users: [ ...stateUsers, ...roomUsers, ], }); } setFreeJoin(e) { this.setState({ freeJoin: e.target.checked, valid: true }); } setRecord(e) { this.setState({ record: e.target.checked }); } getUserByRoom(room) { const { users } = this.state; return users.filter(user => user.room === room); } getUsersByRoomSequence(sequence) { const { breakoutJoinedUsers } = this.state; if (!breakoutJoinedUsers) return []; return breakoutJoinedUsers.filter(room => room.sequence === sequence)[0].joinedUsers || []; } removeRoomUsers() { const { users } = this.props; const { users: stateUsers } = this.state; const userIds = users.map(user => user.userId); const removeUsers = stateUsers.filter(user => userIds.includes(user.userId)); this.setState({ users: removeUsers, }); } handleDismiss() { const { mountModal } = this.props; return new Promise((resolve) => { mountModal(null); this.setState({ preventClosing: false, }, resolve); }); } resetUserWhenRoomsChange(rooms) { const { users } = this.state; const filtredUsers = users.filter(u => u.room > rooms); filtredUsers.forEach(u => this.changeUserRoom(u.userId, 0)); } changeUserRoom(userId, room) { const { users } = this.state; const idxUser = users.findIndex(user => user.userId === userId); const usersCopy = [...users]; usersCopy[idxUser].room = room; this.setState({ users: usersCopy, valid: this.getUserByRoom(0).length !== users.length, }); } increaseDurationTime() { const { durationTime } = this.state; this.setState({ durationTime: (1 * durationTime) + 1 }); } decreaseDurationTime() { const { durationTime } = this.state; const number = ((1 * durationTime) - 1); this.setState({ durationTime: number < 1 ? 1 : number }); } changeDurationTime(event) { this.setState({ durationTime: Number.parseInt(event.target.value, 10) || '' }); } blurDurationTime(event) { const value = Number.parseInt(event.target.value, 10); this.setState({ durationTime: !(value <= 0) ? value : 1 }); } changeNumberOfRooms(event) { const numberOfRooms = Number.parseInt(event.target.value, 10); this.setState({ numberOfRooms, numberOfRoomsIsValid: numberOfRooms <= MAX_BREAKOUT_ROOMS && numberOfRooms >= MIN_BREAKOUT_ROOMS, }); } renderRoomsGrid() { const { intl, isInvitation } = this.props; const { valid, numberOfRooms, } = this.state; const rooms = (numberOfRooms > MAX_BREAKOUT_ROOMS || numberOfRooms < MIN_BREAKOUT_ROOMS) ? 0 : numberOfRooms; const allowDrop = (ev) => { ev.preventDefault(); }; const drop = room => (ev) => { ev.preventDefault(); const data = ev.dataTransfer.getData('text'); this.changeUserRoom(data, room); this.setState({ seletedId: '' }); }; return (
this.listOfUsers = r }>

{intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })}

{this.renderUserItemByRoom(0)}
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
{ _.range(1, rooms + 1).map(value => (

{intl.formatMessage(intlMessages.roomLabel, { 0: (value) })}

{this.renderUserItemByRoom(value)} {isInvitation && this.renderJoinedUsers(value)}
)) }
); } renderBreakoutForm() { const { intl, isInvitation, } = this.props; const { numberOfRooms, durationTime, numberOfRoomsIsValid, } = this.state; if (isInvitation) return null; return (

{intl.formatMessage(intlMessages.numberOfRooms)}

{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
); } renderSelectUserScreen() { const { users, roomSelected, breakoutJoinedUsers, } = this.state; const { isInvitation } = this.props; return ( this.setState({ formFillLevel: 2 })} users={users} room={roomSelected} breakoutJoinedUsers={isInvitation && breakoutJoinedUsers} onCheck={this.changeUserRoom} onUncheck={userId => this.changeUserRoom(userId, 0)} /> ); } renderCheckboxes() { const { intl, isInvitation, isBreakoutRecordable } = this.props; if (isInvitation) return null; const { freeJoin, record, } = this.state; return (
{ isBreakoutRecordable ? ( ) : null }
); } renderUserItemByRoom(room) { const { valid, seletedId, } = this.state; const { intl, isMe } = this.props; const dragStart = (ev) => { ev.dataTransfer.setData('text', ev.target.id); this.setState({ seletedId: ev.target.id }); if (!valid) { this.setState({ valid: true }); } }; const dragEnd = () => { this.setState({ seletedId: '' }); }; return this.getUserByRoom(room) .map(user => (

{user.userName} {(isMe(user.userId)) ? ` (${intl.formatMessage(intlMessages.you)})` : ''}

)); } renderJoinedUsers(room) { return this.getUsersByRoomSequence(room) .map(user => (

{user.name}

)); } renderRoomSortList() { const { intl, isInvitation } = this.props; const { numberOfRooms } = this.state; const onClick = roomNumber => this.setState({ formFillLevel: 3, roomSelected: roomNumber }); return (
{ new Array(numberOfRooms).fill(1).map((room, idx) => (

{intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: idx + 1 })}

)) }
{ isInvitation || this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
); } renderErrorMessages() { const { intl, } = this.props; const { valid, numberOfRoomsIsValid, } = this.state; return ( {!valid && ( {intl.formatMessage(intlMessages.leastOneWarnBreakout)} )} {!numberOfRoomsIsValid && ( {intl.formatMessage(intlMessages.numberOfRoomsIsValid)} )} ); } renderDesktop() { return [ this.renderBreakoutForm(), this.renderCheckboxes(), this.renderRoomsGrid(), ]; } renderMobile() { const { intl } = this.props; const { formFillLevel } = this.state; if (formFillLevel === 2) { return [ this.renderErrorMessages(), this.renderRoomSortList(), ]; } if (formFillLevel === 3) { return [ this.renderErrorMessages(), this.renderSelectUserScreen(), ]; } return [ this.renderErrorMessages(), this.renderBreakoutForm(), this.renderCheckboxes(), this.renderButtonSetLevel(2, intl.formatMessage(intlMessages.nextLabel)), ]; } renderButtonSetLevel(level, label) { return (