Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into user-manage-b

This commit is contained in:
Bobak Oftadeh 2018-11-02 09:17:07 -07:00
commit a97a1ece37
49 changed files with 1462 additions and 177 deletions

View File

@ -37,6 +37,7 @@ Meteor.startup(() => {
Session.set('isChatOpen', false);
Session.set('idChatOpen', '');
Session.set('isMeetingEnded', false);
Session.get('breakoutRoomIsOpen', false);
render(<Base />, document.getElementById('app'));
});
});

View File

@ -3,7 +3,6 @@ import clearBreakouts from '../modifiers/clearBreakouts';
export default function handleBreakoutClosed({ body }) {
const { breakoutId } = body;
check(breakoutId, String);
return clearBreakouts(breakoutId);

View File

@ -1,6 +1,11 @@
import { Meteor } from 'meteor/meteor';
import createBreakoutRoom from '/imports/api/breakouts/server/methods/createBreakout';
import mapToAcl from '/imports/startup/mapToAcl';
import requestJoinURL from './methods/requestJoinURL';
import endAllBreakouts from './methods/endAllBreakouts';
Meteor.methods({
Meteor.methods(mapToAcl(['methods.requestJoinURL', 'methods.createBreakoutRoom', 'methods.endAllBreakouts'], {
requestJoinURL,
});
createBreakoutRoom,
endAllBreakouts,
}));

View File

@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import _ from 'lodash';
export default function createBreakoutRoom(credentials, rooms, durationInMinutes, record = false) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const {
meetingId,
requesterUserId,
requesterToken,
} = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const eventName = 'CreateBreakoutRoomsCmdMsg';
const payload = {
record,
durationInMinutes,
rooms,
meetingId,
};
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,21 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
export default function endAllBreakouts(credentials) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const {
meetingId,
requesterUserId,
requesterToken,
} = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const eventName = 'EndAllBreakoutRoomsMsg';
return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, null);
}

View File

@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Breakouts from '/imports/api/breakouts';
export default function requestJoinURL(credentials, { breakoutId }) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -12,7 +12,9 @@ export default function requestJoinURL(credentials, { breakoutId }) {
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const Breakout = Breakouts.findOne({ breakoutId });
const BreakoutUser = Breakout.users.filter(user => user.userId === requesterUserId).shift();
if (BreakoutUser) return BreakoutUser.redirectToHtml5JoinURL;
const eventName = 'RequestBreakoutJoinURLReqMsg';
return RedisPubSub.publishUserMessage(
CHANNEL, eventName, meetingId, requesterUserId,

View File

@ -2,8 +2,10 @@ import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import endMeeting from './methods/endMeeting';
import toggleRecording from './methods/toggleRecording';
import transferUser from './methods/transferUser';
Meteor.methods(mapToAcl(['methods.endMeeting', 'methods.toggleRecording'], {
Meteor.methods(mapToAcl(['methods.endMeeting', 'methods.toggleRecording', 'methods.transferUser'], {
endMeeting,
toggleRecording,
transferUser,
}));

View File

@ -0,0 +1,27 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
export default function transferUser(credentials, fromMeetingId, toMeetingId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'TransferUserToMeetingRequestMsg';
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const payload = {
fromMeetingId,
toMeetingId,
userId: requesterUserId,
};
Logger.verbose(`userId ${requesterUserId} was transferred from
meeting ${fromMeetingId}' to meeting '${toMeetingId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -14,7 +14,11 @@ function meetings(credentials) {
Logger.info(`Publishing meeting =${meetingId} ${requesterUserId} ${requesterToken}`);
const selector = {
meetingId,
$or: [
{ meetingId },
{ 'meetingProp.isBreakout': true },
{ 'breakoutProps.parentId': meetingId },
],
};
const options = {

View File

@ -21,6 +21,8 @@ export default function addUserSettings(credentials, meetingId, userId, settings
'askForFeedbackOnLogout',
// BRANDING
'displayBrandingArea',
// SHORTCUTS
'shortcuts',
// KURENTO
'enableScreensharing',
'enableVideo',

View File

@ -0,0 +1,11 @@
import React from 'react';
const BreakoutRemainingTime = props => (
<span>
{props.children}
</span>
);
export default BreakoutRemainingTime;

View File

@ -0,0 +1,109 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { defineMessages, injectIntl } from 'react-intl';
import humanizeSeconds from '/imports/utils/humanizeSeconds';
import _ from 'lodash';
import BreakoutRemainingTimeComponent from './component';
const intlMessages = defineMessages({
failedMessage: {
id: 'app.failedMessage',
description: 'Notification for connecting to server problems',
},
connectingMessage: {
id: 'app.connectingMessage',
description: 'Notification message for when client is connecting to server',
},
waitingMessage: {
id: 'app.waitingMessage',
description: 'Notification message for disconnection with reconnection counter',
},
breakoutTimeRemaining: {
id: 'app.breakoutTimeRemainingMessage',
description: 'Message that tells how much time is remaining for the breakout room',
},
breakoutWillClose: {
id: 'app.breakoutWillCloseMessage',
description: 'Message that tells time has ended and breakout will close',
},
calculatingBreakoutTimeRemaining: {
id: 'app.calculatingBreakoutTimeRemaining',
description: 'Message that tells that the remaining time is being calculated',
},
});
class breakoutRemainingTimeContainer extends React.Component {
componentWillUnmount() {
clearInterval(timeRemainingInterval);
timeRemainingInterval = null;
timeRemaining = null;
}
render() {
if (_.isEmpty(this.props.message)) {
return null;
}
return (
<BreakoutRemainingTimeComponent>
{this.props.message}
</BreakoutRemainingTimeComponent>
);
}
}
let timeRemaining = 0;
const timeRemainingDep = new Tracker.Dependency();
let timeRemainingInterval = null;
const getTimeRemaining = () => {
timeRemainingDep.depend();
return timeRemaining;
};
const setTimeRemaining = (sec = 0) => {
if (sec !== timeRemaining) {
timeRemaining = sec;
timeRemainingDep.changed();
}
};
const startCounter = (sec, set, get, interval) => {
clearInterval(interval);
set(sec);
return setInterval(() => {
set(get() - 1);
}, 1000);
};
export default injectIntl(withTracker(({ breakoutRoom, intl, messageDuration }) => {
const data = {};
if (breakoutRoom) {
const roomRemainingTime = breakoutRoom.timeRemaining;
if (!timeRemainingInterval && roomRemainingTime) {
timeRemainingInterval = startCounter(
roomRemainingTime,
setTimeRemaining,
getTimeRemaining,
timeRemainingInterval,
);
}
} else if (timeRemainingInterval) {
clearInterval(timeRemainingInterval);
}
if (timeRemaining) {
if (timeRemaining > 0) {
const time = getTimeRemaining();
data.message = intl.formatMessage(messageDuration, { 0: humanizeSeconds(time) });
} else {
clearInterval(timeRemainingInterval);
data.message = intl.formatMessage(intlMessages.breakoutWillClose);
}
} else if (breakoutRoom) {
data.message = intl.formatMessage(intlMessages.calculatingBreakoutTimeRemaining);
}
return data;
})(breakoutRemainingTimeContainer));

View File

@ -0,0 +1,225 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import Button from '/imports/ui/components/button/component';
import { styles } from './styles';
import Icon from '../icon/component';
import BreakoutRoomContainer from './breakout-remaining-time/container';
const intlMessages = defineMessages({
breakoutTitle: {
id: 'app.createBreakoutRoom.title',
description: 'breakout title',
},
breakoutDuration: {
id: 'app.createBreakoutRoom.duration',
description: 'breakout duration time',
},
breakoutRoom: {
id: 'app.createBreakoutRoom.room',
description: 'breakout duration time',
},
breakoutJoin: {
id: 'app.createBreakoutRoom.join',
description: 'breakout duration time',
},
breakoutJoinAudio: {
id: 'app.createBreakoutRoom.joinAudio',
description: 'breakout duration time',
},
breakoutReturnAudio: {
id: 'app.createBreakoutRoom.returnAudio',
description: 'breakout duration time',
},
generatingURL: {
id: 'app.createBreakoutRoom.generatingURL',
description: 'breakout duration time',
},
generatedURL: {
id: 'app.createBreakoutRoom.generatedURL',
description: 'breakout duration time',
},
endAllBreakouts: {
id: 'app.createBreakoutRoom.endAllBreakouts',
description: 'breakout duration time',
},
});
class BreakoutRoom extends Component {
constructor(props) {
super(props);
this.renderBreakoutRooms = this.renderBreakoutRooms.bind(this);
this.getBreakoutURL = this.getBreakoutURL.bind(this);
this.renderDuration = this.renderDuration.bind(this);
this.transferUserToBreakoutRoom = this.transferUserToBreakoutRoom.bind(this);
this.renderUserActions = this.renderUserActions.bind(this);
this.returnBackToMeeeting = this.returnBackToMeeeting.bind(this);
this.state = {
requestedBreakoutId: '',
waiting: false,
generated: false,
joinedAudioOnly: false,
breakoutId: '',
};
}
componentDidUpdate() {
const { breakoutRoomUser } = this.props;
if (this.state.waiting && !this.state.generated) {
const breakoutUser = breakoutRoomUser(this.state.requestedBreakoutId);
if (!breakoutUser) return;
if (breakoutUser.redirectToHtml5JoinURL !== '') {
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
}
}
}
getBreakoutURL(breakoutId) {
const { requestJoinURL, breakoutRoomUser } = this.props;
const hasUser = breakoutRoomUser(breakoutId);
if (!hasUser && !this.state.waiting) {
this.setState(
{ waiting: true, requestedBreakoutId: breakoutId },
() => requestJoinURL(breakoutId),
);
}
if (hasUser) {
window.open(hasUser.redirectToHtml5JoinURL);
this.setState({ waiting: false, generated: false });
}
return null;
}
transferUserToBreakoutRoom(breakoutId) {
const { transferToBreakout, meetingId } = this.props;
transferToBreakout(meetingId, breakoutId);
this.setState({ joinedAudioOnly: true, breakoutId });
}
returnBackToMeeeting(breakoutId) {
const { transferUserToMeeting, meetingId } = this.props;
transferUserToMeeting(breakoutId, meetingId);
this.setState({ joinedAudioOnly: false, breakoutId });
}
renderUserActions(breakoutId) {
const {
isMicrophoneUser,
isPresenter,
intl,
} = this.props;
const {
joinedAudioOnly,
breakoutId: stateBreakoutId,
generated,
requestedBreakoutId,
waiting,
} = this.state;
const presenterJoinedAudio = isMicrophoneUser && isPresenter;
const disable = waiting && requestedBreakoutId !== breakoutId;
const audioAction = joinedAudioOnly ?
() => this.returnBackToMeeeting(breakoutId) :
() => this.transferUserToBreakoutRoom(breakoutId);
return (
<div className={styles.breakoutActions}>
<Button
label={generated && requestedBreakoutId === breakoutId
? intl.formatMessage(intlMessages.generatedURL)
: intl.formatMessage(intlMessages.breakoutJoin)}
onClick={() => this.getBreakoutURL(breakoutId)}
disabled={disable}
className={styles.joinButton}
/>
{
presenterJoinedAudio ?
[
('|'),
(
<Button
label={
presenterJoinedAudio &&
stateBreakoutId === breakoutId &&
joinedAudioOnly
?
intl.formatMessage(intlMessages.breakoutReturnAudio) :
intl.formatMessage(intlMessages.breakoutJoinAudio)
}
className={styles.button}
onClick={audioAction}
/>
),
]
: null
}
</div>
);
}
renderBreakoutRooms() {
const {
breakoutRooms,
intl,
} = this.props;
const roomItems = breakoutRooms.map(item => (
<div className={styles.content} key={`breakoutRoomList-${item.breakoutId}`}>
<span>{intl.formatMessage(intlMessages.breakoutRoom, item.sequence.toString())}</span>
{this.state.waiting && this.state.requestedBreakoutId === item.breakoutId ? (
<span>
{intl.formatMessage(intlMessages.generatingURL)}
<span className={styles.connectingAnimation} />
</span>
) : this.renderUserActions(item.breakoutId)}
</div>
));
return roomItems;
}
renderDuration() {
const { breakoutRooms } = this.props;
return (
<span className={styles.duration}>
<BreakoutRoomContainer
messageDuration={intlMessages.breakoutDuration}
breakoutRoom={breakoutRooms[0]}
/>
</span>
);
}
render() {
const {
intl, endAllBreakouts, breakoutRooms, isModerator, closeBreakoutPanel,
} = this.props;
if (breakoutRooms.length <= 0) return null;
return (
<div className={styles.panel}>
<div className={styles.header} role="button" onClick={closeBreakoutPanel} >
<span >
<Icon iconName="left_arrow" />
{intl.formatMessage(intlMessages.breakoutTitle)}
</span>
</div>
{this.renderBreakoutRooms()}
{this.renderDuration()}
{
isModerator ?
(
<Button
color="primary"
size="lg"
label={intl.formatMessage(intlMessages.endAllBreakouts)}
className={styles.endButton}
onClick={endAllBreakouts}
/>
) : null
}
</div>
);
}
}
export default injectIntl(BreakoutRoom);

View File

@ -0,0 +1,41 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import AudioService from '/imports/ui/components/audio/service';
import BreakoutComponent from './component';
import Service from './service';
const BreakoutContainer = props => <BreakoutComponent {...props} />;
export default withTracker((props) => {
const {
endAllBreakouts,
requestJoinURL,
findBreakouts,
breakoutRoomUser,
transferUserToMeeting,
transferToBreakout,
meetingId,
isPresenter,
isModerator,
closeBreakoutPanel,
} = Service;
const breakoutRooms = findBreakouts();
const isMicrophoneUser = AudioService.isConnected() && !AudioService.isListenOnly();
return {
...props,
breakoutRooms,
endAllBreakouts,
requestJoinURL,
breakoutRoomUser,
transferUserToMeeting,
transferToBreakout,
isMicrophoneUser,
meetingId,
isPresenter: isPresenter(),
isModerator: isModerator(),
closeBreakoutPanel,
};
})(BreakoutContainer);

View File

@ -0,0 +1,79 @@
import Breakouts from '/imports/api/breakouts';
import Meetings from '/imports/api/meetings';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';
import { Session } from 'meteor/session';
import Users from '/imports/api/users';
import mapUser from '/imports/ui/services/user/mapUser';
const findBreakouts = () => {
const BreakoutRooms = Breakouts.find({
parentMeetingId: Auth.meetingID,
}, {
sort: {
sequence: 1,
},
}).fetch();
return BreakoutRooms;
};
const breakoutRoomUser = (breakoutId) => {
const breakoutRooms = findBreakouts();
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
const breakoutUser = breakoutRoom.users.filter(user => user.userId === Auth.userID).shift();
return breakoutUser;
};
const endAllBreakouts = () => {
makeCall('endAllBreakouts');
};
const requestJoinURL = (breakoutId) => {
makeCall('requestJoinURL', {
breakoutId,
});
};
const transferUserToMeeting = (fromMeetingId, toMeetingId) => makeCall('transferUser', fromMeetingId, toMeetingId);
const transferToBreakout = (fromMeetingId, breakoutId) => {
const breakoutRooms = findBreakouts();
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
const breakoutMeeting = Meetings.findOne({
$and: [
{ 'breakoutProps.sequence': breakoutRoom.sequence },
{ 'breakoutProps.parentId': breakoutRoom.parentMeetingId },
{ 'meetingProp.isBreakout': true },
],
});
transferUserToMeeting(fromMeetingId, breakoutMeeting.meetingId);
};
const isPresenter = () => {
const User = Users.findOne({ intId: Auth.userID });
const mappedUser = mapUser(User);
return mappedUser.isPresenter;
};
const isModerator = () => {
const User = Users.findOne({ intId: Auth.userID });
const mappedUser = mapUser(User);
return mappedUser.isModerator;
};
const closeBreakoutPanel = () => Session.set('breakoutRoomIsOpen', false);
export default {
findBreakouts,
endAllBreakouts,
requestJoinURL,
breakoutRoomUser,
transferUserToMeeting,
transferToBreakout,
meetingId: Auth.meetingID,
isPresenter,
closeBreakoutPanel,
isModerator,
};

View File

@ -0,0 +1,103 @@
@import "/imports/ui/stylesheets/variables/_all";
.panel {
background-color: #fff;
padding: var(--md-padding-x);
display: flex;
flex-grow: 1;
flex-direction: column;
overflow: hidden;
height: 100vh;
}
.link {
text-decoration: none;
}
.breakoutActions
.content,
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.header {
margin-bottom: 2rem;
cursor: pointer;
}
.content {
font-size: var(--font-size-small);
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 1rem;
font-weight: bold;
}
.breakoutActions {
font-weight: var(--headings-font-weight);
color: var(--color-primary);
& > button {
padding: 0 0 0 .5rem;
}
}
.joinButton,
.button {
flex: 0 1 48%;
color: var(--color-primary);
margin: 0;
font-weight: inherit;
}
.joinButton {
padding: 0 .5rem 0 .5rem !important;
}
.endButton {
padding: .5rem;
font-weight: var(--headings-font-weight) !important;
border-radius: .2rem;
font-size: var(--font-size-small);
}
.overlayContainer {
position: fixed;
top: 0;
left: 0;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
color: var(--color-gray);
}
.connectingAnimation{
&:after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: ellipsis steps(4,end) 900ms infinite;
content: "\2026"; /* ascii code for the ellipsis character */
width: 0;
margin-right: 1.25em;
}
}
@keyframes ellipsis {
to {
width: 1.25em;
margin-right: 0;
}
}
.duration {
display: flex;
align-self: center;
margin: .5rem 0 .5rem 0;
}

View File

@ -9,6 +9,8 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import { withModalMounter } from '/imports/ui/components/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import BreakoutRoom from '../create-breakout-room/component';
import { styles } from '../styles';
const propTypes = {
@ -54,21 +56,28 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.stopRecording',
description: 'stop recording option',
},
createBreakoutRoom: {
id: 'app.actionsBar.actionsDropdown.createBreakoutRoom',
description: 'Create breakout room option',
},
createBreakoutRoomDesc: {
id: 'app.actionsBar.actionsDropdown.createBreakoutRoomDesc',
description: 'Description of create breakout room option',
},
});
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const OPEN_ACTIONS_AK = SHORTCUTS_CONFIG.openActions.accesskey;
class ActionsDropdown extends Component {
constructor(props) {
super(props);
this.handlePresentationClick = this.handlePresentationClick.bind(this);
this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
}
componentWillMount() {
this.presentationItemId = _.uniqueId('action-item-');
this.videoItemId = _.uniqueId('action-item-');
this.recordId = _.uniqueId('action-item-');
this.createBreakoutRoomId = _.uniqueId('action-item-');
}
componentWillUpdate(nextProps) {
@ -88,6 +97,9 @@ class ActionsDropdown extends Component {
isRecording,
record,
toggleRecording,
meetingIsBreakout,
hasBreakoutRoom,
meetingName,
} = this.props;
return _.compact([
@ -111,18 +123,36 @@ class ActionsDropdown extends Component {
onClick={toggleRecording}
/>
: null),
(isUserPresenter && !meetingIsBreakout && !hasBreakoutRoom ?
<DropdownListItem
icon="rooms"
label={intl.formatMessage(intlMessages.createBreakoutRoom)}
description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)}
key={this.createBreakoutRoomId}
onClick={this.handleCreateBreakoutRoomClick}
/>
: null),
]);
}
handlePresentationClick() {
this.props.mountModal(<PresentationUploaderContainer />);
}
handleCreateBreakoutRoomClick() {
const {
createBreakoutRoom,
mountModal,
meetingName,
} = this.props;
mountModal(<BreakoutRoom createBreakoutRoom={createBreakoutRoom} meetingName={meetingName} />);
}
render() {
const {
intl,
isUserPresenter,
isUserModerator,
shortcuts: OPEN_ACTIONS_AK,
} = this.props;
const availableActions = this.getAvailableActions();
@ -156,4 +186,4 @@ class ActionsDropdown extends Component {
ActionsDropdown.propTypes = propTypes;
export default withModalMounter(injectIntl(ActionsDropdown));
export default withShortcutHelper(withModalMounter(injectIntl(ActionsDropdown)), 'openActions');

View File

@ -20,6 +20,10 @@ class ActionsBar extends React.PureComponent {
toggleRecording,
screenSharingCheck,
enableVideo,
createBreakoutRoom,
meetingIsBreakout,
hasBreakoutRoom,
meetingName,
} = this.props;
const {
@ -42,6 +46,10 @@ class ActionsBar extends React.PureComponent {
isRecording,
record,
toggleRecording,
createBreakoutRoom,
meetingIsBreakout,
hasBreakoutRoom,
meetingName,
}}
/>
</div>

View File

@ -13,11 +13,15 @@ export default withTracker(() => ({
isUserModerator: Service.isUserModerator(),
handleExitVideo: () => VideoService.exitVideo(),
handleJoinVideo: () => VideoService.joinVideo(),
handleShareScreen: (onFail) => shareScreen(onFail),
handleShareScreen: onFail => shareScreen(onFail),
handleUnshareScreen: () => unshareScreen(),
isVideoBroadcasting: isVideoBroadcasting(),
recordSettingsList: Service.recordSettingsList(),
toggleRecording: Service.toggleRecording,
screenSharingCheck: getFromUserSettings('enableScreensharing', Meteor.settings.public.kurento.enableScreensharing),
enableVideo: getFromUserSettings('enableVideo', Meteor.settings.public.kurento.enableVideo),
createBreakoutRoom: Service.createBreakoutRoom,
meetingIsBreakout: Service.meetingIsBreakout(),
hasBreakoutRoom: Service.hasBreakoutRoom(),
meetingName: Service.meetingName(),
}))(ActionsBarContainer);

View File

@ -0,0 +1,159 @@
import React, { Component } from 'react';
import Modal from '/imports/ui/components/modal/fullscreen/component';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import HoldButton from '/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component';
import { styles } from './styles';
import Icon from '../../icon/component';
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',
},
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',
},
});
const BREAKOUT_CONFIG = Meteor.settings.public.breakout;
const MIN_BREAKOUT_ROOMS = BREAKOUT_CONFIG.rooms.min;
const MAX_BREAKOUT_ROOMS = BREAKOUT_CONFIG.rooms.max;
class BreakoutRoom extends Component {
constructor(props) {
super(props);
this.changeNumberOfRooms = this.changeNumberOfRooms.bind(this);
this.changeDurationTime = this.changeDurationTime.bind(this);
this.increaseDurationTime = this.increaseDurationTime.bind(this);
this.decreaseDurationTime = this.decreaseDurationTime.bind(this);
this.onCreateBreakouts = this.onCreateBreakouts.bind(this);
this.state = {
numberOfRooms: MIN_BREAKOUT_ROOMS,
durationTime: 1,
freeJoin: true,
};
}
onCreateBreakouts() {
const {
createBreakoutRoom,
meetingName,
intl
} = this.props;
const { numberOfRooms, durationTime } = this.state;
const rooms = _.range(1, numberOfRooms + 1).map(value => ({
users: [],
name: intl.formatMessage(intlMessages.roomName, {
0: meetingName,
1: value,
}),
freeJoin: this.state.freeJoin,
sequence: value,
}));
createBreakoutRoom(rooms, durationTime, true);
}
changeNumberOfRooms(event) {
this.setState({ numberOfRooms: Number.parseInt(event.target.value, 10) });
}
changeDurationTime(event) {
this.setState({ durationTime: Number.parseInt(event.target.value, 10) || '' });
}
increaseDurationTime() {
this.setState({ durationTime: (1 * this.state.durationTime) + 1 });
}
decreaseDurationTime() {
const number = ((1 * this.state.durationTime) - 1);
this.setState({ durationTime: number < 1 ? 1 : number });
}
render() {
const { intl } = this.props;
return (
<Modal
title={intl.formatMessage(intlMessages.breakoutRoomTitle)}
confirm={
{
label: intl.formatMessage(intlMessages.confirmButton),
callback: this.onCreateBreakouts,
}
}
>
<div className={styles.content}>
<p className={styles.subTitle}>
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
</p>
<div className={styles.breakoutSettings}>
<label>
<p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p>
<select name="numberOfRooms" className={styles.inputRooms} value={this.state.numberOfRooms} onChange={this.changeNumberOfRooms}>
{
_.range(MIN_BREAKOUT_ROOMS, MAX_BREAKOUT_ROOMS + 1).map(item => (<option key={_.uniqueId('value-')}>{item}</option>))
}
</select>
</label>
<label >
<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} onChange={this.changeDurationTime} />
<span>
<HoldButton
key="decrease-breakout-time"
exec={this.decreaseDurationTime}
minBound={MIN_BREAKOUT_ROOMS}
value={this.state.durationTime}
>
<Icon
className={styles.iconsColor}
iconName="substract"
/>
</HoldButton>
<HoldButton
key="increase-breakout-time"
exec={this.increaseDurationTime}
>
<Icon
className={styles.iconsColor}
iconName="add"
/>
</HoldButton>
</span>
</div>
</label>
<p className={styles.randomText}>{intl.formatMessage(intlMessages.randomlyAssign)}</p>
</div>
</div>
</Modal >
);
}
}
export default injectIntl(BreakoutRoom);

View File

@ -0,0 +1,77 @@
@import "/imports/ui/stylesheets/variables/_all";
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.subTitle {
font-size: var(--font-size-base);
text-align: justify;
color: var(--color-gray-light);
}
.breakoutSettings {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-gap: 1rem;
@include mq($small-only) {
grid-template-columns: 1fr ;
grid-template-rows: 1fr 1fr 1fr;
flex-direction: column;
}
}
.content {
display: flex;
flex-direction: column;
}
.labelText {
color: var(--color-gray);
white-space: nowrap;
margin-bottom: .5rem;
}
.duration,
.inputRooms {
background-color: var(--color-white);
color: var(--color-gray);
border: 1px solid var(--color-gray-lighter);
border-radius: var(--border-radius);
width: 100%;
padding-top: .25rem;
padding-bottom: .25rem;
padding: .25rem 0 .25rem .25rem;
}
.duration {
width: 50%;
text-align: center;
padding: .25rem;
&::placeholder {
color: var(--color-gray);
opacity: 1;
}
}
.iconsColor {
cursor: pointer;
color: var(--color-gray-light);
font-size: var(--font-size-large);
@include mq($small-only) {
font-size: 2rem;
margin-left: .5rem;
}
}
.durationArea {
display: flex;
align-items: center;
justify-content: space-between;
}
.randomText {
color: var(--color-primary);
font-size: var(--font-size-small);
white-space: nowrap;
margin-bottom: .5rem;
align-self: flex-end;
}

View File

@ -2,10 +2,15 @@ import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/api/meetings';
import Breakouts from '/imports/api/breakouts';
export default {
isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
meetingIsBreakout: () => Meetings.findOne({ meetingId: Auth.meetingID }).meetingProp.isBreakout,
meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID }).meetingProp.name,
hasBreakoutRoom: () => Breakouts.find({ parentMeetingId: Auth.meetingID }).fetch().length > 0,
toggleRecording: () => makeCall('toggleRecording'),
createBreakoutRoom: (numberOfRooms, durationInMinutes, freeJoin = true, record = false) => makeCall('createBreakoutRoom', numberOfRooms, durationInMinutes, freeJoin, record),
};

View File

@ -6,6 +6,7 @@ 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 ToastContainer from '../toast/container';
import ModalContainer from '../modal/container';
import NotificationsBarContainer from '../notifications-bar/container';
@ -166,6 +167,18 @@ class App extends Component {
);
}
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;
@ -315,6 +328,7 @@ class App extends Component {
{enableResize ? this.renderUserListResizable() : this.renderUserList()}
{userListIsOpen && enableResize ? <div className={styles.userlistPad} /> : null}
{enableResize ? this.renderChatResizable() : this.renderChat()}
{this.renderBreakoutRoom()}
{this.renderSidebar()}
</section>
<ModalContainer />

View File

@ -15,6 +15,7 @@ import {
getFontSize,
getCaptionsStatus,
meetingIsBreakout,
getBreakoutRooms,
} from './service';
import { withModalMounter } from '../modal/service';
@ -109,7 +110,9 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
return {
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'),
customStyle: getFromUserSettings('customStyle', false),
customStyleUrl: getFromUserSettings('customStyleUrl', false),

View File

@ -12,8 +12,10 @@ const getFontSize = () => {
return applicationSettings ? applicationSettings.fontSize : '16px';
};
const getBreakoutRooms = () => Breakouts.find().fetch();
function meetingIsBreakout() {
const breakouts = Breakouts.find().fetch();
const breakouts = getBreakoutRooms();
return (breakouts && breakouts.some(b => b.breakoutId === Auth.meetingID));
}
@ -21,4 +23,5 @@ export {
getCaptionsStatus,
getFontSize,
meetingIsBreakout,
getBreakoutRooms,
};

View File

@ -80,14 +80,6 @@
background: linear-gradient(to top, rgba(0, 0, 0, .45) -20%, transparent 20%);
}
&:hover,
&:active {
&:before,
&:after {
opacity: 1;
}
}
@include mq($medium-up) {
flex: 5;
order: 2;
@ -126,7 +118,7 @@
.compact {
flex-basis: 4.6rem;
}
.breakoutRoom,
.chat {
@extend %full-page;
@ -149,6 +141,15 @@
}
}
.breakoutRoom {
height: 100%;
width: 20vw;
background-color: var(--color-white);
@include mq($small-only) {
width: auto;
}
}
.sidebar {
@extend %full-page;
z-index: 4;

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import cx from 'classnames';
import { defineMessages, intlShape, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { styles } from './styles';
const intlMessages = defineMessages({
@ -41,11 +42,6 @@ const defaultProps = {
unmute: false,
};
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const JOIN_AUDIO_AK = SHORTCUTS_CONFIG.joinAudio.accesskey;
const LEAVE_AUDIO_AK = SHORTCUTS_CONFIG.leaveAudio.accesskey;
const MUTE_UNMUTE_AK = SHORTCUTS_CONFIG.toggleMute.accesskey;
const AudioControls = ({
handleToggleMuteMicrophone,
handleJoinAudio,
@ -56,6 +52,7 @@ const AudioControls = ({
glow,
join,
intl,
shortcuts,
}) => (
<span className={styles.container}>
{mute ?
@ -70,7 +67,7 @@ const AudioControls = ({
icon={unmute ? 'mute' : 'unmute'}
size="lg"
circle
accessKey={MUTE_UNMUTE_AK}
accessKey={shortcuts.toggleMute}
/> : null}
<Button
className={styles.button}
@ -83,11 +80,11 @@ const AudioControls = ({
icon={join ? 'audio_off' : 'audio_on'}
size="lg"
circle
accessKey={join ? LEAVE_AUDIO_AK : JOIN_AUDIO_AK}
accessKey={join ? shortcuts.leaveAudio : shortcuts.joinAudio}
/>
</span>);
AudioControls.propTypes = propTypes;
AudioControls.defaultProps = defaultProps;
export default injectIntl(AudioControls);
export default withShortcutHelper(injectIntl(AudioControls), ['joinAudio', 'leaveAudio', 'toggleMute']);

View File

@ -4,11 +4,11 @@ import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/button/component';
import { Session } from 'meteor/session';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { styles } from './styles';
import MessageForm from './message-form/component';
import MessageList from './message-list/component';
import ChatDropdown from './chat-dropdown/component';
import Icon from '../icon/component';
const ELEMENT_ID = 'chat-messages';
@ -23,10 +23,6 @@ const intlMessages = defineMessages({
},
});
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const HIDE_CHAT_AK = SHORTCUTS_CONFIG.hidePrivateChat.accesskey;
const CLOSE_CHAT_AK = SHORTCUTS_CONFIG.closePrivateChat.accesskey;
const Chat = (props) => {
const {
chatID,
@ -42,8 +38,12 @@ const Chat = (props) => {
maxMessageLength,
actions,
intl,
shortcuts,
} = props;
const HIDE_CHAT_AK = shortcuts.hidePrivateChat;
const CLOSE_CHAT_AK = shortcuts.closePrivateChat;
return (
<div
data-test="publicChat"
@ -108,7 +108,7 @@ const Chat = (props) => {
);
};
export default injectWbResizeEvent(injectIntl(Chat));
export default withShortcutHelper(injectWbResizeEvent(injectIntl(Chat)), ['hidePrivateChat', 'closePrivateChat']);
const propTypes = {
chatID: PropTypes.string.isRequired,

View File

@ -10,6 +10,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import { withModalMounter } from '/imports/ui/components/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { defineMessages, injectIntl } from 'react-intl';
import { styles } from './styles.scss';
import Button from '../button/component';
@ -44,20 +45,19 @@ const intlMessages = defineMessages({
});
const propTypes = {
presentationTitle: PropTypes.string.isRequired,
hasUnreadMessages: PropTypes.bool.isRequired,
beingRecorded: PropTypes.object.isRequired,
presentationTitle: PropTypes.string,
hasUnreadMessages: PropTypes.bool,
beingRecorded: PropTypes.object,
shortcuts: PropTypes.string,
};
const defaultProps = {
presentationTitle: 'Default Room Title',
hasUnreadMessages: false,
beingRecorded: false,
shortcuts: '',
};
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const TOGGLE_USERLIST_AK = SHORTCUTS_CONFIG.toggleUserList.accesskey;
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) =>
mountModal(<BreakoutJoinConfirmation
breakout={breakout}
@ -82,7 +82,6 @@ class NavBar extends Component {
componentDidUpdate(oldProps) {
const {
breakouts,
getBreakoutJoinURL,
isBreakoutRoom,
mountModal,
} = this.props;
@ -174,7 +173,11 @@ class NavBar extends Component {
render() {
const {
hasUnreadMessages, beingRecorded, isExpanded, intl,
hasUnreadMessages,
beingRecorded,
isExpanded,
intl,
shortcuts: TOGGLE_USERLIST_AK,
} = this.props;
const recordingMessage = beingRecorded.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff';
@ -223,4 +226,4 @@ class NavBar extends Component {
NavBar.propTypes = propTypes;
NavBar.defaultProps = defaultProps;
export default withModalMounter(injectIntl(NavBar));
export default withShortcutHelper(withModalMounter(injectIntl(NavBar)), 'toggleUserList');

View File

@ -3,12 +3,9 @@ import { defineMessages, injectIntl } from 'react-intl';
import cx from 'classnames';
import _ from 'lodash';
import { withModalMounter } from '/imports/ui/components/modal/service';
import LogoutConfirmationContainer from '/imports/ui/components/logout-confirmation/container';
import AboutContainer from '/imports/ui/components/about/container';
import SettingsMenuContainer from '/imports/ui/components/settings/container';
import Button from '/imports/ui/components/button/component';
import Dropdown from '/imports/ui/components/dropdown/component';
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
@ -17,6 +14,7 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { styles } from '../styles';
@ -83,9 +81,6 @@ const intlMessages = defineMessages({
},
});
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const OPEN_OPTIONS_AK = SHORTCUTS_CONFIG.openOptions.accesskey;
class SettingsDropdown extends Component {
constructor(props) {
super(props);
@ -210,7 +205,10 @@ class SettingsDropdown extends Component {
}
render() {
const { intl } = this.props;
const {
intl,
shortcuts: OPEN_OPTIONS_AK,
} = this.props;
return (
<Dropdown
@ -245,4 +243,4 @@ class SettingsDropdown extends Component {
}
}
export default withModalMounter(injectIntl(SettingsDropdown));
export default withShortcutHelper(withModalMounter(injectIntl(SettingsDropdown)), 'openOptions');

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import { styles } from '../styles.scss';
import HoldButton from './holdButton/component';
const DELAY_MILLISECONDS = 200;
const STEP_TIME = 100;
@ -150,21 +151,27 @@ export default class ZoomTool extends Component {
return (
[
ZoomTool.renderAriaLabelsDescs(),
(<Button
key="zoom-tool-1"
aria-labelledby="zoomInLabel"
aria-describedby="zoomInDesc"
role="button"
label="-"
icon="minus"
onClick={() => this.decrement()}
disabled={(value <= minBound)}
onMouseDown={() => this.mouseDownHandler(false)}
onMouseUp={this.mouseUpHandler}
onMouseLeave={this.mouseUpHandler}
className={styles.prevSlide}
hideLabel
/>),
(
<HoldButton
key="zoom-tool-1"
exec={this.decrement}
value={value}
minBound={minBound}
>
<Button
key="zoom-tool-1"
aria-labelledby="zoomInLabel"
aria-describedby="zoomInDesc"
role="button"
label="-"
icon="minus"
onClick={() => {}}
disabled={(value <= minBound)}
className={styles.prevSlide}
hideLabel
/>
</HoldButton>
),
(
<span
key="zoom-tool-2"
@ -175,21 +182,27 @@ export default class ZoomTool extends Component {
{`${this.state.value}%`}
</span>
),
(<Button
key="zoom-tool-3"
aria-labelledby="zoomOutLabel"
aria-describedby="zoomOutDesc"
role="button"
label="+"
icon="plus"
onClick={() => this.increment()}
disabled={(value >= maxBound)}
onMouseDown={() => this.mouseDownHandler(true)}
onMouseUp={this.mouseUpHandler}
onMouseLeave={this.mouseUpHandler}
className={styles.skipSlide}
hideLabel
/>),
(
<HoldButton
key="zoom-tool-3"
exec={this.increment}
value={value}
maxBound={maxBound}
>
<Button
key="zoom-tool-3"
aria-labelledby="zoomOutLabel"
aria-describedby="zoomOutDesc"
role="button"
label="+"
icon="plus"
onClick={() => {}}
disabled={(value >= maxBound)}
className={styles.skipSlide}
hideLabel
/>
</HoldButton>
),
]
);
}

View File

@ -0,0 +1,125 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
const DELAY_MILLISECONDS = 300;
const STEP_TIME = 100;
class HoldDownButton extends Component {
constructor(props) {
super(props);
this.mouseDownHandler = this.mouseDownHandler.bind(this);
this.mouseUpHandler = this.mouseUpHandler.bind(this);
this.touchStart = this.touchStart.bind(this);
this.touchEnd = this.touchEnd.bind(this);
this.execInterval = this.execInterval.bind(this);
this.onClick = this.onClick.bind(this);
this.setInt = 0;
this.state = {
mouseHolding: false,
};
}
onClick() {
const {
exec,
minBound,
maxBound,
value,
} = this.props;
const bounds = (value === maxBound) || (value === minBound);
if (bounds) return;
exec();
}
execInterval() {
const interval = () => {
clearInterval(this.setInt);
this.setInt = setInterval(this.onClick, STEP_TIME);
};
setTimeout(() => {
if (this.state.mouseHolding) {
interval();
}
}, DELAY_MILLISECONDS);
}
mouseDownHandler() {
this.setState({
...this.state,
mouseHolding: true,
}, () => {
this.execInterval();
});
}
mouseUpHandler() {
this.setState({
...this.state,
mouseHolding: false,
}, () => clearInterval(this.setInt));
}
touchStart() {
this.setState({
...this.state,
mouseHolding: true,
}, () => {
this.execInterval();
});
}
touchEnd() {
this.setState({
...this.state,
mouseHolding: false,
}, () => clearInterval(this.setInt));
}
render() {
const {
key,
className,
children,
} = this.props;
return (
<span
role="button"
key={key}
onClick={this.onClick}
onMouseDown={this.mouseDownHandler}
onMouseUp={this.mouseUpHandler}
onTouchStart={this.touchStart}
onTouchEnd={this.touchEnd}
onMouseLeave={this.mouseUpHandler}
className={className}
>
{children}
</span>
);
}
}
const defaultProps = {
exec: () => {},
minBound: null,
maxBound: Infinity,
key: _.uniqueId('holdButton-'),
value: 0,
};
const propTypes = {
key: PropTypes.string,
exec: PropTypes.func.isRequired,
minBound: PropTypes.number,
maxBound: PropTypes.number,
children: PropTypes.node.isRequired,
};
HoldDownButton.defaultProps = propTypes;
HoldDownButton.defaultProps = defaultProps;
export default HoldDownButton;

View File

@ -1,9 +1,11 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import browser from 'browser-detect';
import Modal from '/imports/ui/components/modal/simple/component';
import _ from 'lodash';
import { styles } from './styles';
import withShortcutHelper from './service';
const intlMessages = defineMessages({
title: {
@ -72,60 +74,69 @@ const intlMessages = defineMessages({
},
});
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const ShortcutHelpComponent = (props) => {
const { intl, shortcuts } = props;
const { name } = browser();
class ShortcutHelpComponent extends Component {
render() {
const { intl } = this.props;
const shortcuts = Object.values(SHORTCUTS_CONFIG);
const { name } = browser();
let accessMod = null;
let accessMod = null;
switch (name) {
case 'chrome':
case 'edge':
accessMod = 'Alt';
break;
case 'firefox':
accessMod = 'Alt + Shift';
break;
case 'safari':
case 'crios':
case 'fxios':
accessMod = 'Control + Alt';
break;
}
return (
<Modal
title={intl.formatMessage(intlMessages.title)}
dismiss={{
label: intl.formatMessage(intlMessages.closeLabel),
description: intl.formatMessage(intlMessages.closeDesc),
}}
>
{ !accessMod ? <p>{intl.formatMessage(intlMessages.accessKeyNotAvailable)}</p> :
<span>
<table className={styles.shortcutTable}>
<tbody>
<tr>
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
<th>{intl.formatMessage(intlMessages.functionLabel)}</th>
</tr>
{shortcuts.map(shortcut => (
<tr key={_.uniqueId('hotkey-item-')}>
<td className={styles.keyCell}>{`${accessMod} + ${shortcut.accesskey}`}</td>
<td className={styles.descCell}>{intl.formatMessage(intlMessages[`${shortcut.descId}`])}</td>
</tr>
))}
</tbody>
</table>
</span>
}
</Modal>
);
switch (name) {
case 'chrome':
case 'edge':
accessMod = 'Alt';
break;
case 'firefox':
accessMod = 'Alt + Shift';
break;
case 'safari':
case 'crios':
case 'fxios':
accessMod = 'Control + Alt';
break;
default:
break;
}
}
export default injectIntl(ShortcutHelpComponent);
return (
<Modal
title={intl.formatMessage(intlMessages.title)}
dismiss={{
label: intl.formatMessage(intlMessages.closeLabel),
description: intl.formatMessage(intlMessages.closeDesc),
}}
>
{!accessMod ? <p>{intl.formatMessage(intlMessages.accessKeyNotAvailable)}</p> :
<span>
<table className={styles.shortcutTable}>
<tbody>
<tr>
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
<th>{intl.formatMessage(intlMessages.functionLabel)}</th>
</tr>
{shortcuts.map(shortcut => (
<tr key={_.uniqueId('hotkey-item-')}>
<td className={styles.keyCell}>{`${accessMod} + ${shortcut.accesskey}`}</td>
<td className={styles.descCell}>{intl.formatMessage(intlMessages[`${shortcut.descId}`])}</td>
</tr>
))}
</tbody>
</table>
</span>
}
</Modal>
);
};
ShortcutHelpComponent.defaultProps = {
intl: intlShape,
};
ShortcutHelpComponent.propTypes = {
intl: intlShape,
shortcuts: PropTypes.arrayOf(PropTypes.shape({
accesskey: PropTypes.string.isRequired,
descId: PropTypes.string.isRequired,
})).isRequired,
};
export default withShortcutHelper(injectIntl(ShortcutHelpComponent));

View File

@ -0,0 +1,37 @@
import React from 'react';
import getFromUserSettings from '/imports/ui/services/users-settings';
const BASE_SHORTCUTS = Meteor.settings.public.app.shortcuts;
const withShortcutHelper = (WrappedComponent, param) => (props) => {
const ENABLED_SHORTCUTS = getFromUserSettings('shortcuts', null);
let shortcuts = Object.values(BASE_SHORTCUTS);
if (ENABLED_SHORTCUTS) {
shortcuts = Object.values(BASE_SHORTCUTS)
.filter(el => ENABLED_SHORTCUTS.includes(el.descId));
}
if (param !== undefined) {
if (!Array.isArray(param)) {
shortcuts = shortcuts
.filter(el => el.descId === param)
.map(el => el.accesskey)
.pop();
} else {
shortcuts = shortcuts
.filter(el => param.includes(el.descId))
.reduce((acc, current) => {
acc[current.descId] = current.accesskey;
return acc;
}, {});
}
}
return (
<WrappedComponent {...props} shortcuts={shortcuts} />
);
};
export default withShortcutHelper;

View File

@ -12,6 +12,7 @@ import Icon from '../icon/component';
import { styles } from './styles';
import AudioService from '../audio/service';
let breakoutNotified = false;
const intlMessages = defineMessages({
toastBreakoutRoomEnded: {
@ -41,9 +42,13 @@ class ToastContainer extends React.Component {
export default injectIntl(injectNotify(withTracker(({ notify, intl }) => {
Breakouts.find().observeChanges({
added() {
breakoutNotified = false;
},
removed() {
if (!AudioService.isUsingAudio()) {
if (!AudioService.isUsingAudio() && !breakoutNotified) {
notify(intl.formatMessage(intlMessages.toastBreakoutRoomEnded), 'info', 'rooms');
breakoutNotified = true;
}
},
});

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import { Session } from 'meteor/session';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { styles } from './styles';
import ChatAvatar from './chat-avatar/component';
import ChatIcon from './chat-icon/component';
@ -23,9 +24,6 @@ const intlMessages = defineMessages({
},
});
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
const TOGGLE_CHAT_PUB_AK = SHORTCUTS_CONFIG.togglePublicChat.accesskey;
const propTypes = {
chat: PropTypes.shape({
id: PropTypes.string.isRequired,
@ -39,14 +37,17 @@ const propTypes = {
}).isRequired,
tabIndex: PropTypes.number.isRequired,
isPublicChat: PropTypes.func.isRequired,
shortcuts: PropTypes.string,
};
const defaultProps = {
openChat: '',
shortcuts: '',
};
const toggleChatOpen = () => {
Session.set('isChatOpen', !Session.get('isChatOpen'));
Session.set('breakoutRoomIsOpen', false);
};
const ChatListItem = (props) => {
@ -57,6 +58,7 @@ const ChatListItem = (props) => {
intl,
tabIndex,
isPublicChat,
shortcuts: TOGGLE_CHAT_PUB_AK,
} = props;
const isCurrentChat = chat.id === openChat;
@ -109,4 +111,4 @@ const ChatListItem = (props) => {
ChatListItem.propTypes = propTypes;
ChatListItem.defaultProps = defaultProps;
export default injectIntl(ChatListItem);
export default withShortcutHelper(injectIntl(ChatListItem), 'togglePublicChat');

View File

@ -68,6 +68,7 @@ class UserList extends Component {
getEmojiList,
getEmoji,
showBranding,
hasBreakoutRoom,
} = this.props;
return (
@ -103,6 +104,7 @@ class UserList extends Component {
handleEmojiChange,
getEmojiList,
getEmoji,
hasBreakoutRoom,
}
}
/>}

View File

@ -35,6 +35,7 @@ UserListContainer.propTypes = propTypes;
export default withTracker(({ chatID, compact }) => ({
users: Service.getUsers(),
meeting: Meetings.findOne({}),
hasBreakoutRoom: Service.hasBreakoutRoom(),
currentUser: Service.getCurrentUser(),
openChats: Service.getOpenChats(chatID),
isBreakoutRoom: meetingIsBreakout(),

View File

@ -1,6 +1,7 @@
import Users from '/imports/api/users';
import GroupChat from '/imports/api/group-chat';
import GroupChatMsg from '/imports/api/group-chat-msg';
import Breakouts from '/imports/api/breakouts/';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
@ -190,6 +191,8 @@ const getUsers = () => {
.sort(sortUsers);
};
const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID }).count() > 0;
const getOpenChats = (chatID) => {
const privateChat = GroupChat
.find({ users: { $all: [Auth.userID] } })
@ -453,6 +456,7 @@ export default {
setCustomLogoUrl,
getCustomLogoUrl,
getGroupChatPrivate,
hasBreakoutRoom,
getEmojiList: () => EMOJI_STATUSES,
getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
};

View File

@ -0,0 +1,42 @@
import React, { Component } 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';
import { styles } from './styles';
const intlMessages = defineMessages({
breakoutTitle: {
id: 'app.createBreakoutRoom.title',
description: 'breakout title',
},
});
const toggleBreakoutPanel = () => {
const breakoutPanelState = Session.get('breakoutRoomIsOpen');
Session.set('breakoutRoomIsOpen', !breakoutPanelState);
Session.set('isChatOpen', false);
};
const BreakoutRoomItem = ({
hasBreakoutRoom,
intl,
}) => {
if (hasBreakoutRoom) {
return (
<div role="button" onClick={toggleBreakoutPanel}>
<h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()}</h2>
<div className={styles.BreakoutRoomsItem}>
<div className={styles.BreakoutRoomsContents}>
<div className={styles.BreakoutRoomsIcon} >
<Icon iconName="rooms" />
</div>
<span className={styles.BreakoutRoomsText}>{intl.formatMessage(intlMessages.breakoutTitle)}</span>
</div>
</div>
</div>
);
}
return <span />;
};
export default injectIntl(BreakoutRoomItem);

View File

@ -0,0 +1,47 @@
@import "/imports/ui/components/user-list/styles.scss";
@import "/imports/ui/stylesheets/mixins/_scrollable";
@import "/imports/ui/stylesheets/mixins/focus";
@import "/imports/ui/stylesheets/variables/_all";
.smallTitle {
@extend .smallTitle;
}
.BreakoutRoomsContents {
flex-grow: 0;
display: flex;
flex-flow: row;
padding-top: var(--lg-padding-y);
padding-bottom: var(--lg-padding-y);
}
.BreakoutRoomsIcon {
display: flex;
flex-flow: row;
justify-content: space-between;
margin-left: var(--sm-padding-x) / 2;
text-align: right;
font-size: 175%;
flex-shrink: 1;
color: var(--user-icons-color);
}
.BreakoutRoomsText {
@extend %flex-column;
min-width: 0;
flex-grow: 1;
margin-left: var(--sm-padding-x);
justify-content: center;
font-size: var(--font-size-small);
color: var(--color-gray-dark);
}
.BreakoutRoomsItem {
@extend %list-item;
margin-left: 0.375rem;
}
.link {
text-decoration: none;
}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { styles } from './styles';
import UserParticipants from './user-participants/component';
import UserMessages from './user-messages/component';
import BreakoutRoomItem from './breakout-room/component';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
@ -63,6 +64,7 @@ class UserContent extends React.PureComponent {
isPublicChat,
openChats,
getGroupChatPrivate,
hasBreakoutRoom,
} = this.props;
return (
@ -80,6 +82,7 @@ class UserContent extends React.PureComponent {
roving,
}}
/>
<BreakoutRoomItem isPresenter={currentUser.isPresenter} hasBreakoutRoom={hasBreakoutRoom} />
<UserParticipants
{...{
users,

View File

@ -1,4 +1,7 @@
@import "/imports/ui/stylesheets/variables/_all";
:root {
--color-white-with-transparency: #ffffff40;
}
.videoCanvas {
--cam-dropdown-width: 70%;
@ -51,7 +54,7 @@
right: 0;
bottom: 0;
left: 0;
border: 5px solid rgba(var(--color-white), .25);
border: 5px solid var(--color-white-with-transparency);
border-radius: 5px;
opacity: 0;
transition: opacity .1s;
@ -80,7 +83,7 @@
@extend %media-area;
position: absolute;
background-color: var(--color-gray);
color: rgba(var(--color-white), .25);
color: var(--color-white-with-transparency);
font-size: 2.5rem;
text-align: center;
white-space: nowrap;
@ -132,6 +135,7 @@
.dropdownTrigger {
@extend %text-elipsis;
cursor: pointer;
position: relative;
background-color: rgba(var(--color-gray), .5);
color: var(--color-white);

View File

@ -222,7 +222,7 @@ WhiteboardOverlay.propTypes = {
// Annotation thickness (not normalized)
thickness: PropTypes.number.isRequired,
// The name of the tool currently selected
tool: PropTypes.string.isRequired,
tool: PropTypes.string,
// Font size for the text shape
textFontSize: PropTypes.number.isRequired,
// Text shape value

View File

@ -348,6 +348,6 @@ ShapeDrawListener.propTypes = {
// Annotation thickness (not normalized)
thickness: PropTypes.number.isRequired,
// The name of the tool currently selected
tool: PropTypes.string.isRequired,
tool: PropTypes.string,
}).isRequired,
};

View File

@ -159,7 +159,11 @@ class WhiteboardToolbar extends Component {
// we might need to trigger svg animation for Color and Thickness icons
this.animateSvgIcons(prevState);
if (!hadInAnnotations) {
if (prevProps.annotations.length !== annotations.length && annotations.length === 0) {
this.handleAnnotationChange({ icon: null, value: null });
}
if (!hadInAnnotations && annotations.length) {
this.handleAnnotationChange(annotations[annotations.length - 1]);
}
}
@ -323,10 +327,11 @@ class WhiteboardToolbar extends Component {
}
renderToolItem() {
const { intl } = this.props;
const { intl, annotations } = this.props;
const isDisabled = !annotations.length;
return (
<ToolbarMenuItem
disabled={isDisabled}
label={intl.formatMessage(intlMessages.toolbarTools)}
icon={this.state.annotationSelected.icon}
onItemClick={this.displaySubMenu}
@ -334,18 +339,18 @@ class WhiteboardToolbar extends Component {
onBlur={this.closeSubMenu}
className={cx(styles.toolbarButton, this.state.currentSubmenuOpen === 'annotationList' ? styles.toolbarActive : null)}
>
{this.state.currentSubmenuOpen === 'annotationList' ?
{this.state.currentSubmenuOpen === 'annotationList' && annotations.length > 1 ?
<ToolbarSubmenu
type="annotations"
customIcon={false}
label="Annotations"
onItemClick={this.handleAnnotationChange}
objectsToRender={this.props.annotations}
objectsToRender={annotations}
objectSelected={this.state.annotationSelected}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
: null}
</ToolbarMenuItem>
);
}
@ -373,7 +378,7 @@ class WhiteboardToolbar extends Component {
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
: null}
</ToolbarMenuItem>
);
}
@ -395,8 +400,8 @@ class WhiteboardToolbar extends Component {
}
renderThicknessItem() {
const { intl } = this.props;
const isDisabled = this.state.annotationSelected.value === 'hand';
const { intl, annotations } = this.props;
const isDisabled = this.state.annotationSelected.value === 'hand' || !annotations.length;
return (
<ToolbarMenuItem
disabled={isDisabled}
@ -420,7 +425,7 @@ class WhiteboardToolbar extends Component {
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
: null}
</ToolbarMenuItem>
);
}
@ -475,8 +480,8 @@ class WhiteboardToolbar extends Component {
}
renderColorItem() {
const { intl } = this.props;
const isDisabled = this.state.annotationSelected.value === 'hand';
const { intl, annotations } = this.props;
const isDisabled = this.state.annotationSelected.value === 'hand' || !annotations.length;
return (
<ToolbarMenuItem
disabled={isDisabled}
@ -500,7 +505,7 @@ class WhiteboardToolbar extends Component {
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
: null}
</ToolbarMenuItem>
);
}
@ -569,7 +574,10 @@ class WhiteboardToolbar extends Component {
return (
<ToolbarMenuItem
label={multiUser ? intl.formatMessage(intlMessages.toolbarMultiUserOff) : intl.formatMessage(intlMessages.toolbarMultiUserOn)}
label={multiUser ?
intl.formatMessage(intlMessages.toolbarMultiUserOff)
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
}
icon={multiUser ? 'multi_whiteboard' : 'whiteboard'}
onItemClick={this.handleSwitchWhiteboardMode}
className={cx(styles.toolbarButton, styles.notActive)}
@ -584,17 +592,11 @@ class WhiteboardToolbar extends Component {
<div className={styles.toolbarContainer}>
<div className={styles.toolbarWrapper}>
{this.renderToolItem()}
{annotationSelected.value === 'text' ?
this.renderFontItem()
:
this.renderThicknessItem()
}
{annotationSelected.value === 'text' ? this.renderFontItem() : this.renderThicknessItem()}
{this.renderColorItem()}
{this.renderUndoItem()}
{this.renderClearAllItem()}
{isPresenter ?
this.renderMultiUserItem()
: null }
{isPresenter ? this.renderMultiUserItem() : null}
</div>
</div>
);
@ -605,6 +607,7 @@ WhiteboardToolbar.defaultProps = {
colors: ANNOTATION_COLORS,
thicknessRadiuses: THICKNESS_RADIUSES,
fontSizes: FONT_SIZES,
intl: intlShape,
};
WhiteboardToolbar.propTypes = {
@ -630,18 +633,18 @@ WhiteboardToolbar.propTypes = {
// defines an array of font-sizes for the Font-size submenu of the text shape
fontSizes: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.number.isRequired,
}).isRequired).isRequired,
}).isRequired),
// defines an array of colors for the toolbar (color submenu)
colors: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired,
}).isRequired).isRequired,
}).isRequired),
// defines an array of thickness values for the toolbar and their corresponding session values
thicknessRadiuses: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.number.isRequired,
}).isRequired).isRequired,
}).isRequired),
intl: intlShape.isRequired,
intl: intlShape,
};

View File

@ -133,7 +133,7 @@ class AudioManager {
this.isListenOnly = true;
this.isEchoTest = false;
// The kurento bridge isn't a full audio bridge yet, so we have to differ it
const bridge = USE_KURENTO? this.listenOnlyBridge : this.bridge;
const bridge = USE_KURENTO ? this.listenOnlyBridge : this.bridge;
const callOptions = {
isListenOnly: true,

8
bigbluebutton-html5/private/config/settings.yml Normal file → Executable file
View File

@ -129,6 +129,7 @@ public:
- createGroupChat
- destroyGroupChat
- sendGroupChatMsg
- requestJoinURL
moderator:
methods:
- assignPresenter
@ -143,6 +144,7 @@ public:
- toggleRecording
- muteAllUsers
- muteAllExceptPresenter
- endAllBreakouts
presenter:
methods:
- assignPresenter
@ -156,6 +158,12 @@ public:
- setPresentation
- zoomSlide
- requestPresentationUploadToken
- transferUser
- createBreakoutRoom
breakout:
rooms:
min: 2
max: 8
chat:
min_message_length: 1
max_message_length: 5000

View File

@ -218,7 +218,8 @@
"app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with",
"app.actionsBar.actionsDropdown.startRecording": "Start recording",
"app.actionsBar.actionsDropdown.stopRecording": "Stop recording",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "Create breakout rooms",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "create breakouts for split the current meeting ",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Set Status",
"app.actionsBar.emojiMenu.awayLabel": "Away",
"app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
@ -375,7 +376,6 @@
"app.sfu.serverIceGatheringFailed2022": "Error 2022: Media server ICE connection failed",
"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",
"app.whiteboard.toolbar.tools": "Tools",
"app.whiteboard.toolbar.tools.hand": "Hand",
@ -414,5 +414,20 @@
"app.videoDock.webcamFocusLabel": "Focus",
"app.videoDock.webcamFocusDesc": "Focus the selected webcam",
"app.videoDock.webcamUnfocusLabel": "Unfocus",
"app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam"
"app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam",
"app.createBreakoutRoom.title": "Breakout Rooms",
"app.createBreakoutRoom.generatingURL": "Generating URL",
"app.createBreakoutRoom.generatedURL": "Generated",
"app.createBreakoutRoom.duration": "Duration {0}",
"app.createBreakoutRoom.room": "Room {0}",
"app.createBreakoutRoom.join": "Join Room",
"app.createBreakoutRoom.joinAudio": "Join audio",
"app.createBreakoutRoom.returnAudio": "Return audio",
"app.createBreakoutRoom.confirm": "Create",
"app.createBreakoutRoom.numberOfRooms": "Number of Rooms",
"app.createBreakoutRoom.durationInMinutes": "Duration (minutes)",
"app.createBreakoutRoom.randomlyAssign": "Randomly Assign",
"app.createBreakoutRoom.endAllBreakouts": "End All Breakout Rooms",
"app.createBreakoutRoom.roomName": "{0} (Room - {1})",
"app.createBreakoutRoom.modalDesc": "Complete the steps below to create rooms in your session, To add participants to a room."
}