diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx index 240bc3c1f67235c1fca8c1c6d22e83a3ccce134c..327f22ed763adb35d79792a081ab966518ba0538 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx @@ -2,10 +2,13 @@ import React, { Component } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; import cx from 'classnames'; +import browser from 'browser-detect'; +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'; import Icon from '../../icon/component'; @@ -58,6 +61,22 @@ const intlMessages = defineMessages({ 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', + }, }); const MIN_BREAKOUT_ROOMS = 2; const MAX_BREAKOUT_ROOMS = 8; @@ -78,6 +97,11 @@ class BreakoutRoom extends Component { this.renderRoomsGrid = this.renderRoomsGrid.bind(this); this.renderBreakoutForm = this.renderBreakoutForm.bind(this); this.renderFreeJoinCheck = this.renderFreeJoinCheck.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.handleDismiss = this.handleDismiss.bind(this); this.state = { @@ -86,6 +110,8 @@ class BreakoutRoom extends Component { users: [], durationTime: 1, freeJoin: false, + formFillLevel: 1, + roomSelected: 0, preventClosing: true, valid: true, }; @@ -108,8 +134,12 @@ class BreakoutRoom extends Component { meetingName, intl, } = this.props; + const { + users, + freeJoin, + } = this.state; - if (this.state.users.length === this.getUserByRoom(0).length) { + if (users.length === this.getUserByRoom(0).length) { this.setState({ valid: false }); return; } @@ -121,11 +151,11 @@ class BreakoutRoom extends Component { 0: meetingName, 1: value, }), - freeJoin: this.state.freeJoin, + freeJoin, sequence: value, })); - createBreakoutRoom(rooms, durationTime, this.state.freeJoin); + createBreakoutRoom(rooms, durationTime, freeJoin); Session.set('isUserListOpen', true); } @@ -147,7 +177,8 @@ class BreakoutRoom extends Component { } getUserByRoom(room) { - return this.state.users.filter(user => user.room === room); + const { users } = this.state; + return users.filter(user => user.room === room); } handleDismiss() { @@ -176,11 +207,13 @@ class BreakoutRoom extends Component { } increaseDurationTime() { - this.setState({ durationTime: (1 * this.state.durationTime) + 1 }); + const { durationTime } = this.state; + this.setState({ durationTime: (1 * durationTime) + 1 }); } decreaseDurationTime() { - const number = ((1 * this.state.durationTime) - 1); + const { durationTime } = this.state; + const number = ((1 * durationTime) - 1); this.setState({ durationTime: number < 1 ? 1 : number }); } @@ -194,7 +227,10 @@ class BreakoutRoom extends Component { renderRoomsGrid() { const { intl } = this.props; - + const { + valid, + numberOfRooms, + } = this.state; const allowDrop = (ev) => { ev.preventDefault(); }; @@ -208,32 +244,33 @@ class BreakoutRoom extends Component { return ( <div className={styles.boxContainer}> - <label htmlFor="BreakoutRoom" className={!this.state.valid ? styles.changeToWarn : null}> + <label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}> <p className={styles.freeJoinLabel} > {intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })} </p> - <div className={styles.breakoutBox} onDrop={drop(0)} onDragOver={allowDrop} > + <div className={styles.breakoutBox} onDrop={drop(0)} onDragOver={allowDrop}> {this.renderUserItemByRoom(0)} </div> - <span className={this.state.valid ? styles.dontShow : styles.leastOneWarn} > + <span className={valid ? styles.dontShow : styles.leastOneWarn}> {intl.formatMessage(intlMessages.leastOneWarnBreakout)} </span> </label> { - _.range(1, this.state.numberOfRooms + 1).map(value => - ( - <label htmlFor="BreakoutRoom" key={`room-${value}`}> - <p - className={styles.freeJoinLabel} - > - {intl.formatMessage(intlMessages.roomLabel, { 0: (value) })} - </p> - <div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}> - {this.renderUserItemByRoom(value)} - </div> - </label>)) + _.range(1, numberOfRooms + 1).map(value => ( + <label htmlFor="BreakoutRoom" key={`room-${value}`}> + <p + id="BreakoutRoom" + className={styles.freeJoinLabel} + > + {intl.formatMessage(intlMessages.roomLabel, { 0: (value) })} + </p> + <div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}> + {this.renderUserItemByRoom(value)} + </div> + </label> + )) } </div> ); @@ -241,15 +278,19 @@ class BreakoutRoom extends Component { renderBreakoutForm() { const { intl } = this.props; - + const { + numberOfRooms, + durationTime, + } = this.state; return ( <div className={styles.breakoutSettings}> <label htmlFor="numberOfRooms"> <p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p> <select + id="numberOfRooms" name="numberOfRooms" className={styles.inputRooms} - value={this.state.numberOfRooms} + value={numberOfRooms} onChange={this.changeNumberOfRooms} > { @@ -257,14 +298,14 @@ class BreakoutRoom extends Component { } </select> </label> - <label htmlFor="breakoutRoomTime" > + <label htmlFor="breakoutRoomTime"> <p className={styles.labelText}>{intl.formatMessage(intlMessages.duration)}</p> <div className={styles.durationArea}> <input type="number" className={styles.duration} min={MIN_BREAKOUT_ROOMS} - value={this.state.durationTime} + value={durationTime} onChange={this.changeDurationTime} /> <span> @@ -272,7 +313,7 @@ class BreakoutRoom extends Component { key="decrease-breakout-time" exec={this.decreaseDurationTime} minBound={MIN_BREAKOUT_ROOMS} - value={this.state.durationTime} + value={durationTime} > <Icon className={styles.iconsColor} @@ -297,15 +338,32 @@ class BreakoutRoom extends Component { ); } + renderSelectUserScreen() { + const { + users, + roomSelected, + } = this.state; + return ( + <SortList + confirm={() => this.setState({ formFillLevel: 2 })} + users={users} + room={roomSelected} + onCheck={this.changeUserRoom} + onUncheck={userId => this.changeUserRoom(userId, 0)} + /> + ); + } + renderFreeJoinCheck() { const { intl } = this.props; + const { freeJoin } = this.state; return ( <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}> <input type="checkbox" className={styles.freeJoinCheckbox} onChange={this.setFreeJoin} - checked={this.state.freeJoin} + checked={freeJoin} /> {intl.formatMessage(intlMessages.freeJoinLabel)} </label> @@ -313,16 +371,21 @@ class BreakoutRoom extends Component { } renderUserItemByRoom(room) { + const { + valid, + seletedId, + } = this.state; const dragStart = (ev) => { ev.dataTransfer.setData('text', ev.target.id); this.setState({ seletedId: ev.target.id }); - if (!this.state.valid) { + if (!valid) { this.setState({ valid: true }); } }; - const dragEnd = (ev) => { + + const dragEnd = () => { this.setState({ seletedId: '' }); }; @@ -333,8 +396,8 @@ class BreakoutRoom extends Component { key={user.userId} className={cx( styles.roomUserItem, - this.state.seletedId === user.userId ? styles.selectedItem : null, - ) + seletedId === user.userId ? styles.selectedItem : null, + ) } draggable onDragStart={dragStart} @@ -344,8 +407,79 @@ class BreakoutRoom extends Component { </p>)); } + renderRoomSortList() { + const { intl } = this.props; + const { numberOfRooms } = this.state; + const onClick = roomNumber => this.setState({ formFillLevel: 3, roomSelected: roomNumber }); + return ( + <div className={styles.listContainer}> + <span> + { + new Array(numberOfRooms).fill(1).map((room, idx) => ( + <div className={styles.roomItem}> + <h2 className={styles.itemTitle}> + {intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: idx + 1 })} + </h2> + <Button + className={styles.itemButton} + label={intl.formatMessage(intlMessages.addParticipantLabel)} + size="lg" + ghost + color="primary" + onClick={() => onClick(idx + 1)} + /> + </div> + )) + } + </span> + {this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))} + </div> + ); + } + + renderDesktop() { + return [ + this.renderBreakoutForm(), + this.renderFreeJoinCheck(), + this.renderRoomsGrid(), + ]; + } + + renderMobile() { + const { intl } = this.props; + const { formFillLevel } = this.state; + if (formFillLevel === 2) { + return this.renderRoomSortList(); + } + + if (formFillLevel === 3) { + return this.renderSelectUserScreen(); + } + + return [ + this.renderBreakoutForm(), + this.renderFreeJoinCheck(), + this.renderButtonSetLevel(2, intl.formatMessage(intlMessages.nextLabel)), + ]; + } + + renderButtonSetLevel(level, label) { + return ( + <Button + color="primary" + size="lg" + label={label} + onClick={() => this.setState({ formFillLevel: level })} + /> + ); + } + render() { const { intl } = this.props; + const { preventClosing } = this.state; + + const BROWSER_RESULTS = browser(); + const isMobileBrowser = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android'); return ( <Modal @@ -360,17 +494,15 @@ class BreakoutRoom extends Component { callback: this.handleDismiss, label: intl.formatMessage(intlMessages.dismissLabel), }} - preventClosing={this.state.preventClosing} + preventClosing={preventClosing} > <div className={styles.content}> <p className={styles.subTitle}> {intl.formatMessage(intlMessages.breakoutRoomDesc)} </p> - {this.renderBreakoutForm()} - {this.renderFreeJoinCheck()} - {this.renderRoomsGrid()} + {isMobileBrowser ? this.renderMobile() : this.renderDesktop()} </div> - </Modal > + </Modal> ); } } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/sort-user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/sort-user-list/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b0f9b49f98cf87f5df9b3b2dc615928dbab816c2 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/sort-user-list/component.jsx @@ -0,0 +1,124 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import { styles } from '../styles'; + +const propTypes = { + confirm: PropTypes.func.isRequired, + users: PropTypes.arrayOf(PropTypes.object).isRequired, + room: PropTypes.number.isRequired, + onCheck: PropTypes.func, + onUncheck: PropTypes.func, +}; + +const defaultProps = { + onCheck: () => {}, + onUncheck: () => {}, +}; + +const intlMessages = defineMessages({ + breakoutRoomLabel: { + id: 'app.createBreakoutRoom.breakoutRoomLabel', + description: 'breakout room label', + }, + doneLabel: { + id: 'app.createBreakoutRoom.doneLabel', + description: 'done label', + }, +}); + +class SortUsers extends Component { + constructor(props) { + super(props); + + this.setUsers = this.setUsers.bind(this); + this.renderUserItem = this.renderUserItem.bind(this); + this.onChage = this.onChage.bind(this); + + this.state = { + users: [], + }; + } + + componentDidMount() { + const { users } = this.props; + this.setUsers(users); + } + + onChage(userId, room) { + const { + onCheck, + onUncheck, + } = this.props; + return (ev) => { + const check = ev.target.checked; + if (check) { + return onCheck(userId, room); + } + return onUncheck(userId, room); + }; + } + + setUsers(users) { + this.setState({ users: users.sort((a, b) => a.room - b.room) }); + } + + renderUserItem() { + const { room } = this.props; + const { users } = this.state; + return users + .map((user, idx) => ( + <div id={user.userId} className={styles.selectUserContainer} key={`breakout-user-${user.userId}`}> + <span className={styles.round}> + <input + type="checkbox" + id={`itemId${idx}`} + defaultChecked={user.room === room} + onChange={this.onChage(user.userId, room)} + /> + <label htmlFor={`itemId${idx}`}> + <input + type="checkbox" + id={`itemId${idx}`} + defaultChecked={user.room === room} + onChange={this.onChage(user.userId, room)} + /> + </label> + </span> + <span className={styles.textName}> + {user.userName} + {user.room && !(user.room === room) ? `\t[${user.room}]` : ''} + </span> + </div>)); + } + + render() { + const { + intl, + room, + confirm, + } = this.props; + return ( + <div className={styles.selectUserScreen}> + <header className={styles.header}> + <h2 className={styles.title}> + {intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: room })} + </h2> + <Button + className={styles.buttonAdd} + size="md" + label={intl.formatMessage(intlMessages.doneLabel)} + color="primary" + onClick={confirm} + /> + </header> + {this.renderUserItem()} + </div> + ); + } +} +SortUsers.propTypes = propTypes; +SortUsers.defaultProps = defaultProps; + +export default injectIntl(SortUsers); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.scss index fded63ce5dc8bd0e16a1398de10a7778872911df..4160d99bdbe7001d61469ad795e37e244ad6c2d2 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.scss @@ -154,6 +154,127 @@ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-i color: var(--color-white) } +/* mobile */ + +.listContainer { + display: flex; + justify-content: flex-start; + flex-direction: column; +} + +.itemTitle { + color: var(--color-blue-light); + margin: 0; +} + +.roomItem { + margin: 1rem 0 1rem 0; + +} + +.itemButton { + padding: 0; + outline: none !important; + span { + color: var(--color-blue-light); + } +} + +.selectUserScreenContainer { + position: fixed; + z-index: 1002; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, .85); +} + +.selectUserScreen { + position: fixed; + display: block; + height: 100vh; + width: 100%; + background-color: var(--color-white); + z-index: 1002; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.header { + display: flex; + padding: var(--line-height-computed) 0; + border-bottom: var(--border-size) solid var(--color-gray-lighter); + margin: 0 1rem 0 1rem; +} + +.title { + @extend %text-elipsis; + align-content: flex-end; + flex: 1; + margin: 0; + font-weight: 400; +} + +.buttonAdd { + flex: 0 1 35%; +} + +.selectUserContainer { + margin: 1.5rem 1rem; +} + +.textName { + @extend %text-elipsis; + margin-left: 1.5rem; +} + +.round { + position: relative; + + & label { + margin-top: -10px; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 50%; + cursor: pointer; + height: 28px; + left: 0; + position: absolute; + top: 0; + width: 28px; + } + + & label:after { + border: 2px solid #fff; + border-top: none; + border-right: none; + content: ""; + height: 6px; + left: 7px; + opacity: 0; + position: absolute; + top: 8px; + transform: rotate(-45deg); + width: 12px; + } + + & input[type="checkbox"] { + visibility: hidden; + } + + & input[type="checkbox"]:checked + label { + background-color: #66bb6a; + border-color: #66bb6a; + } + + & input[type="checkbox"]:checked + label:after { + opacity: 1; + } +} + .dontShow { display: none; } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss index f30735e9e4f9c2b8f408d63152a1d9b3898c9897..7f093a220baee480ee60d432bb40a52202dbdef7 100644 --- a/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/modal/fullscreen/styles.scss @@ -8,6 +8,7 @@ outline: none; @include mq($small-only) { width: 100%; + height: 100%; } } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 885ad442ed3e1bd91c38c26e2cd1b65be58d628c..e47b002d4fad3a0ac03ee5aaf3aaaba672decc2e 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -458,6 +458,7 @@ "app.videoDock.webcamUnfocusLabel": "Unfocus", "app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam", "app.createBreakoutRoom.title": "Breakout Rooms", + "app.createBreakoutRoom.breakoutRoomLabel": "Breakout Rooms {0}", "app.createBreakoutRoom.generatingURL": "Generating URL", "app.createBreakoutRoom.generatedURL": "Generated", "app.createBreakoutRoom.duration": "Duration {0}", @@ -472,6 +473,9 @@ "app.createBreakoutRoom.randomlyAssign": "Randomly Assign", "app.createBreakoutRoom.endAllBreakouts": "End All Breakout Rooms", "app.createBreakoutRoom.roomName": "{0} (Room - {1})", + "app.createBreakoutRoom.doneLabel": "Done", + "app.createBreakoutRoom.nextLabel": "Next", + "app.createBreakoutRoom.addParticipantLabel": "+ Add participant", "app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join", "app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.", "app.createBreakoutRoom.modalDesc": "Complete the steps below to create rooms in your session, To add participants to a room."