diff --git a/bigbluebutton-html5/client/main.jsx b/bigbluebutton-html5/client/main.jsx index 893beb2ef4..8a0d1df182 100755 --- a/bigbluebutton-html5/client/main.jsx +++ b/bigbluebutton-html5/client/main.jsx @@ -7,6 +7,7 @@ import Base from '/imports/startup/client/base'; import JoinHandler from '/imports/ui/components/join-handler/component'; import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component'; import Subscriptions from '/imports/ui/components/subscriptions/component'; +import JoinLoading from '/imports/ui/components/join-loading/component'; Meteor.startup(() => { // Logs all uncaught exceptions to the client logger @@ -33,13 +34,15 @@ Meteor.startup(() => { // TODO make this a Promise render( - - - - - - - , + + + + + + + + + , document.getElementById('app'), ); }); diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index eae2a5cbc0..7b4dbe945e 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -3,9 +3,7 @@ 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 AudioManager from '/imports/ui/services/audio-manager'; import logger from '/imports/startup/client/logger'; @@ -18,6 +16,7 @@ import Breakouts from '/imports/api/breakouts'; import AudioService from '/imports/ui/components/audio/service'; import { FormattedMessage } from 'react-intl'; import { notify } from '/imports/ui/services/notification'; +import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context'; const HTML = document.getElementsByTagName('html')[0]; @@ -87,12 +86,29 @@ class Base extends Component { ejected, isMeteorConnected, subscriptionsReady, + codeError, + meetingHasEnded, + dispatch, } = this.props; const { loading, meetingExisted, } = this.state; + if (codeError && !meetingHasEnded) { + // 680 is set for the codeError when the user requests a logout + if (codeError !== '680') { + logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`); + } + Session.set('codeError', codeError); + return dispatch('hasError'); + } + + if (!(loading || !subscriptionsReady) + && !meetingHasEnded && meetingExist) { + dispatch('hideLoading'); + } + if (!prevProps.subscriptionsReady && subscriptionsReady) { logger.info({ logCode: 'startup_client_subscriptions_ready' }, 'Subscriptions are ready'); } @@ -152,21 +168,14 @@ class Base extends Component { renderByState() { const { updateLoadingState } = this; const stateControls = { updateLoadingState }; - const { loading } = this.state; const codeError = Session.get('codeError'); const { ejected, - meetingExist, meetingHasEnded, meetingIsBreakout, - subscriptionsReady, User, } = this.props; - if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) { - return ({loading}); - } - if (ejected && ejected.ejectedReason) { const { ejectedReason } = ejected; AudioManager.exitAudio(); @@ -179,14 +188,6 @@ class Base extends Component { AudioManager.exitAudio(); return (); } - - if (codeError && !meetingHasEnded) { - // 680 is set for the codeError when the user requests a logout - if (codeError !== '680') { - logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`); - } - return (); - } // this.props.annotationsHandler.stop(); return (); } @@ -196,15 +197,11 @@ class Base extends Component { const { locale, meetingExist } = this.props; const stateControls = { updateLoadingState }; const { meetingExisted } = this.state; - + if ((!meetingExisted && !meetingExist && Auth.loggedIn)) return null; return ( - (!meetingExisted && !meetingExist && Auth.loggedIn) - ? - : ( - - {this.renderByState()} - - ) + + {this.renderByState()} + ); } } @@ -316,4 +313,4 @@ const BaseContainer = withTracker(() => { }; })(Base); -export default BaseContainer; +export default withJoinLoadingConsumer(BaseContainer); diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index c93805c743..8f541853ec 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; import Settings from '/imports/ui/services/settings'; -import LoadingScreen from '/imports/ui/components/loading-screen/component'; +import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context'; // currently supported locales. import ar from 'react-intl/locale-data/ar'; @@ -106,7 +106,7 @@ class IntlStartup extends Component { fetchLocalizedMessages(locale) { const url = `/html5client/locale?locale=${locale}`; - + const { dispatch } = this.props; this.setState({ fetching: true }, () => { fetch(url) .then((response) => { @@ -120,6 +120,7 @@ class IntlStartup extends Component { const dasherizedLocale = normalizedLocale.replace('_', '-'); this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => { this.saveLocale(dasherizedLocale); + dispatch('hideLoading'); }); }) .catch(() => { @@ -146,7 +147,7 @@ class IntlStartup extends Component { const { fetching, normalizedLocale, messages } = this.state; const { children } = this.props; - return fetching ? : ( + return !fetching && ( {children} @@ -154,7 +155,7 @@ class IntlStartup extends Component { } } -export default IntlStartup; +export default withJoinLoadingConsumer(IntlStartup); IntlStartup.propTypes = propTypes; IntlStartup.defaultProps = defaultProps; diff --git a/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx b/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx index 209052abd1..f13b2bd894 100644 --- a/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx @@ -2,18 +2,13 @@ import React, { Component } from 'react'; import { Session } from 'meteor/session'; import logger from '/imports/startup/client/logger'; import Auth from '/imports/ui/services/auth'; -import LoadingScreen from '/imports/ui/components/loading-screen/component'; +import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context'; const STATUS_CONNECTING = 'connecting'; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; class AuthenticatedHandler extends Component { - static setError(codeError) { - Session.set('hasError', true); - if (codeError) Session.set('codeError', codeError); - } - static shouldAuthenticate(status, lastStatus) { return lastStatus != null && lastStatus === STATUS_CONNECTING && status.connected; } @@ -35,7 +30,32 @@ class AuthenticatedHandler extends Component { }); } - static async authenticatedRouteHandler(callback) { + constructor(props) { + super(props); + this.setError = this.setError.bind(this); + this.authenticatedRouteHandler = this.authenticatedRouteHandler.bind(this); + this.state = { + authenticated: false, + }; + } + + componentDidMount() { + if (Session.get('codeError')) { + return this.setError(Session.get('codeError')); + } + return this.authenticatedRouteHandler((value, error) => { + if (error) return this.setError(error); + return this.setState({ authenticated: true }); + }); + } + + setError(codeError) { + const { dispatch } = this.props; + dispatch('hasError'); + if (codeError) Session.set('codeError', codeError); + } + + async authenticatedRouteHandler(callback) { if (Auth.loggedIn) { callback(); } @@ -43,6 +63,7 @@ class AuthenticatedHandler extends Component { AuthenticatedHandler.addReconnectObservable(); const setReason = (reason) => { + this.setError(reason.error); logger.error({ logCode: 'authenticatedhandlercomponent_setreason', extraInfo: { reason }, @@ -60,21 +81,6 @@ class AuthenticatedHandler extends Component { } } - constructor(props) { - super(props); - this.state = { - authenticated: false, - }; - } - - componentDidMount() { - if (Session.get('codeError')) return this.changeState(true); - AuthenticatedHandler.authenticatedRouteHandler((value, error) => { - if (error) AuthenticatedHandler.setError(error); - this.setState({ authenticated: true }); - }); - } - render() { const { children, @@ -89,11 +95,9 @@ class AuthenticatedHandler extends Component { Session.set('isPollOpen', false); Session.set('breakoutRoomIsOpen', false); - return authenticated - ? children - : (); + return authenticated && children; } } -export default AuthenticatedHandler; +export default withJoinLoadingConsumer(AuthenticatedHandler); diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx index bfcf865a4e..609b7f6dee 100644 --- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx @@ -57,16 +57,16 @@ class ErrorScreen extends React.PureComponent { children, } = this.props; + const codeError = Session.get('codeError') || code; let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]); - if (code in intlMessages) { - formatedMessage = intl.formatMessage(intlMessages[code]); + if (codeError in intlMessages) { + formatedMessage = intl.formatMessage(intlMessages[codeError]); } - return (

- {code} + {codeError}

{formatedMessage} diff --git a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx index d5e2d13c4b..57d1b3c7b8 100644 --- a/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/join-handler/component.jsx @@ -6,7 +6,7 @@ import { setCustomLogoUrl } from '/imports/ui/components/user-list/service'; import { makeCall } from '/imports/ui/services/api'; import deviceInfo from '/imports/utils/deviceInfo'; import logger from '/imports/startup/client/logger'; -import LoadingScreen from '/imports/ui/components/loading-screen/component'; +import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context'; const propTypes = { children: PropTypes.element.isRequired, @@ -17,14 +17,10 @@ const { showParticipantsOnLogin } = APP_CONFIG; const CHAT_ENABLED = Meteor.settings.public.chat.enabled; class JoinHandler extends Component { - static setError(codeError) { - Session.set('hasError', true); - if (codeError) Session.set('codeError', codeError); - } - constructor(props) { super(props); this.fetchToken = this.fetchToken.bind(this); + this.setError = this.setError.bind(this); this.numFetchTokenRetries = 0; this.state = { @@ -41,6 +37,12 @@ class JoinHandler extends Component { this._isMounted = false; } + setError(codeError) { + const { dispatch } = this.props; + if (codeError) Session.set('codeError', codeError); + dispatch('hasError'); + } + async fetchToken() { if (!this._isMounted) return; @@ -63,7 +65,7 @@ class JoinHandler extends Component { const sessionToken = urlParams.get('sessionToken'); if (!sessionToken) { - JoinHandler.setError('400'); + this.setError('400'); Session.set('errorMessageDescription', 'Session token was not provided'); } @@ -141,7 +143,6 @@ class JoinHandler extends Component { const { response } = parseToJson; setLogoutURL(response); - if (response.returncode !== 'FAILED') { await setAuth(response); @@ -167,9 +168,11 @@ class JoinHandler extends Component { response, }, }, 'User successfully went through main.joinRouteHandler'); + this.setState({ joined: true }); } else { const e = new Error(response.message); if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message); + this.setError(401); logger.error({ logCode: 'joinhandler_component_joinroutehandler_error', extraInfo: { @@ -178,18 +181,15 @@ class JoinHandler extends Component { }, }, 'User faced an error on main.joinRouteHandler.'); } - this.setState({ joined: true }); } render() { const { children } = this.props; const { joined } = this.state; - return joined - ? children - : (); + return joined && children; } } -export default JoinHandler; +export default withJoinLoadingConsumer(JoinHandler); JoinHandler.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/join-loading/component.jsx b/bigbluebutton-html5/imports/ui/components/join-loading/component.jsx new file mode 100644 index 0000000000..c52f5c81b1 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/join-loading/component.jsx @@ -0,0 +1,23 @@ +import React, { Fragment } from 'react'; +import LoadingScreen from '/imports/ui/components/loading-screen/component'; +import ErrorScreen from '/imports/ui/components/error-screen/component'; +import { withJoinLoadingContext } from './context/context'; +import IntlProvider from '/imports/startup/client/intl'; +import Settings from '/imports/ui/services/settings'; + +const JoinLoadComponent = (props) => { + const { children, showLoading, hasError } = props; + const { locale } = Settings.application; + return ( + + {(showLoading && !hasError) && ()} + {hasError ? ( + + + + ) : children} + + ); +}; + +export default withJoinLoadingContext(JoinLoadComponent); diff --git a/bigbluebutton-html5/imports/ui/components/join-loading/context/context.jsx b/bigbluebutton-html5/imports/ui/components/join-loading/context/context.jsx new file mode 100644 index 0000000000..3c40b6c545 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/join-loading/context/context.jsx @@ -0,0 +1,64 @@ +import React, { useReducer, createContext } from 'react'; + +function reducer(state, action) { + switch (action) { + case 'showLoading': + return { + ...state, + showLoading: true, + }; + case 'hideLoading': + return { + ...state, + showLoading: false, + }; + case 'hasError': + return { + ...state, + hasError: true, + }; + default: + throw new Error(); + } +} + +const JoinLoadingContext = createContext({}); +const initialState = { + showLoading: true, + hasError: false, +}; +const JoinLoading = Component => (props) => { + const [state, dispatch] = useReducer(reducer, initialState); + const contextValue = { + ...props, + ...state, + dispatch, + }; + + return ( + + + + ); +}; + +const joinLoadingContextConsumer = Component => props => ( + + { contexts => } + +); + + +const withJoinLoadingConsumer = Component => joinLoadingContextConsumer(Component); + +const withJoinLoadingContext = Component => JoinLoading(withJoinLoadingConsumer(Component)); + +export { + withJoinLoadingContext, + withJoinLoadingConsumer, + joinLoadingContextConsumer, + JoinLoading, + reducer, + initialState, + JoinLoadingContext, +}; diff --git a/bigbluebutton-html5/imports/ui/components/loading-screen/styles.scss b/bigbluebutton-html5/imports/ui/components/loading-screen/styles.scss index eeaf0ceade..0cfaa82333 100644 --- a/bigbluebutton-html5/imports/ui/components/loading-screen/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/loading-screen/styles.scss @@ -11,13 +11,18 @@ } .background { - position: fixed; display: flex; flex-flow: column; justify-content: center; + position: fixed; width: 100%; height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; background-color: var(--loader-bg); + z-index: 99999; } .message { diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx index 55ccf33c9b..33e4d51a79 100755 --- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx @@ -8,6 +8,7 @@ import Annotations from '/imports/api/annotations'; import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service'; import AnnotationsLocal from '/imports/ui/components/whiteboard/service'; import mapUser from '/imports/ui/services/user/mapUser'; +import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context'; const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_ENABLED = CHAT_CONFIG.enabled; @@ -34,7 +35,7 @@ class Subscriptions extends Component { } } -export default withTracker(() => { +export default withJoinLoadingConsumer(withTracker(({ dispatch }) => { const { credentials } = Auth; const { meetingId, requesterUserId } = credentials; if (Session.get('codeError')) { @@ -50,6 +51,7 @@ export default withTracker(() => { extraInfo: { error }, }, 'Error while subscribing to collections'); Session.set('codeError', error.error); + dispatch('hasError'); }, }; @@ -115,4 +117,4 @@ export default withTracker(() => { subscriptionsReady: ready, subscriptionsHandlers, }; -})(Subscriptions); +})(Subscriptions));