`;
- }
-
- /**
- *
- */
- render() {
- this.htmlStr = this._getHtmlStr(this.callee);
- this._attach();
- }
-
- /**
- * Destroys and clears all the objects (html elements and audio interval)
- * related to the ring overlay.
- */
- destroy() {
- this.isRinging = false;
- this._stopAudio();
- this._detach();
- }
-
- _attach() {
- $("body").append(this.htmlStr);
- }
-
- _detach() {
- $(`#${this._containerId}`).remove();
- }
-
- /**
- * Stops the ringing and clears related timers.
- */
- _stopAudio() {
- if (this.interval) {
- clearInterval(this.interval);
- }
- if (this._timeout) {
- clearTimeout(this._timeout);
- }
- }
-}
-
-export default {
- /**
- * Shows the ring overlay for the passed callee.
- *
- * @param {Object} callee - The callee. Object containing data about
- * callee.
- * @param {boolean} disableRinging - If true the ringing sound won't be
- * played.
- * @returns {void}
- */
- show(callee, disableRinging = false) {
- if (overlay) {
- this.hide();
- }
-
- overlay = new RingOverlay(callee, disableRinging);
- APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
- onAvatarVisible);
- },
-
- /**
- * Hides the ring overlay. Destroys all the elements related to the ring
- * overlay.
- */
- hide() {
- if (!overlay) {
- return false;
- }
- overlay.destroy();
- overlay = null;
- APP.UI.removeListener(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
- onAvatarVisible);
- return true;
- },
-
- /**
- * Checks whether or not the ring overlay is currently displayed.
- *
- * @returns {boolean} true if the ring overlay is currently displayed or
- * false otherwise.
- */
- isVisible() {
- return overlay !== null;
- }
-};
diff --git a/react/features/base/connection/actions.web.js b/react/features/base/connection/actions.web.js
index 80dd21bfda..422d9b5734 100644
--- a/react/features/base/connection/actions.web.js
+++ b/react/features/base/connection/actions.web.js
@@ -77,7 +77,6 @@ export function connect() {
}
})
.catch(error => {
- APP.UI.hideRingOverlay();
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
diff --git a/react/features/filmstrip/middleware.js b/react/features/filmstrip/middleware.js
index cd848d4a06..d9d90e6552 100644
--- a/react/features/filmstrip/middleware.js
+++ b/react/features/filmstrip/middleware.js
@@ -1,6 +1,10 @@
-import UIEvents from '../../../service/UI/UIEvents';
+/* @flow */
import { MiddlewareRegistry } from '../base/redux';
+import { SET_CALL_OVERLAY_VISIBLE } from '../jwt';
+
+import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
+import UIEvents from '../../../service/UI/UIEvents';
import {
SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
@@ -10,19 +14,33 @@ import {
declare var APP: Object;
// eslint-disable-next-line no-unused-vars
-MiddlewareRegistry.register(store => next => action => {
- const result = next(action);
-
+MiddlewareRegistry.register(({ getState }) => next => action => {
switch (action.type) {
- case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
- case SET_FILMSTRIP_VISIBILITY: {
- if (typeof APP !== 'undefined') {
- APP.UI.emitEvent(UIEvents.UPDATED_FILMSTRIP_DISPLAY);
+ case SET_CALL_OVERLAY_VISIBLE:
+ if (typeof APP === 'undefined') {
+ const oldValue
+ = Boolean(getState()['features/jwt'].callOverlayVisible);
+ const result = next(action);
+ const newValue
+ = Boolean(getState()['features/jwt'].callOverlayVisible);
+ oldValue === newValue
+ || Filmstrip.toggleFilmstrip(!newValue, false);
+
+ return result;
}
break;
+
+ case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
+ case SET_FILMSTRIP_VISIBILITY: {
+ const result = next(action);
+
+ typeof APP === 'undefined'
+ || APP.UI.emitEvent(UIEvents.UPDATED_FILMSTRIP_DISPLAY);
+
+ return result;
}
}
- return result;
+ return next(action);
});
diff --git a/react/features/jwt/actionTypes.js b/react/features/jwt/actionTypes.js
index c95ee71c19..9061caf5da 100644
--- a/react/features/jwt/actionTypes.js
+++ b/react/features/jwt/actionTypes.js
@@ -1,3 +1,13 @@
+/**
+ * The type of redux action which sets the visibility of {@code CallOverlay}.
+ *
+ * {
+ * type: SET_CALL_OVERLAY_VISIBLE,
+ * callOverlayVisible: boolean
+ * }
+ */
+export const SET_CALL_OVERLAY_VISIBLE = Symbol('SET_CALL_OVERLAY_VISIBLE');
+
/**
* The type of redux action which stores a specific JSON Web Token (JWT) into
* the redux store.
diff --git a/react/features/jwt/actions.js b/react/features/jwt/actions.js
index 4710b75ed7..53d374833a 100644
--- a/react/features/jwt/actions.js
+++ b/react/features/jwt/actions.js
@@ -1,6 +1,27 @@
/* @flow */
-import { SET_JWT } from './actionTypes';
+import { SET_CALL_OVERLAY_VISIBLE, SET_JWT } from './actionTypes';
+
+/**
+ * Sets the visibility of {@code CallOverlay}.
+ *
+ * @param {boolean|undefined} callOverlayVisible - If {@code CallOverlay} is to
+ * be displayed/visible, then {@code true}; otherwise, {@code false} or
+ * {@code undefined}.
+ * @returns {{
+ * type: SET_CALL_OVERLAY_VISIBLE,
+ * callOverlayVisible: (boolean|undefined)
+ * }}
+ */
+export function setCallOverlayVisible(callOverlayVisible: boolean) {
+ return (dispatch: Dispatch<*>, getState: Function) => {
+ getState()['features/jwt'].callOverlayVisible === callOverlayVisible
+ || dispatch({
+ type: SET_CALL_OVERLAY_VISIBLE,
+ callOverlayVisible
+ });
+ };
+}
/**
* Stores a specific JSON Web Token (JWT) into the redux store.
diff --git a/react/features/jwt/components/CallOverlay.js b/react/features/jwt/components/CallOverlay.js
new file mode 100644
index 0000000000..ec52d3b2d3
--- /dev/null
+++ b/react/features/jwt/components/CallOverlay.js
@@ -0,0 +1,288 @@
+/* @flow */
+
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { Audio } from '../../base/media';
+import { Avatar } from '../../base/participants';
+import { Container, Text } from '../../base/react';
+import UIEvents from '../../../../service/UI/UIEvents';
+
+declare var $: Object;
+declare var APP: Object;
+declare var interfaceConfig: Object;
+
+/**
+ * Implements a React {@link Component} which depicts the establishment of a
+ * call with a specific remote callee.
+ *
+ * @extends Component
+ */
+class CallOverlay extends Component {
+ /**
+ * The (reference to the) {@link Audio} which plays/renders the audio
+ * depicting the ringing phase of the call establishment represented by this
+ * {@code CallOverlay}.
+ */
+ _audio: ?Audio
+
+ _onLargeVideoAvatarVisible: Function
+
+ _playAudioInterval: ?number
+
+ _ringingTimeout: ?number
+
+ _setAudio: Function
+
+ state: {
+
+ /**
+ * The CSS class (name), if any, to add to this {@code CallOverlay}.
+ *
+ * @type {string}
+ */
+ className: ?string,
+
+ /**
+ * The indicator which determines whether this {@code CallOverlay}
+ * should play/render audio to indicate the ringing phase of the
+ * call establishment between the local participant and the
+ * associated remote callee.
+ *
+ * @type {boolean}
+ */
+ renderAudio: boolean,
+
+ /**
+ * The indicator which determines whether this {@code CallOverlay}
+ * is depicting the ringing phase of the call establishment between
+ * the local participant and the associated remote callee or the
+ * phase afterwards when the callee has not answered the call for a
+ * period of time and, consequently, is considered unavailable.
+ *
+ * @type {boolean}
+ */
+ ringing: boolean
+ }
+
+ /**
+ * {@code CallOverlay} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ _callee: React.PropTypes.object
+ };
+
+ /**
+ * Initializes a new {@code CallOverlay} instance.
+ *
+ * @param {Object} props - The read-only React {@link Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ className: undefined,
+ renderAudio:
+ typeof interfaceConfig !== 'object'
+ || !interfaceConfig.DISABLE_RINGING,
+ ringing: true
+ };
+
+ this._onLargeVideoAvatarVisible
+ = this._onLargeVideoAvatarVisible.bind(this);
+ this._setAudio = this._setAudio.bind(this);
+
+ if (typeof APP === 'object') {
+ APP.UI.addListener(
+ UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
+ this._onLargeVideoAvatarVisible);
+ }
+ }
+
+ /**
+ * Sets up timeouts such as the timeout to end the ringing phase of the call
+ * establishment depicted by this {@code CallOverlay}.
+ *
+ * @inheritdoc
+ */
+ componentDidMount() {
+ // Set up the timeout to end the ringing phase of the call establishment
+ // depicted by this CallOverlay.
+ if (this.state.ringing && !this._ringingTimeout) {
+ this._ringingTimeout
+ = setTimeout(
+ () => {
+ this._pauseAudio();
+
+ this._ringingTimeout = undefined;
+ this.setState({
+ ringing: false
+ });
+ },
+ 30000);
+ }
+
+ this._playAudio();
+ }
+
+ /**
+ * Cleans up before this {@code Calleverlay} is unmounted and destroyed.
+ *
+ * @inheritdoc
+ */
+ componentWillUnmount() {
+ this._pauseAudio();
+
+ if (this._ringingTimeout) {
+ clearTimeout(this._ringingTimeout);
+ this._ringingTimeout = undefined;
+ }
+
+ if (typeof APP === 'object') {
+ APP.UI.removeListener(
+ UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
+ this._onLargeVideoAvatarVisible);
+ }
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { className, ringing } = this.state;
+ const { avatarUrl, name } = this.props._callee;
+
+ return (
+
+
+ { ringing ? Calling... : null }
+
+
+
+ { name }
+ { ringing ? null : ' isn\'t available' }
+
+
+
+ { this._renderAudio() }
+
+ );
+ }
+
+ /**
+ * Notifies this {@code CallOverlay} that the visibility of the
+ * participant's avatar in the large video has changed.
+ *
+ * @param {boolean} largeVideoAvatarVisible - If the avatar in the large
+ * video (i.e. of the participant on the stage) is visible, then
+ * {@code true}; otherwise, {@code false}.
+ * @private
+ * @returns {void}
+ */
+ _onLargeVideoAvatarVisible(largeVideoAvatarVisible: boolean) {
+ this.setState({
+ className: largeVideoAvatarVisible ? 'solidBG' : undefined
+ });
+ }
+
+ /**
+ * Stops the playback of the audio which represents the ringing phase of the
+ * call establishment depicted by this {@code CallOverlay}.
+ *
+ * @private
+ * @returns {void}
+ */
+ _pauseAudio() {
+ const audio = this._audio;
+
+ if (audio) {
+ audio.pause();
+ }
+ if (this._playAudioInterval) {
+ clearInterval(this._playAudioInterval);
+ this._playAudioInterval = undefined;
+ }
+ }
+
+ /**
+ * Starts the playback of the audio which represents the ringing phase of
+ * the call establishment depicted by this {@code CallOverlay}.
+ *
+ * @private
+ * @returns {void}
+ */
+ _playAudio() {
+ if (this._audio) {
+ this._audio.play();
+ if (!this._playAudioInterval) {
+ this._playAudioInterval
+ = setInterval(() => this._playAudio(), 5000);
+ }
+ }
+ }
+
+ /**
+ * Renders an audio element to represent the ringing phase of the call
+ * establishment represented by this {@code CallOverlay}.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderAudio() {
+ if (this.state.renderAudio && this.state.ringing) {
+ return (
+
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the (reference to the) {@link Audio} which renders the ringing phase
+ * of the call establishment represented by this {@code CallOverlay}.
+ *
+ * @param {Audio} audio - The (reference to the) {@code Audio} which
+ * plays/renders the audio depicting the ringing phase of the call
+ * establishment represented by this {@code CallOverlay}.
+ * @private
+ * @returns {void}
+ */
+ _setAudio(audio) {
+ this._audio = audio;
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to {@code CallOverlay}'s props.
+ *
+ * @param {Object} state - The redux state.
+ * @private
+ * @returns {{
+ * _callee: Object
+ * }}
+ */
+function _mapStateToProps(state) {
+ return {
+ /**
+ *
+ * @private
+ * @type {Object}
+ */
+ _callee: state['features/jwt'].callee
+ };
+}
+
+export default connect(_mapStateToProps)(CallOverlay);
diff --git a/react/features/jwt/components/index.js b/react/features/jwt/components/index.js
new file mode 100644
index 0000000000..fb6e02ca99
--- /dev/null
+++ b/react/features/jwt/components/index.js
@@ -0,0 +1 @@
+export { default as CallOverlay } from './CallOverlay';
diff --git a/react/features/jwt/index.js b/react/features/jwt/index.js
index eea8d73936..3354f4deb3 100644
--- a/react/features/jwt/index.js
+++ b/react/features/jwt/index.js
@@ -1,4 +1,5 @@
export * from './actions';
+export * from './components';
export * from './functions';
import './middleware';
diff --git a/react/features/jwt/middleware.js b/react/features/jwt/middleware.js
index 79f0ce72e1..c838227512 100644
--- a/react/features/jwt/middleware.js
+++ b/react/features/jwt/middleware.js
@@ -1,22 +1,38 @@
import jwtDecode from 'jwt-decode';
+import {
+ CONFERENCE_FAILED,
+ CONFERENCE_LEFT,
+ CONFERENCE_WILL_LEAVE,
+ SET_ROOM
+} from '../base/conference';
import { SET_CONFIG } from '../base/config';
import { SET_LOCATION_URL } from '../base/connection';
+import { LIB_INIT_ERROR } from '../base/lib-jitsi-meet';
+import { PARTICIPANT_JOINED } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
-import { setJWT } from './actions';
+import { setCallOverlayVisible, setJWT } from './actions';
import { SET_JWT } from './actionTypes';
import { parseJWTFromURLParams } from './functions';
/**
* Middleware to parse token data upon setting a new room URL.
*
- * @param {Store} store - The Redux store.
+ * @param {Store} store - The redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
+ case CONFERENCE_FAILED:
+ case CONFERENCE_LEFT:
+ case CONFERENCE_WILL_LEAVE:
+ case LIB_INIT_ERROR:
+ case PARTICIPANT_JOINED:
+ case SET_ROOM:
+ return _maybeSetCallOverlayVisible(store, next, action);
+
case SET_CONFIG:
case SET_LOCATION_URL:
// XXX The JSON Web Token (JWT) is not the only piece of state that we
@@ -33,16 +49,87 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
+/**
+ * Notifies the feature jwt that a specific {@code action} is being dispatched
+ * within a specific redux {@code store} which may have an effect on the
+ * visiblity of (the) {@code CallOverlay}.
+ *
+ * @param {Store} store - The redux store in which the specified {@code action}
+ * is being dispatched.
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
+ * specified {@code action} to the specified {@code store}.
+ * @param {Action} action - The redux action which is being dispatched in the
+ * specified {@code store}.
+ * @private
+ * @returns {Object} The new state that is the result of the reduction of the
+ * specified {@code action}.
+ */
+function _maybeSetCallOverlayVisible({ dispatch, getState }, next, action) {
+ const result = next(action);
+
+ const state = getState();
+ const stateFeaturesJWT = state['features/jwt'];
+ let callOverlayVisible;
+
+ if (stateFeaturesJWT.callee) {
+ const { conference, leaving, room } = state['features/base/conference'];
+
+ // XXX The CallOverlay is to be displayed/visible as soon as
+ // possible including even before the conference is joined.
+ if (room && (!conference || conference !== leaving)) {
+ switch (action.type) {
+ case CONFERENCE_FAILED:
+ case CONFERENCE_LEFT:
+ case CONFERENCE_WILL_LEAVE:
+ case LIB_INIT_ERROR:
+ // Because the CallOverlay is to be displayed/visible as soon as
+ // possible even before the connection is established and/or the
+ // conference is joined, it is very complicated to figure out
+ // based on the current state alone. In order to reduce the
+ // risks of displaying the CallOverly at inappropirate times, do
+ // not even attempt to figure out based on the current state.
+ // The (redux) actions listed above are also the equivalents of
+ // the execution ponints at which APP.UI.hideRingOverlay() used
+ // to be invoked.
+ break;
+
+ default: {
+ // The CallOverlay it to no longer be displayed/visible as soon
+ // as another participant joins.
+ const participants = state['features/base/participants'];
+
+ callOverlayVisible
+ = Boolean(
+ participants
+ && participants.length === 1
+ && participants[0].local);
+
+ // However, the CallDialog is not to be displayed/visible again
+ // after all remote participants leave.
+ if (callOverlayVisible
+ && stateFeaturesJWT.callOverlayVisible === false) {
+ callOverlayVisible = false;
+ }
+ break;
+ }
+ }
+ }
+ }
+ dispatch(setCallOverlayVisible(callOverlayVisible));
+
+ return result;
+}
+
/**
* Notifies the feature jwt that the action {@link SET_CONFIG} or
- * {@link SET_LOCATION_URL} is being dispatched within a specific Redux
+ * {@link SET_LOCATION_URL} is being dispatched within a specific redux
* {@code store}.
*
- * @param {Store} store - The Redux store in which the specified {@code action}
+ * @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
- * @param {Dispatch} next - The Redux dispatch function to dispatch the
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
* specified {@code action} to the specified {@code store}.
- * @param {Action} action - The Redux action {@code SET_CONFIG} or
+ * @param {Action} action - The redux action {@code SET_CONFIG} or
* {@code SET_LOCATION_URL} which is being dispatched in the specified
* {@code store}.
* @private
@@ -65,26 +152,26 @@ function _setConfigOrLocationURL({ dispatch, getState }, next, action) {
/**
* Notifies the feature jwt that the action {@link SET_JWT} is being dispatched
- * within a specific Redux {@code store}.
+ * within a specific redux {@code store}.
*
- * @param {Store} store - The Redux store in which the specified {@code action}
+ * @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
- * @param {Dispatch} next - The Redux dispatch function to dispatch the
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
* specified {@code action} to the specified {@code store}.
- * @param {Action} action - The Redux action {@code SET_JWT} which is being
+ * @param {Action} action - The redux action {@code SET_JWT} which is being
* dispatched in the specified {@code store}.
* @private
* @returns {Object} The new state that is the result of the reduction of the
* specified {@code action}.
*/
-function _setJWT({ getState }, next, action) {
+function _setJWT(store, next, action) {
// eslint-disable-next-line no-unused-vars
const { jwt, type, ...actionPayload } = action;
if (jwt && !Object.keys(actionPayload).length) {
const {
enableUserRolesBasedOnToken
- } = getState()['features/base/config'];
+ } = store.getState()['features/base/config'];
action.isGuest = !enableUserRolesBasedOnToken;
@@ -103,5 +190,5 @@ function _setJWT({ getState }, next, action) {
}
}
- return next(action);
+ return _maybeSetCallOverlayVisible(store, next, action);
}
diff --git a/react/features/jwt/reducer.js b/react/features/jwt/reducer.js
index 92fa32fbfe..38c6799987 100644
--- a/react/features/jwt/reducer.js
+++ b/react/features/jwt/reducer.js
@@ -1,6 +1,6 @@
-import { equals, ReducerRegistry } from '../base/redux';
+import { equals, set, ReducerRegistry } from '../base/redux';
-import { SET_JWT } from './actionTypes';
+import { SET_CALL_OVERLAY_VISIBLE, SET_JWT } from './actionTypes';
/**
* The initial redux state of the feature jwt.
@@ -11,6 +11,14 @@ import { SET_JWT } from './actionTypes';
* }}
*/
const _INITIAL_STATE = {
+ /**
+ * The indicator which determines whether (the) {@code CallOverlay} is
+ * visible.
+ *
+ * @type {boolean|undefined}
+ */
+ callOverlayVisible: undefined,
+
/**
* The indicator which determines whether the local participant is a guest
* in the conference.
@@ -31,6 +39,9 @@ const _INITIAL_STATE = {
*/
ReducerRegistry.register('features/jwt', (state = _INITIAL_STATE, action) => {
switch (action.type) {
+ case SET_CALL_OVERLAY_VISIBLE:
+ return set(state, 'callOverlayVisible', action.callOverlayVisible);
+
case SET_JWT: {
// eslint-disable-next-line no-unused-vars
const { type, ...payload } = action;
diff --git a/react/features/overlay/components/OverlayContainer.js b/react/features/overlay/components/OverlayContainer.js
index da9ccaab3a..c12638389a 100644
--- a/react/features/overlay/components/OverlayContainer.js
+++ b/react/features/overlay/components/OverlayContainer.js
@@ -1,6 +1,8 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
+import { CallOverlay } from '../../jwt';
+
import PageReloadFilmstripOnlyOverlay from './PageReloadFilmstripOnlyOverlay';
import PageReloadOverlay from './PageReloadOverlay';
import SuspendedFilmstripOnlyOverlay from './SuspendedFilmstripOnlyOverlay';
@@ -33,6 +35,15 @@ class OverlayContainer extends Component {
*/
_browser: React.PropTypes.string,
+ /**
+ * The indicator which determines whether the {@link CallOverlay} is
+ * displayed/visible.
+ *
+ * @private
+ * @type {boolean}
+ */
+ _callOverlayVisible: React.PropTypes.bool,
+
/**
* The indicator which determines whether the status of the
* JitsiConnection object has been "established" or not.
@@ -172,6 +183,8 @@ class OverlayContainer extends Component {
props = {
browser: this.props._browser
};
+ } else if (this.props._callOverlayVisible) {
+ overlayComponent = CallOverlay;
}
return (
@@ -182,17 +195,18 @@ class OverlayContainer extends Component {
}
/**
- * Maps (parts of) the Redux state to the associated OverlayContainer's props.
+ * Maps (parts of) the redux state to the associated OverlayContainer's props.
*
- * @param {Object} state - The Redux state.
+ * @param {Object} state - The redux state.
* @returns {{
* _browser: string,
- * _connectionEstablished: bool,
- * _haveToReload: bool,
- * _isNetworkFailure: bool,
- * _isMediaPermissionPromptVisible: bool,
+ * _callOverlayVisible: boolean,
+ * _connectionEstablished: boolean,
+ * _haveToReload: boolean,
+ * _isNetworkFailure: boolean,
+ * _isMediaPermissionPromptVisible: boolean,
* _reason: string,
- * _suspendDetected: bool
+ * _suspendDetected: boolean
* }}
* @private
*/
@@ -203,18 +217,27 @@ function _mapStateToProps(state) {
/**
* The browser which is used currently.
*
- * NOTE: Used by UserMediaPermissionsOverlay only.
+ * NOTE: Used by {@link UserMediaPermissionsOverlay} only.
*
* @private
* @type {string}
*/
_browser: stateFeaturesOverlay.browser,
+ /**
+ * The indicator which determines whether the {@link CallOverlay} is
+ * displayed/visible.
+ *
+ * @private
+ * @type {boolean}
+ */
+ _callOverlayVisible: Boolean(state['features/jwt'].callOverlayVisible),
+
/**
* The indicator which determines whether the status of the
* JitsiConnection object has been "established" or not.
*
- * NOTE: Used by PageReloadOverlay only.
+ * NOTE: Used by {@link PageReloadOverlay} only.
*
* @private
* @type {boolean}
@@ -225,7 +248,7 @@ function _mapStateToProps(state) {
* The indicator which determines whether a critical error for reload
* has been received.
*
- * NOTE: Used by PageReloadOverlay only.
+ * NOTE: Used by {@link PageReloadOverlay} only.
*
* @private
* @type {boolean}
@@ -236,7 +259,7 @@ function _mapStateToProps(state) {
* The indicator which determines whether the GUM permissions prompt is
* displayed or not.
*
- * NOTE: Used by UserMediaPermissionsOverlay only.
+ * NOTE: Used by {@link UserMediaPermissionsOverlay} only.
*
* @private
* @type {boolean}
@@ -248,7 +271,7 @@ function _mapStateToProps(state) {
* The indicator which determines whether the reload was caused by
* network failure.
*
- * NOTE: Used by PageReloadOverlay only.
+ * NOTE: Used by {@link PageReloadOverlay} only.
*
* @private
* @type {boolean}
@@ -258,7 +281,7 @@ function _mapStateToProps(state) {
/**
* The reason for the error that will cause the reload.
*
- * NOTE: Used by PageReloadOverlay only.
+ * NOTE: Used by {@link PageReloadOverlay} only.
*
* @private
* @type {string}
@@ -269,7 +292,7 @@ function _mapStateToProps(state) {
* The indicator which determines whether the GUM permissions prompt is
* displayed or not.
*
- * NOTE: Used by SuspendedOverlay only.
+ * NOTE: Used by {@link SuspendedOverlay} only.
*
* @private
* @type {string}
diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js
index 8ae4aca418..cb754a37a5 100644
--- a/react/features/toolbox/actions.web.js
+++ b/react/features/toolbox/actions.web.js
@@ -178,7 +178,7 @@ export function hideToolbox(force: boolean = false): Function {
if (!force
&& (hovered
- || APP.UI.isRingOverlayVisible()
+ || state['features/jwt'].callOverlayVisible
|| SideContainerToggler.isVisible())) {
dispatch(
setToolboxTimeout(