Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into user-manage-b
This commit is contained in:
commit
a97a1ece37
@ -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'));
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import clearBreakouts from '../modifiers/clearBreakouts';
|
||||
|
||||
export default function handleBreakoutClosed({ body }) {
|
||||
const { breakoutId } = body;
|
||||
|
||||
check(breakoutId, String);
|
||||
|
||||
return clearBreakouts(breakoutId);
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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);
|
||||
}
|
@ -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 = {
|
||||
|
@ -21,6 +21,8 @@ export default function addUserSettings(credentials, meetingId, userId, settings
|
||||
'askForFeedbackOnLogout',
|
||||
// BRANDING
|
||||
'displayBrandingArea',
|
||||
// SHORTCUTS
|
||||
'shortcuts',
|
||||
// KURENTO
|
||||
'enableScreensharing',
|
||||
'enableVideo',
|
||||
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const BreakoutRemainingTime = props => (
|
||||
<span>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
export default BreakoutRemainingTime;
|
@ -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));
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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;
|
||||
}
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
@ -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;
|
||||
}
|
@ -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),
|
||||
};
|
||||
|
@ -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 />
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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']);
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
@ -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));
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
/>}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
@ -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;
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
||||
};
|
||||
|
||||
|
@ -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
8
bigbluebutton-html5/private/config/settings.yml
Normal file → Executable 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
|
||||
|
@ -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."
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user