[React] Call/ring overlay

Rewrite the non-React RingOverlay into the React Component CallOverlay
in a way which makes it easier to port to React Native.
This commit is contained in:
Lyubo Marinov
2017-06-06 00:11:39 -05:00
parent 409255f056
commit d437f3db03
15 changed files with 517 additions and 258 deletions
@@ -77,7 +77,6 @@ export function connect() {
}
})
.catch(error => {
APP.UI.hideRingOverlay();
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
+27 -9
View File
@@ -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);
});
+10
View File
@@ -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.
+22 -1
View File
@@ -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.
@@ -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 (
<Container
className = { `ringing ${className || ''}` }
id = 'ringOverlay'>
<Container className = 'ringing__content'>
{ ringing ? <Text>Calling...</Text> : null }
<Avatar
className = 'ringing__avatar'
uri = { avatarUrl } />
<Container className = 'ringing__caller-info'>
<Text>
{ name }
{ ringing ? null : ' isn\'t available' }
</Text>
</Container>
</Container>
{ this._renderAudio() }
</Container>
);
}
/**
* 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 (
<Audio
ref = { this._setAudio }
src = './sounds/ring.ogg' />
);
}
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);
+1
View File
@@ -0,0 +1 @@
export { default as CallOverlay } from './CallOverlay';
+1
View File
@@ -1,4 +1,5 @@
export * from './actions';
export * from './components';
export * from './functions';
import './middleware';
+100 -13
View File
@@ -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);
}
+13 -2
View File
@@ -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;
@@ -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}
+1 -1
View File
@@ -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(