bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx

604 lines
19 KiB
React
Raw Normal View History

2019-06-25 02:06:02 +08:00
import React, { PureComponent } from 'react';
2018-10-02 21:48:12 +08:00
import { defineMessages, injectIntl } from 'react-intl';
import { Session } from 'meteor/session';
2019-08-22 01:38:04 +08:00
import logger from '/imports/startup/client/logger';
2021-10-29 01:26:15 +08:00
import Styled from './styles';
import Service from './service';
2023-02-23 19:49:44 +08:00
import MeetingRemainingTime from '../notifications-bar/meeting-remaining-time/container';
import MessageFormContainer from './message-form/container';
import VideoService from '/imports/ui/components/video-provider/service';
2021-05-18 04:25:07 +08:00
import { PANELS, ACTIONS } from '../layout/enums';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
import AudioManager from '/imports/ui/services/audio-manager';
2021-10-29 01:26:15 +08:00
import Settings from '/imports/ui/services/settings';
import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Header from '/imports/ui/components/common/control-header/component';
2018-10-02 21:48:12 +08:00
const intlMessages = defineMessages({
breakoutTitle: {
id: 'app.createBreakoutRoom.title',
description: 'breakout title',
},
2019-01-28 21:33:50 +08:00
breakoutAriaTitle: {
id: 'app.createBreakoutRoom.ariaTitle',
description: 'breakout aria title',
},
2018-10-02 21:48:12 +08:00
breakoutDuration: {
id: 'app.createBreakoutRoom.duration',
description: 'breakout duration time',
},
breakoutRoom: {
id: 'app.createBreakoutRoom.room',
description: 'breakout room',
2018-10-02 21:48:12 +08:00
},
breakoutJoin: {
id: 'app.createBreakoutRoom.join',
description: 'label for join breakout room',
2018-10-02 21:48:12 +08:00
},
2018-10-24 01:18:09 +08:00
breakoutJoinAudio: {
id: 'app.createBreakoutRoom.joinAudio',
description: 'label for option to transfer audio',
2018-10-24 01:18:09 +08:00
},
breakoutReturnAudio: {
id: 'app.createBreakoutRoom.returnAudio',
description: 'label for option to return audio',
2018-10-24 01:18:09 +08:00
},
askToJoin: {
id: 'app.createBreakoutRoom.askToJoin',
description: 'label for generate breakout room url',
},
2018-10-24 01:18:09 +08:00
generatingURL: {
id: 'app.createBreakoutRoom.generatingURL',
description: 'label for generating breakout room url',
2018-10-24 01:18:09 +08:00
},
2018-10-24 03:29:19 +08:00
endAllBreakouts: {
id: 'app.createBreakoutRoom.endAllBreakouts',
description: 'Button label to end all breakout rooms',
},
chatTitleMsgAllRooms: {
id: 'app.createBreakoutRoom.chatTitleMsgAllRooms',
description: 'chat title for send message to all rooms',
},
alreadyConnected: {
id: 'app.createBreakoutRoom.alreadyConnected',
description: 'label for the user that is already connected to breakout room',
2018-10-24 03:29:19 +08:00
},
setTimeInMinutes: {
id: 'app.createBreakoutRoom.setTimeInMinutes',
description: 'Label for input to set time (minutes)',
},
setTimeLabel: {
id: 'app.createBreakoutRoom.setTimeLabel',
description: 'Button label to set breakout rooms time',
},
setTimeCancel: {
id: 'app.createBreakoutRoom.setTimeCancel',
description: 'Button label to cancel set breakout rooms time',
},
setTimeHigherThanMeetingTimeError: {
id: 'app.createBreakoutRoom.setTimeHigherThanMeetingTimeError',
description: 'Label for error when new breakout rooms time would be higher than remaining time in parent meeting',
},
2018-10-02 21:48:12 +08:00
});
let prevBreakoutData = {};
2019-06-25 02:06:02 +08:00
class BreakoutRoom extends PureComponent {
static sortById(a, b) {
if (a.userId > b.userId) {
return 1;
}
if (a.userId < b.userId) {
return -1;
}
return 0;
}
2018-10-02 21:48:12 +08:00
constructor(props) {
super(props);
2018-10-24 01:18:09 +08:00
this.renderBreakoutRooms = this.renderBreakoutRooms.bind(this);
this.getBreakoutURL = this.getBreakoutURL.bind(this);
2022-01-20 21:03:18 +08:00
this.hasBreakoutUrl = this.hasBreakoutUrl.bind(this);
this.getBreakoutLabel = this.getBreakoutLabel.bind(this);
2018-10-24 01:18:09 +08:00
this.renderDuration = this.renderDuration.bind(this);
this.transferUserToBreakoutRoom = this.transferUserToBreakoutRoom.bind(this);
this.changeSetTime = this.changeSetTime.bind(this);
this.showSetTimeForm = this.showSetTimeForm.bind(this);
this.resetSetTimeForm = this.resetSetTimeForm.bind(this);
2018-10-24 01:18:09 +08:00
this.renderUserActions = this.renderUserActions.bind(this);
this.returnBackToMeeeting = this.returnBackToMeeeting.bind(this);
this.closePanel = this.closePanel.bind(this);
this.handleClickOutsideDurationContainer = this.handleClickOutsideDurationContainer.bind(this);
2018-10-24 01:18:09 +08:00
this.state = {
requestedBreakoutId: '',
waiting: false,
generated: false,
2018-10-24 01:18:09 +08:00
joinedAudioOnly: false,
breakoutId: '',
visibleSetTimeForm: false,
visibleSetTimeHigherThanMeetingTimeError: false,
newTime: 15,
2018-10-24 01:18:09 +08:00
};
}
componentDidMount() {
if (this.panel) this.panel.firstChild.focus();
}
2018-10-24 01:18:09 +08:00
componentDidUpdate() {
const {
getBreakoutRoomUrl,
setBreakoutAudioTransferStatus,
isMicrophoneUser,
isReconnecting,
breakoutRooms,
} = this.props;
const {
waiting,
requestedBreakoutId,
joinedAudioOnly,
generated,
} = this.state;
if (breakoutRooms.length === 0) {
return this.closePanel();
}
if (waiting && !generated) {
const breakoutUrlData = getBreakoutRoomUrl(requestedBreakoutId);
if (!breakoutUrlData) return false;
if (breakoutUrlData.redirectToHtml5JoinURL !== ''
&& breakoutUrlData.redirectToHtml5JoinURL !== prevBreakoutData.redirectToHtml5JoinURL) {
prevBreakoutData = breakoutUrlData;
Session.set('lastBreakoutIdOpened', requestedBreakoutId);
window.open(breakoutUrlData.redirectToHtml5JoinURL, '_blank');
2023-02-24 01:38:48 +08:00
setTimeout(() => {
this.setState({ generated: true, waiting: false });
}, 1000);
2018-10-24 01:18:09 +08:00
}
}
if (joinedAudioOnly && (!isMicrophoneUser || isReconnecting)) {
this.clearJoinedAudioOnly();
setBreakoutAudioTransferStatus({
breakoutMeetingId: '',
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.DISCONNECTED,
});
}
2021-08-09 22:24:02 +08:00
return true;
2018-10-24 01:18:09 +08:00
}
2018-10-24 01:18:09 +08:00
getBreakoutURL(breakoutId) {
const { requestJoinURL, getBreakoutRoomUrl } = this.props;
const { waiting } = this.state;
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
if (!breakoutRoomUrlData && !waiting) {
2018-10-24 03:29:19 +08:00
this.setState(
{
waiting: true,
generated: false,
requestedBreakoutId: breakoutId,
},
2018-10-24 03:29:19 +08:00
() => requestJoinURL(breakoutId),
);
2018-10-24 01:18:09 +08:00
}
if (breakoutRoomUrlData) {
Session.set('lastBreakoutIdOpened', breakoutId);
window.open(breakoutRoomUrlData.redirectToHtml5JoinURL, '_blank');
this.setState({ waiting: false, generated: false });
2018-10-24 01:18:09 +08:00
}
return null;
}
2022-01-20 21:03:18 +08:00
hasBreakoutUrl(breakoutId) {
const { getBreakoutRoomUrl } = this.props;
const { requestedBreakoutId, generated } = this.state;
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
2022-01-20 21:03:18 +08:00
if ((generated && requestedBreakoutId === breakoutId) || breakoutRoomUrlData) {
return true;
}
2022-01-20 21:03:18 +08:00
return false;
}
getBreakoutLabel(breakoutId) {
const { intl } = this.props;
const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId)
if (hasBreakoutUrl) {
return intl.formatMessage(intlMessages.breakoutJoin);
}
return intl.formatMessage(intlMessages.askToJoin);
}
clearJoinedAudioOnly() {
this.setState({ joinedAudioOnly: false });
}
changeSetTime(event) {
const newSetTime = Number.parseInt(event.target.value, 10) || 0;
this.setState({ newTime: newSetTime >= 0 ? newSetTime : 0 });
}
handleClickOutsideDurationContainer(e) {
if (this.durationContainerRef) {
const { x, right, y, bottom } = this.durationContainerRef.getBoundingClientRect();
if (e.clientX < x || e.clientX > right || e.clientY < y || e.clientY > bottom) {
this.resetSetTimeForm();
}
}
}
showSetTimeForm() {
this.setState({ visibleSetTimeForm: true });
window.addEventListener('click', this.handleClickOutsideDurationContainer);
}
showSetTimeHigherThanMeetingTimeError(show) {
this.setState({ visibleSetTimeHigherThanMeetingTimeError: show });
}
resetSetTimeForm() {
this.setState({ visibleSetTimeForm: false, newTime: 5 });
window.removeEventListener('click', this.handleClickOutsideDurationContainer);
}
2018-10-24 01:18:09 +08:00
transferUserToBreakoutRoom(breakoutId) {
2024-01-16 19:28:15 +08:00
const { transferUserToMeeting, meetingId } = this.props;
transferUserToMeeting(meetingId, breakoutId);
2018-10-24 01:18:09 +08:00
this.setState({ joinedAudioOnly: true, breakoutId });
}
2018-10-24 01:18:09 +08:00
returnBackToMeeeting(breakoutId) {
const { transferUserToMeeting, meetingId } = this.props;
transferUserToMeeting(breakoutId, meetingId);
this.setState({ joinedAudioOnly: false, breakoutId });
}
closePanel() {
2021-08-05 19:03:24 +08:00
const { layoutContextDispatch } = this.props;
2021-08-05 19:03:24 +08:00
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
2021-08-05 19:03:24 +08:00
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}
2022-02-25 01:40:20 +08:00
renderUserActions(breakoutId, joinedUsers, shortName) {
2018-10-24 01:18:09 +08:00
const {
isMicrophoneUser,
amIModerator,
2021-11-24 00:41:37 +08:00
amIPresenter,
2018-10-24 01:18:09 +08:00
intl,
isUserInBreakoutRoom,
forceExitAudio,
rejoinAudio,
setBreakoutAudioTransferStatus,
getBreakoutAudioTransferStatus,
2024-01-27 00:21:58 +08:00
sendUserUnshareWebcam,
2018-10-24 01:18:09 +08:00
} = this.props;
const {
joinedAudioOnly,
breakoutId: _stateBreakoutId,
2018-10-24 01:18:09 +08:00
requestedBreakoutId,
waiting,
} = this.state;
const {
breakoutMeetingId: currentAudioTransferBreakoutId,
status,
} = getBreakoutAudioTransferStatus();
const isInBreakoutAudioTransfer = status
=== AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED;
const stateBreakoutId = _stateBreakoutId || currentAudioTransferBreakoutId;
const moderatorJoinedAudio = isMicrophoneUser && amIModerator;
2018-10-24 01:18:09 +08:00
const disable = waiting && requestedBreakoutId !== breakoutId;
2022-01-20 21:03:18 +08:00
const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId);
2022-02-25 01:40:20 +08:00
const dataTest = `${hasBreakoutUrl ? 'join' : 'askToJoin'}${shortName.replace(' ', '')}`;
2022-01-20 21:03:18 +08:00
const audioAction = joinedAudioOnly || isInBreakoutAudioTransfer
2019-08-22 01:38:04 +08:00
? () => {
setBreakoutAudioTransferStatus({
breakoutMeetingId: breakoutId,
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.RETURNING,
});
2019-08-22 01:38:04 +08:00
this.returnBackToMeeeting(breakoutId);
return logger.info({
2019-08-22 01:38:04 +08:00
logCode: 'breakoutroom_return_main_audio',
extraInfo: { logType: 'user_action' },
2019-08-22 01:38:04 +08:00
}, 'Returning to main audio (breakout room audio closed)');
}
: () => {
setBreakoutAudioTransferStatus({
breakoutMeetingId: breakoutId,
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED,
});
2019-08-22 01:38:04 +08:00
this.transferUserToBreakoutRoom(breakoutId);
return logger.info({
logCode: 'breakoutroom_join_audio_from_main_room',
extraInfo: { logType: 'user_action' },
2019-08-22 01:38:04 +08:00
}, 'joining breakout room audio (main room audio closed)');
};
2018-10-24 01:18:09 +08:00
return (
2021-10-29 01:26:15 +08:00
<Styled.BreakoutActions>
2021-08-09 22:24:02 +08:00
{
isUserInBreakoutRoom(joinedUsers)
? (
<Styled.AlreadyConnected data-test="alreadyConnected">
2021-08-09 22:24:02 +08:00
{intl.formatMessage(intlMessages.alreadyConnected)}
2021-10-29 01:26:15 +08:00
</Styled.AlreadyConnected>
2021-08-09 22:24:02 +08:00
)
: (
2021-10-29 01:26:15 +08:00
<Styled.JoinButton
label={this.getBreakoutLabel(breakoutId)}
2022-01-20 21:03:18 +08:00
data-test={dataTest}
2022-02-25 01:40:20 +08:00
aria-label={`${this.getBreakoutLabel(breakoutId)} ${shortName}`}
2021-08-09 22:24:02 +08:00
onClick={() => {
this.getBreakoutURL(breakoutId);
// leave main room's audio,
// and stops video and screenshare when joining a breakout room
forceExitAudio();
logger.info({
2021-08-09 22:24:02 +08:00
logCode: 'breakoutroom_join',
extraInfo: { logType: 'user_action' },
}, 'joining breakout room closed audio in the main room');
VideoService.storeDeviceIds();
2024-01-27 00:21:58 +08:00
VideoService.exitVideo(sendUserUnshareWebcam);
2021-11-24 00:41:37 +08:00
if (amIPresenter) screenshareHasEnded();
Tracker.autorun((c) => {
const selector = {
meetingId: breakoutId,
};
const query = Users.find(selector, {
fields: {
loggedOut: 1,
extId: 1,
},
});
2022-02-12 00:45:55 +08:00
const observeLogOut = (user) => {
if (user?.loggedOut && user?.extId?.startsWith(Auth.userID)) {
rejoinAudio();
c.stop();
}
}
query.observe({
2022-02-12 00:45:55 +08:00
added: observeLogOut,
changed: observeLogOut,
2022-02-10 01:03:35 +08:00
});
});
2021-08-09 22:24:02 +08:00
}}
disabled={disable}
/>
)
}
2018-10-24 01:18:09 +08:00
{
moderatorJoinedAudio
? [
2018-10-24 01:18:09 +08:00
('|'),
(
2021-10-29 01:26:15 +08:00
<Styled.AudioButton
2018-10-24 01:18:09 +08:00
label={
2021-05-18 04:25:07 +08:00
stateBreakoutId === breakoutId
&& (joinedAudioOnly || isInBreakoutAudioTransfer)
? intl.formatMessage(intlMessages.breakoutReturnAudio)
: intl.formatMessage(intlMessages.breakoutJoinAudio)
2018-10-24 01:18:09 +08:00
}
disabled={stateBreakoutId !== breakoutId && joinedAudioOnly}
2019-09-19 04:00:32 +08:00
key={`join-audio-${breakoutId}`}
2018-10-24 03:29:19 +08:00
onClick={audioAction}
2018-10-24 01:18:09 +08:00
/>
),
]
: null
}
2021-10-29 01:26:15 +08:00
</Styled.BreakoutActions>
2018-10-24 01:18:09 +08:00
);
}
renderBreakoutRooms() {
const {
breakoutRooms,
intl,
} = this.props;
const {
waiting,
requestedBreakoutId,
} = this.state;
2021-10-29 01:26:15 +08:00
const { animations } = Settings.application;
2021-08-09 22:24:02 +08:00
const roomItems = breakoutRooms.map((breakout) => (
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`}>
2021-10-29 01:26:15 +08:00
<Styled.Content key={`breakoutRoomList-${breakout.breakoutId}`}>
2023-01-17 07:10:13 +08:00
<Styled.BreakoutRoomListNameLabel data-test={breakout.shortName} aria-hidden>
2021-08-09 22:24:02 +08:00
{breakout.isDefaultName
? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })
2021-08-09 22:24:02 +08:00
: breakout.shortName}
2021-10-29 01:26:15 +08:00
<Styled.UsersAssignedNumberLabel>
(
2019-09-28 06:59:55 +08:00
{breakout.joinedUsers.length}
)
2021-10-29 01:26:15 +08:00
</Styled.UsersAssignedNumberLabel>
</Styled.BreakoutRoomListNameLabel>
{waiting && requestedBreakoutId === breakout.breakoutId ? (
<span>
{intl.formatMessage(intlMessages.generatingURL)}
2021-10-29 01:26:15 +08:00
<Styled.ConnectingAnimation animations={animations}/>
</span>
2019-09-28 06:59:55 +08:00
) : this.renderUserActions(
breakout.breakoutId,
breakout.joinedUsers,
2022-02-25 01:40:20 +08:00
breakout.shortName,
2019-09-28 06:59:55 +08:00
)}
2021-10-29 01:26:15 +08:00
</Styled.Content>
2022-09-16 02:34:14 +08:00
<Styled.JoinedUserNames
2022-09-30 19:17:15 +08:00
data-test={`userNameBreakoutRoom-${breakout.shortName}`}
2022-09-16 02:34:14 +08:00
>
{breakout.joinedUsers
.sort(BreakoutRoom.sortById)
.filter((value, idx, arr) => !(value.userId === (arr[idx + 1] || {}).userId))
.sort(Service.sortUsersByName)
2021-08-09 22:24:02 +08:00
.map((u) => u.name)
.join(', ')}
2021-10-29 01:26:15 +08:00
</Styled.JoinedUserNames>
</Styled.BreakoutItems>
2018-10-24 01:18:09 +08:00
));
return (
2021-10-29 01:26:15 +08:00
<Styled.BreakoutColumn>
2022-09-30 19:17:15 +08:00
<Styled.BreakoutScrollableList data-test="breakoutRoomList">
{roomItems}
2021-10-29 01:26:15 +08:00
</Styled.BreakoutScrollableList>
</Styled.BreakoutColumn>
);
2018-10-24 01:18:09 +08:00
}
renderDuration() {
const {
2021-08-09 22:24:02 +08:00
intl,
breakoutRooms,
amIModerator,
isMeteorConnected,
setBreakoutsTime,
isNewTimeHigherThanMeetingRemaining,
} = this.props;
2021-08-09 22:24:02 +08:00
const {
newTime,
visibleSetTimeForm,
visibleSetTimeHigherThanMeetingTimeError,
2021-08-09 22:24:02 +08:00
} = this.state;
2018-10-24 01:18:09 +08:00
return (
<Styled.DurationContainer
centeredText={!visibleSetTimeForm}
ref={(ref) => this.durationContainerRef = ref}
>
<Styled.Duration>
2023-02-23 19:49:44 +08:00
<MeetingRemainingTime
messageDuration={intlMessages.breakoutDuration}
breakoutRoom={breakoutRooms[0]}
fromBreakoutPanel
/>
</Styled.Duration>
{amIModerator && visibleSetTimeForm ? (
<Styled.SetTimeContainer>
<label htmlFor="inputSetTimeSelector" >
{intl.formatMessage(intlMessages.setTimeInMinutes)}
</label>
<br />
<Styled.FlexRow>
<Styled.SetDurationInput
id="inputSetTimeSelector"
type="number"
min="1"
value={newTime}
onChange={this.changeSetTime}
aria-label={intl.formatMessage(intlMessages.setTimeInMinutes)}
/>
&nbsp;
&nbsp;
<Styled.EndButton
2022-09-16 02:34:14 +08:00
data-test="sendButtonDurationTime"
color="primary"
disabled={!isMeteorConnected}
size="sm"
label={intl.formatMessage(intlMessages.setTimeLabel)}
onClick={() => {
this.showSetTimeHigherThanMeetingTimeError(false);
if (isNewTimeHigherThanMeetingRemaining(newTime)) {
this.showSetTimeHigherThanMeetingTimeError(true);
} else if (setBreakoutsTime(newTime)) {
this.resetSetTimeForm();
}
}}
/>
</Styled.FlexRow>
{visibleSetTimeHigherThanMeetingTimeError ? (
2021-10-29 01:26:15 +08:00
<Styled.WithError>
{intl.formatMessage(intlMessages.setTimeHigherThanMeetingTimeError)}
2021-10-29 01:26:15 +08:00
</Styled.WithError>
2021-08-09 22:24:02 +08:00
) : null}
</Styled.SetTimeContainer>
2021-08-09 22:24:02 +08:00
) : null}
2021-10-29 01:26:15 +08:00
</Styled.DurationContainer>
2018-10-24 01:18:09 +08:00
);
2018-10-02 21:48:12 +08:00
}
2018-10-24 01:18:09 +08:00
2018-10-02 21:48:12 +08:00
render() {
2018-10-24 01:18:09 +08:00
const {
2021-05-18 04:25:07 +08:00
isMeteorConnected,
intl,
endAllBreakouts,
amIModerator,
2022-05-13 21:42:19 +08:00
isRTL,
2018-10-24 01:18:09 +08:00
} = this.props;
2018-10-02 21:48:12 +08:00
return (
2023-06-17 00:04:05 +08:00
<Styled.Panel ref={(n) => this.panel = n} onCopy={(e) => { e.stopPropagation(); }}>
<Header
leftButtonProps={{
'aria-label': intl.formatMessage(intlMessages.breakoutAriaTitle),
label: intl.formatMessage(intlMessages.breakoutTitle),
onClick: () => {
this.closePanel();
},
}}
customRightButton={amIModerator && (
<BreakoutDropdown
openBreakoutTimeManager={this.showSetTimeForm}
endAllBreakouts={() => {
this.closePanel();
endAllBreakouts();
}}
isMeteorConnected={isMeteorConnected}
amIModerator={amIModerator}
2022-05-13 21:42:19 +08:00
isRTL={isRTL}
/>
)}
/>
{this.renderDuration()}
2022-01-29 01:41:10 +08:00
{amIModerator
? (
<MessageFormContainer
{...{
title: intl.formatMessage(intlMessages.chatTitleMsgAllRooms),
}}
chatId="breakouts"
chatTitle={intl.formatMessage(intlMessages.chatTitleMsgAllRooms)}
disabled={!isMeteorConnected}
connected={isMeteorConnected}
locked={false}
/>
) : null }
{amIModerator ? <Styled.Separator /> : null }
2018-10-24 01:18:09 +08:00
{this.renderBreakoutRooms()}
2021-10-29 01:26:15 +08:00
</Styled.Panel>
2018-10-02 21:48:12 +08:00
);
}
}
export default injectIntl(BreakoutRoom);