bigbluebutton-Github/bigbluebutton-html5/imports/startup/client/base.jsx
Mario Jr 8036ce71b1 Fix audio and push alerts for user join
When validating tokens, the dummyUser created at the beginning is set
with validated=true. This means there won't be the state change that used
to occur from validated:false to validated:true (which detects the moment
joined the meeting) , therefore the alert code that expects for this change
won't run.
To fix this for audio alerts, we now detect when user join by observing
additions in Users's collection. This is actually good because we start
observing this only once (in componentDidMount), differently we used to do,
by calling this every time the tracker was activated.

To distinguish between the user addition that initially populates user's
collection from those that happens after user join (which are the ones we
want), we store the initial state (at componentDidMount) and compare it
with new additions. If the user added is present at the initial state,
then it is an addition to populates the collection, otherwise this is a real
user addition (happened after user joined the meeting)

Partially fixes #11399
2021-03-02 08:43:56 -03:00

445 lines
13 KiB
JavaScript
Executable File

import React, { Component, Fragment } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import Auth from '/imports/ui/services/auth';
import AppContainer from '/imports/ui/components/app/container';
import ErrorScreen from '/imports/ui/components/error-screen/component';
import MeetingEnded from '/imports/ui/components/meeting-ended/component';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import Settings from '/imports/ui/services/settings';
import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import { Session } from 'meteor/session';
import { FormattedMessage } from 'react-intl';
import Meetings, { RecordMeetings } from '../../api/meetings';
import AppService from '/imports/ui/components/app/service';
import Breakouts from '/imports/api/breakouts';
import AudioService from '/imports/ui/components/audio/service';
import { notify } from '/imports/ui/services/notification';
import deviceInfo from '/imports/utils/deviceInfo';
import { invalidateCookie } from '/imports/ui/components/audio/audio-modal/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import LayoutManager from '/imports/ui/components/layout/layout-manager';
import { withLayoutContext } from '/imports/ui/components/layout/context';
import VideoService from '/imports/ui/components/video-provider/service';
import DebugWindow from '/imports/ui/components/debug-window/component'
import {Meteor} from "meteor/meteor";
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_ENABLED = CHAT_CONFIG.enabled;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const BREAKOUT_END_NOTIFY_DELAY = 50;
const HTML = document.getElementsByTagName('html')[0];
let breakoutNotified = false;
const propTypes = {
subscriptionsReady: PropTypes.bool,
approved: PropTypes.bool,
meetingHasEnded: PropTypes.bool.isRequired,
meetingExist: PropTypes.bool,
};
const defaultProps = {
approved: false,
meetingExist: false,
subscriptionsReady: false,
};
const fullscreenChangedEvents = [
'fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'MSFullscreenChange',
];
class Base extends Component {
static handleFullscreenChange() {
if (document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement) {
Session.set('isFullscreen', true);
} else {
Session.set('isFullscreen', false);
}
}
constructor(props) {
super(props);
this.state = {
loading: false,
meetingExisted: false,
};
this.updateLoadingState = this.updateLoadingState.bind(this);
}
componentDidMount() {
const { animations } = this.props;
const {
userID: localUserId,
credentials,
} = Auth;
const { meetingId } = credentials;
if (animations) HTML.classList.add('animationsEnabled');
if (!animations) HTML.classList.add('animationsDisabled');
fullscreenChangedEvents.forEach((event) => {
document.addEventListener(event, Base.handleFullscreenChange);
});
Session.set('isFullscreen', false);
const users = Users.find({}, { fields: {
validated: 1,
name: 1,
userId: 1,
meetingId: 1,
}
});
this.usersAlreadyInMeetingAtBeggining =
users && (typeof users.map === 'function') ?
users.map(user => user.userId)
: [];
users.observe({
added: (user) => {
const {
userJoinAudioAlerts,
userJoinPushAlerts,
} = Settings.application;
if (user.validated && user.name
&& user.userId !== localUserId
&& meetingId == user.meetingId
&& !this.usersAlreadyInMeetingAtBeggining.includes(user.userId)) {
if (userJoinAudioAlerts) {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
+ '/resources/sounds/userJoin.mp3');
}
if (userJoinPushAlerts) {
notify(
<FormattedMessage
id="app.notification.userJoinPushAlert"
description="Notification for a user joins the meeting"
values={{
0: user.name,
}}
/>,
'info',
'user',
);
}
}
}
});
}
componentDidUpdate(prevProps, prevState) {
const {
approved,
meetingExist,
animations,
ejected,
isMeteorConnected,
subscriptionsReady,
meetingIsBreakout,
layoutContextDispatch,
usersVideo,
} = this.props;
const {
loading,
meetingExisted,
} = this.state;
if (prevProps.meetingIsBreakout === undefined && !meetingIsBreakout) {
invalidateCookie('joinedAudio');
}
if (usersVideo !== prevProps.usersVideo) {
layoutContextDispatch(
{
type: 'setUsersVideo',
value: usersVideo.length,
},
);
}
if (!prevProps.subscriptionsReady && subscriptionsReady) {
logger.info({ logCode: 'startup_client_subscriptions_ready' }, 'Subscriptions are ready');
}
if (prevProps.meetingExist && !meetingExist && !meetingExisted) {
this.setMeetingExisted(true);
}
// In case the meteor restart avoid error log
if (isMeteorConnected && (prevState.meetingExisted !== meetingExisted) && meetingExisted) {
this.setMeetingExisted(false);
}
// In case the meeting delayed to load
if (!subscriptionsReady || !meetingExist) return;
if (approved && loading && subscriptionsReady) this.updateLoadingState(false);
if (prevProps.ejected || ejected) {
Session.set('codeError', '403');
Session.set('isMeetingEnded', true);
}
// In case the meteor restart avoid error log
if (isMeteorConnected && (prevState.meetingExisted !== meetingExisted)) {
this.setMeetingExisted(false);
}
const enabled = HTML.classList.contains('animationsEnabled');
const disabled = HTML.classList.contains('animationsDisabled');
if (animations && animations !== prevProps.animations) {
if (disabled) HTML.classList.remove('animationsDisabled');
HTML.classList.add('animationsEnabled');
} else if (!animations && animations !== prevProps.animations) {
if (enabled) HTML.classList.remove('animationsEnabled');
HTML.classList.add('animationsDisabled');
}
}
componentWillUnmount() {
fullscreenChangedEvents.forEach((event) => {
document.removeEventListener(event, Base.handleFullscreenChange);
});
}
setMeetingExisted(meetingExisted) {
this.setState({ meetingExisted });
}
updateLoadingState(loading = false) {
this.setState({
loading,
});
}
renderByState() {
const { updateLoadingState } = this;
const stateControls = { updateLoadingState };
const { loading } = this.state;
const {
codeError,
ejected,
ejectedReason,
meetingExist,
meetingHasEnded,
meetingIsBreakout,
subscriptionsReady,
User,
} = this.props;
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
return (<LoadingScreen>{loading}</LoadingScreen>);
}
if (ejected) {
return (<MeetingEnded code="403" reason={ejectedReason} />);
}
if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
window.close();
return null;
}
if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && User?.loggedOut)) {
return (<MeetingEnded code={codeError} />);
}
if (codeError && !meetingHasEnded) {
// 680 is set for the codeError when the user requests a logout
if (codeError !== '680') {
return (<ErrorScreen code={codeError} />);
}
return (<MeetingEnded code={codeError} />);
}
return (<AppContainer {...this.props} baseControls={stateControls} />);
}
render() {
const {
meetingExist,
} = this.props;
const { meetingExisted } = this.state;
return (
<Fragment>
{meetingExist && Auth.loggedIn && <DebugWindow />}
{meetingExist && Auth.loggedIn && <LayoutManager />}
{
(!meetingExisted && !meetingExist && Auth.loggedIn)
? <LoadingScreen />
: this.renderByState()
}
</Fragment>
);
}
}
Base.propTypes = propTypes;
Base.defaultProps = defaultProps;
const BaseContainer = withTracker(() => {
const {
animations,
userJoinAudioAlerts,
userJoinPushAlerts,
} = Settings.application;
const {
credentials,
loggedIn,
userID: localUserId,
} = Auth;
const { meetingId } = credentials;
let breakoutRoomSubscriptionHandler;
let meetingModeratorSubscriptionHandler;
const fields = {
approved: 1,
authed: 1,
ejected: 1,
ejectedReason: 1,
color: 1,
effectiveConnectionType: 1,
extId: 1,
guest: 1,
intId: 1,
locked: 1,
loggedOut: 1,
meetingId: 1,
userId: 1,
inactivityCheck: 1,
responseDelay: 1,
};
const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
const meeting = Meetings.findOne({ meetingId }, {
fields: {
meetingEnded: 1,
meetingProp: 1,
},
});
if (meeting && meeting.meetingEnded) {
Session.set('codeError', '410');
}
const approved = User?.approved && User?.guest;
const ejected = User?.ejected;
const ejectedReason = User?.ejectedReason;
let userSubscriptionHandler;
Breakouts.find({}, { fields: { _id: 1 } }).observeChanges({
added() {
breakoutNotified = false;
},
removed() {
// Need to check the number of breakouts left because if a user's role changes to viewer
// then all but one room is removed. The data here isn't reactive so no need to filter
// the fields
const numBreakouts = Breakouts.find().count();
if (!AudioService.isUsingAudio() && !breakoutNotified && numBreakouts === 0) {
if (meeting && !meeting.meetingEnded && !meeting.meetingProp.isBreakout) {
// There's a race condition when reloading a tab where the collection gets cleared
// out and then refilled. The removal of the old data triggers the notification so
// instead wait a bit and check to see that records weren't added right after.
setTimeout(() => {
if (breakoutNotified) {
notify(
<FormattedMessage
id="app.toast.breakoutRoomEnded"
description="message when the breakout room is ended"
/>,
'info',
'rooms',
);
}
}, BREAKOUT_END_NOTIFY_DELAY);
}
breakoutNotified = true;
}
},
});
RecordMeetings.find({ meetingId }, { fields: { recording: 1 } }).observe({
changed: (newDocument, oldDocument) => {
if (newDocument) {
if (!oldDocument.recording && newDocument.recording) {
notify(
<FormattedMessage
id="app.notification.recordingStart"
description="Notification for when the recording starts"
/>,
'success',
'record',
);
}
if (oldDocument.recording && !newDocument.recording) {
notify(
<FormattedMessage
id="app.notification.recordingPaused"
description="Notification for when the recording stops"
/>,
'error',
'record',
);
}
}
},
});
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.type().isPhone) {
if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', PUBLIC_CHAT_ID);
} else {
Session.set('openPanel', 'userlist');
}
} else {
Session.set('openPanel', '');
}
const codeError = Session.get('codeError');
const usersVideo = VideoService.getVideoStreams();
return {
approved,
ejected,
ejectedReason,
userSubscriptionHandler,
breakoutRoomSubscriptionHandler,
meetingModeratorSubscriptionHandler,
animations,
User,
isMeteorConnected: Meteor.status().connected,
meetingExist: !!meeting,
meetingHasEnded: !!meeting && meeting.meetingEnded,
meetingIsBreakout: AppService.meetingIsBreakout(),
subscriptionsReady: Session.get('subscriptionsReady'),
loggedIn,
codeError,
usersVideo,
};
})(withLayoutContext(Base));
export default BaseContainer;