feat(callee-info): Redesign.
This commit is contained in:
parent
485ff81443
commit
769e782c6f
|
@ -1662,6 +1662,7 @@ export default {
|
|||
const displayName = user.getDisplayName();
|
||||
|
||||
APP.store.dispatch(participantJoined({
|
||||
botType: user.getBotType(),
|
||||
conference: room,
|
||||
id,
|
||||
name: displayName,
|
||||
|
@ -1862,6 +1863,17 @@ export default {
|
|||
APP.UI.changeDisplayName(id, formattedDisplayName);
|
||||
}
|
||||
);
|
||||
room.on(
|
||||
JitsiConferenceEvents.BOT_TYPE_CHANGED,
|
||||
(id, botType) => {
|
||||
|
||||
APP.store.dispatch(participantUpdated({
|
||||
conference: room,
|
||||
id,
|
||||
botType
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
height: 100%;
|
||||
position: fixed;
|
||||
z-index: $ringingZ;
|
||||
background: linear-gradient(transparent, #000);
|
||||
opacity: 0.8;
|
||||
@include transparentBg(#283447, 0.95);
|
||||
|
||||
&.solidBG {
|
||||
background: $defaultBackground;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
@ -22,20 +20,26 @@
|
|||
top: 50%;
|
||||
margin-left: -200px;
|
||||
margin-top: -125px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #1B2638;
|
||||
}
|
||||
|
||||
&__caller-info {
|
||||
.mention {
|
||||
color: #333;
|
||||
}
|
||||
&__status{
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -632,12 +632,12 @@
|
|||
},
|
||||
"presenceStatus": {
|
||||
"invited": "Invited",
|
||||
"ringing": "Ringing",
|
||||
"calling": "Calling",
|
||||
"initializingCall": "Initializing Call",
|
||||
"ringing": "Ringing...",
|
||||
"calling": "Calling...",
|
||||
"initializingCall": "Initializing Call...",
|
||||
"connected": "Connected",
|
||||
"connecting": "Connecting",
|
||||
"connecting2": "Connecting*",
|
||||
"connecting": "Connecting...",
|
||||
"connecting2": "Connecting*...",
|
||||
"disconnected": "Disconnected",
|
||||
"busy": "Busy",
|
||||
"rejected": "Rejected",
|
||||
|
|
|
@ -113,8 +113,10 @@ function initCommands() {
|
|||
|
||||
switch (name) {
|
||||
case 'invite':
|
||||
// The store should be already available because API.init is called
|
||||
// on appWillMount action.
|
||||
APP.store.dispatch(
|
||||
invite(request.invitees))
|
||||
invite(request.invitees, true))
|
||||
.then(failedInvitees => {
|
||||
let error;
|
||||
let result;
|
||||
|
|
|
@ -238,7 +238,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
|||
}
|
||||
})
|
||||
});
|
||||
this._invitees = invitees;
|
||||
this.invite(invitees);
|
||||
this._isLargeVideoVisible = true;
|
||||
this._numberOfParticipants = 0;
|
||||
this._participants = {};
|
||||
|
@ -369,9 +369,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
|||
|
||||
switch (name) {
|
||||
case 'video-conference-joined':
|
||||
if (this._invitees) {
|
||||
this.invite(this._invitees);
|
||||
}
|
||||
this._myUserID = userID;
|
||||
this._participants[userID] = {
|
||||
avatarURL: data.avatarURL
|
||||
|
|
|
@ -492,7 +492,10 @@ UI.updateUserRole = user => {
|
|||
* @param {string} status - The new status.
|
||||
*/
|
||||
UI.updateUserStatus = (user, status) => {
|
||||
if (!status) {
|
||||
const reduxState = APP.store.getState() || {};
|
||||
const { calleeInfoVisible } = reduxState['features/invite'] || {};
|
||||
|
||||
if (!status || calleeInfoVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -428,7 +428,12 @@ export default class LargeVideoManager {
|
|||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel participantID = { id } />
|
||||
<PresenceLabel
|
||||
noContentStyles = { {
|
||||
className: 'presence-label no-presence'
|
||||
} }
|
||||
participantID = { id }
|
||||
styles = { { className: 'presence-label' } } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
presenceLabelContainer.get(0));
|
||||
|
|
|
@ -573,7 +573,12 @@ RemoteVideo.prototype.addPresenceLabel = function() {
|
|||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel participantID = { this.id } />
|
||||
<PresenceLabel
|
||||
noContentStyles = { {
|
||||
className: 'presence-label no-presence'
|
||||
} }
|
||||
participantID = { this.id }
|
||||
styles = { { className: 'presence-label' } } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
presenceLabelContainer);
|
||||
|
|
|
@ -144,6 +144,7 @@ function _addConferenceListeners(conference, dispatch) {
|
|||
conference.on(
|
||||
JitsiConferenceEvents.USER_JOINED,
|
||||
(id, user) => !user.isHidden() && dispatch(participantJoined({
|
||||
botType: user.getBotType(),
|
||||
conference,
|
||||
id,
|
||||
name: user.getDisplayName(),
|
||||
|
@ -161,6 +162,14 @@ function _addConferenceListeners(conference, dispatch) {
|
|||
JitsiConferenceEvents.USER_STATUS_CHANGED,
|
||||
(...args) => dispatch(participantPresenceChanged(...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.BOT_TYPE_CHANGED,
|
||||
(id, botType) => dispatch(participantUpdated({
|
||||
conference,
|
||||
id,
|
||||
botType
|
||||
})));
|
||||
|
||||
conference.addCommandListener(
|
||||
AVATAR_ID_COMMAND,
|
||||
(data, id) => dispatch(participantUpdated({
|
||||
|
|
|
@ -1,13 +1,3 @@
|
|||
/**
|
||||
* The type of redux action which sets the visibility of {@code CalleeInfo}.
|
||||
*
|
||||
* {
|
||||
* type: SET_CALLEE_INFO_VISIBLE,
|
||||
* calleeInfoVisible: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
|
||||
|
||||
/**
|
||||
* The type of redux action which stores a specific JSON Web Token (JWT) into
|
||||
* the redux store.
|
||||
|
|
|
@ -1,28 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { SET_CALLEE_INFO_VISIBLE, SET_JWT } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets the visibility of {@code CalleeInfo}.
|
||||
*
|
||||
* @param {boolean|undefined} [calleeInfoVisible] - If {@code CalleeInfo} is
|
||||
* to be displayed/visible, then {@code true}; otherwise, {@code false} or
|
||||
* {@code undefined}.
|
||||
* @returns {{
|
||||
* type: SET_CALLEE_INFO_VISIBLE,
|
||||
* calleeInfoVisible: (boolean|undefined)
|
||||
* }}
|
||||
*/
|
||||
export function setCalleeInfoVisible(calleeInfoVisible: ?boolean) {
|
||||
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||
getState()['features/base/jwt']
|
||||
.calleeInfoVisible === calleeInfoVisible
|
||||
|| dispatch({
|
||||
type: SET_CALLEE_INFO_VISIBLE,
|
||||
calleeInfoVisible
|
||||
});
|
||||
};
|
||||
}
|
||||
import { SET_JWT } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Stores a specific JSON Web Token (JWT) into the redux store.
|
||||
|
|
|
@ -1,347 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Audio } from '../../media';
|
||||
import { Avatar } from '../../participants';
|
||||
import { Container, Text } from '../../react';
|
||||
import UIEvents from '../../../../../service/UI/UIEvents';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
declare var $: Object;
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link CalleeInfo}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The callee's information such as avatar and display name.
|
||||
*/
|
||||
_callee: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link CalleeInfo}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The CSS class (name), if any, to add to this {@code CalleeInfo}.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
className: ?string,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether this {@code CalleeInfo}
|
||||
* 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 CalleeInfo}
|
||||
* 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
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which depicts the establishment of a
|
||||
* call with a specific remote callee.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class CalleeInfo extends Component<Props, State> {
|
||||
/**
|
||||
* The (reference to the) {@link Audio} which plays/renders the audio
|
||||
* depicting the ringing phase of the call establishment represented by this
|
||||
* {@code CalleeInfo}.
|
||||
*/
|
||||
_audio: ?Audio;
|
||||
|
||||
_onLargeVideoAvatarVisible: Function;
|
||||
|
||||
_playAudioInterval: ?IntervalID;
|
||||
|
||||
_ringingTimeout: ?TimeoutID;
|
||||
|
||||
_setAudio: Function;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code CalleeInfo} 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 CalleeInfo}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Set up the timeout to end the ringing phase of the call establishment
|
||||
// depicted by this CalleeInfo.
|
||||
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, avatar, name } = this.props._callee;
|
||||
|
||||
return (
|
||||
<Container
|
||||
{ ...this._style('ringing', className) }
|
||||
id = 'ringOverlay'>
|
||||
<Container
|
||||
{ ...this._style('ringing__content') }>
|
||||
<Text { ...this._style('ringing__text') }>
|
||||
{ ringing ? 'Calling...' : '' }
|
||||
</Text>
|
||||
<Avatar
|
||||
{ ...this._style('ringing__avatar') }
|
||||
uri = { avatarUrl || avatar } />
|
||||
<Container
|
||||
{ ...this._style('ringing__caller-info') }>
|
||||
<Text
|
||||
{ ...this._style('ringing__text') }>
|
||||
{ name }
|
||||
{ ringing ? '' : ' isn\'t available' }
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
{ this._renderAudio() }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this {@code CalleeInfo} 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 CalleeInfo}.
|
||||
*
|
||||
* @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 CalleeInfo}.
|
||||
*
|
||||
* @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 CalleeInfo}.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAudio() {
|
||||
if (this.state.renderAudio && this.state.ringing) {
|
||||
return (
|
||||
<Audio
|
||||
setRef = { 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 CalleeInfo}.
|
||||
*
|
||||
* @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 CalleeInfo}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setAudio(audio) {
|
||||
this._audio = audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert specified CSS class names into React
|
||||
* {@link Component} props {@code style} or {@code className}.
|
||||
*
|
||||
* @param {Array<string>} classNames - The CSS class names to convert
|
||||
* into React {@code Component} props {@code style} or {@code className}.
|
||||
* @returns {{
|
||||
* className: string,
|
||||
* style: Object
|
||||
* }}
|
||||
*/
|
||||
_style(...classNames: Array<?string>) {
|
||||
let className = '';
|
||||
let style;
|
||||
|
||||
for (const aClassName of classNames) {
|
||||
if (aClassName) {
|
||||
// Attemp to convert aClassName into style.
|
||||
if (styles && aClassName in styles) {
|
||||
// React Native will accept an Array as the value of the
|
||||
// style prop. However, I do not know about React.
|
||||
style = {
|
||||
...style,
|
||||
...styles[aClassName]
|
||||
};
|
||||
} else {
|
||||
// Otherwise, leave it as className.
|
||||
className += aClassName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose which of the className and/or style props has a value and,
|
||||
// consequently, must be returned.
|
||||
const props = {};
|
||||
|
||||
if (className) {
|
||||
props.className = className;
|
||||
}
|
||||
if (style) {
|
||||
props.style = style;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to {@code CalleeInfo}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _callee: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
/**
|
||||
* The callee's information such as avatar and display name.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
_callee: state['features/base/jwt'].callee
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(CalleeInfo);
|
|
@ -1,6 +1,5 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
|
|
|
@ -2,24 +2,15 @@
|
|||
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_ROOM
|
||||
} from '../conference';
|
||||
import { SET_CONFIG } from '../config';
|
||||
import { SET_LOCATION_URL } from '../connection';
|
||||
import { LIB_INIT_ERROR } from '../lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount,
|
||||
PARTICIPANT_JOINED,
|
||||
participantUpdated
|
||||
} from '../participants';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import { setCalleeInfoVisible, setJWT } from './actions';
|
||||
import { setJWT } from './actions';
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { parseJWTFromURLParams } from './functions';
|
||||
|
||||
|
@ -34,14 +25,6 @@ declare var APP: Object;
|
|||
*/
|
||||
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 _maybeSetCalleeInfoVisible(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
|
||||
|
@ -58,73 +41,6 @@ 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 CalleeInfo}.
|
||||
*
|
||||
* @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 _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const state = getState();
|
||||
const stateFeaturesBaseJWT = state['features/base/jwt'];
|
||||
let calleeInfoVisible;
|
||||
|
||||
if (stateFeaturesBaseJWT.callee) {
|
||||
const { conference, leaving, room } = state['features/base/conference'];
|
||||
|
||||
// XXX The CalleeInfo 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 CalleeInfo 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 CalleeInfo is to no longer be displayed/visible as soon
|
||||
// as another participant joins.
|
||||
calleeInfoVisible
|
||||
= getParticipantCount(state) === 1
|
||||
&& Boolean(getLocalParticipant(state));
|
||||
|
||||
// However, the CallDialog is not to be displayed/visible again
|
||||
// after all remote participants leave.
|
||||
if (calleeInfoVisible
|
||||
&& stateFeaturesBaseJWT.calleeInfoVisible === false) {
|
||||
calleeInfoVisible = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(setCalleeInfoVisible(calleeInfoVisible));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the properties {@code avatarURL}, {@code email}, and {@code name}
|
||||
* of the local participant stored in the redux state base/participants.
|
||||
|
@ -248,7 +164,7 @@ function _setJWT(store, next, action) {
|
|||
}
|
||||
}
|
||||
|
||||
return _maybeSetCalleeInfoVisible(store, next, action);
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,27 +1,18 @@
|
|||
// @flow
|
||||
|
||||
import { equals, set, ReducerRegistry } from '../redux';
|
||||
import { equals, ReducerRegistry } from '../redux';
|
||||
|
||||
import { SET_CALLEE_INFO_VISIBLE, SET_JWT } from './actionTypes';
|
||||
import { SET_JWT } from './actionTypes';
|
||||
|
||||
/**
|
||||
* The default/initial redux state of the feature jwt.
|
||||
*
|
||||
* @private
|
||||
* @type {{
|
||||
* calleeInfoVisible: ?boolean
|
||||
* isGuest: boolean
|
||||
* }}
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
/**
|
||||
* The indicator which determines whether (the) {@code CalleeInfo} is
|
||||
* visible.
|
||||
*
|
||||
* @type {boolean|undefined}
|
||||
*/
|
||||
calleeInfoVisible: undefined,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the local participant is a guest
|
||||
* in the conference.
|
||||
|
@ -44,9 +35,6 @@ ReducerRegistry.register(
|
|||
'features/base/jwt',
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SET_CALLEE_INFO_VISIBLE:
|
||||
return set(state, 'calleeInfoVisible', action.calleeInfoVisible);
|
||||
|
||||
case SET_JWT: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { type, ...payload } = action;
|
||||
|
|
|
@ -180,6 +180,7 @@ function _participant(state: Object = {}, action) {
|
|||
function _participantJoined({ participant }) {
|
||||
const {
|
||||
avatarURL,
|
||||
botType,
|
||||
connectionStatus,
|
||||
dominantSpeaker,
|
||||
email,
|
||||
|
@ -212,6 +213,7 @@ function _participantJoined({ participant }) {
|
|||
return {
|
||||
avatarID,
|
||||
avatarURL,
|
||||
botType,
|
||||
conference,
|
||||
connectionStatus,
|
||||
dominantSpeaker: dominantSpeaker || false,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { connect as reactReduxConnect } from 'react-redux';
|
|||
import { appNavigate } from '../../app';
|
||||
import { connect, disconnect } from '../../base/connection';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { CalleeInfoContainer } from '../../base/jwt';
|
||||
import { CalleeInfoContainer } from '../../invite';
|
||||
import { getParticipantCount } from '../../base/participants';
|
||||
import { Container, LoadingIndicator, TintedView } from '../../base/react';
|
||||
import { TestConnectionInfo } from '../../base/testing';
|
||||
|
|
|
@ -7,8 +7,8 @@ import { connect as reactReduxConnect } from 'react-redux';
|
|||
import { connect, disconnect } from '../../base/connection';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { CalleeInfoContainer } from '../../base/jwt';
|
||||
import { Filmstrip } from '../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../invite';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
import { NotificationsContainer } from '../../notifications';
|
||||
import { SidePanel } from '../../side-panel';
|
||||
|
|
|
@ -32,6 +32,10 @@ export function isFilmstripVisible(stateful: Object | Function) {
|
|||
* in the filmstrip, then {@code true}; otherwise, {@code false}.
|
||||
*/
|
||||
export function shouldRemoteVideosBeVisible(state: Object) {
|
||||
if (state['features/invite'].calleeInfoVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participantCount = getParticipantCount(state);
|
||||
let pinnedParticipant;
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { setLastN } from '../base/conference';
|
||||
import { SET_CALLEE_INFO_VISIBLE } from '../base/jwt';
|
||||
import { pinParticipant } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||
|
||||
import { SET_FILMSTRIP_ENABLED } from './actionTypes';
|
||||
|
||||
|
@ -12,9 +10,6 @@ declare var APP: Object;
|
|||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CALLEE_INFO_VISIBLE:
|
||||
return _setCalleeInfoVisible(store, next, action);
|
||||
|
||||
case SET_FILMSTRIP_ENABLED:
|
||||
return _setFilmstripEnabled(store, next, action);
|
||||
}
|
||||
|
@ -22,42 +17,6 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies the feature filmstrip that the action
|
||||
* {@link SET_CALLEE_INFO_VISIBLE} is being dispatched within a specific redux
|
||||
* store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is being
|
||||
* dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Action} action - The redux action {@code SET_CALLEE_INFO_VISIBLE}
|
||||
* which is being dispatched in the specified store.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setCalleeInfoVisible({ getState }, next, action) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const oldValue
|
||||
= Boolean(getState()['features/base/jwt'].calleeInfoVisible);
|
||||
const result = next(action);
|
||||
const newValue
|
||||
= Boolean(getState()['features/base/jwt'].calleeInfoVisible);
|
||||
|
||||
oldValue === newValue
|
||||
|
||||
// FIXME The following accesses the private state filmstrip of
|
||||
// Filmstrip. It is written with the understanding that Filmstrip
|
||||
// will be rewritten in React and, consequently, will not need the
|
||||
// middleware implemented here, Filmstrip.init, and UI.start.
|
||||
|| (Filmstrip.filmstrip && Filmstrip.toggleFilmstrip(!newValue));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature filmstrip that the action {@link SET_FILMSTRIP_ENABLED}
|
||||
* is being dispatched within a specific redux store.
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* The type of the (redux) action which signals that a click/tap has been
|
||||
* performed on {@link InviteButton} and that the execution flow for
|
||||
* adding/inviting people to the current conference/meeting is to begin.
|
||||
*
|
||||
* {
|
||||
* type: BEGIN_ADD_PEOPLE
|
||||
* }
|
||||
*/
|
||||
export const BEGIN_ADD_PEOPLE = Symbol('BEGIN_ADD_PEOPLE');
|
||||
|
||||
/**
|
||||
* The type of redux action to set the {@code EventEmitter} subscriptions
|
||||
* utilized by the feature invite.
|
||||
|
@ -22,6 +11,49 @@ export const BEGIN_ADD_PEOPLE = Symbol('BEGIN_ADD_PEOPLE');
|
|||
*/
|
||||
export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS');
|
||||
|
||||
/**
|
||||
* The type of redux action which will add pending invite request to the redux
|
||||
* store.
|
||||
*
|
||||
* {
|
||||
* type: ADD_PENDING_INVITE_REQUEST,
|
||||
* request: Object
|
||||
* }
|
||||
*/
|
||||
export const ADD_PENDING_INVITE_REQUEST = Symbol('ADD_PENDING_INVITE_REQUEST');
|
||||
|
||||
/**
|
||||
* The type of the (redux) action which signals that a click/tap has been
|
||||
* performed on {@link InviteButton} and that the execution flow for
|
||||
* adding/inviting people to the current conference/meeting is to begin.
|
||||
*
|
||||
* {
|
||||
* type: BEGIN_ADD_PEOPLE
|
||||
* }
|
||||
*/
|
||||
export const BEGIN_ADD_PEOPLE = Symbol('BEGIN_ADD_PEOPLE');
|
||||
|
||||
/**
|
||||
* The type of redux action which will remove pending invite requests from the
|
||||
* redux store.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_PENDING_INVITE_REQUESTS
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_PENDING_INVITE_REQUESTS
|
||||
= Symbol('REMOVE_PENDING_INVITE_REQUESTS');
|
||||
|
||||
/**
|
||||
* The type of redux action which sets the visibility of {@code CalleeInfo}.
|
||||
*
|
||||
* {
|
||||
* type: SET_CALLEE_INFO_VISIBLE,
|
||||
* calleeInfoVisible: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
|
||||
|
||||
/**
|
||||
* The type of the action which signals an error occurred while requesting dial-
|
||||
* in numbers.
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
import { getInviteURL } from '../base/connection';
|
||||
import { inviteVideoRooms } from '../videosipgw';
|
||||
import { getParticipants } from '../base/participants';
|
||||
|
||||
import {
|
||||
ADD_PENDING_INVITE_REQUEST,
|
||||
BEGIN_ADD_PEOPLE,
|
||||
REMOVE_PENDING_INVITE_REQUESTS,
|
||||
SET_CALLEE_INFO_VISIBLE,
|
||||
UPDATE_DIAL_IN_NUMBERS_FAILED,
|
||||
UPDATE_DIAL_IN_NUMBERS_SUCCESS
|
||||
} from './actionTypes';
|
||||
|
@ -31,23 +35,51 @@ export function beginAddPeople() {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invites (i.e. sends invites to) an array of invitees (which may be a
|
||||
* combination of users, rooms, phone numbers, and video rooms).
|
||||
*
|
||||
* @param {Array<Object>} invitees - The recepients to send invites to.
|
||||
* @param {Array<Object>} showCalleeInfo - Indicates whether the
|
||||
* {@code CalleeInfo} should be displayed or not.
|
||||
* @returns {Promise<Array<Object>>} A {@code Promise} resolving with an array
|
||||
* of invitees who were not invited (i.e. invites were not sent to them).
|
||||
*/
|
||||
export function invite(invitees: Array<Object>) {
|
||||
export function invite(
|
||||
invitees: Array<Object>,
|
||||
showCalleeInfo: boolean = false) {
|
||||
return (
|
||||
dispatch: Dispatch<*>,
|
||||
getState: Function): Promise<Array<Object>> => {
|
||||
const state = getState();
|
||||
const participants = getParticipants(state);
|
||||
const { calleeInfoVisible } = state['features/invite'];
|
||||
|
||||
if (showCalleeInfo
|
||||
&& !calleeInfoVisible
|
||||
&& invitees.length === 1
|
||||
&& invitees[0].type === 'user'
|
||||
&& participants.length === 1) {
|
||||
dispatch(setCalleeInfoVisible(true, invitees[0]));
|
||||
}
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (typeof conference === 'undefined') {
|
||||
// Invite will fail before CONFERENCE_JOIN. The request will be
|
||||
// cached in order to be executed on CONFERENCE_JOIN.
|
||||
return new Promise(resolve => {
|
||||
dispatch(addPendingInviteRequest({
|
||||
invitees,
|
||||
callback: failedInvitees => resolve(failedInvitees)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [ ...invitees ];
|
||||
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const {
|
||||
callFlowsEnabled,
|
||||
inviteServiceUrl,
|
||||
|
@ -57,27 +89,25 @@ export function invite(invitees: Array<Object>) {
|
|||
const { jwt } = state['features/base/jwt'];
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (conference) {
|
||||
const phoneNumbers
|
||||
= invitesLeftToSend.filter(({ type }) => type === 'phone');
|
||||
const phoneNumbers
|
||||
= invitesLeftToSend.filter(({ type }) => type === 'phone');
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(item => {
|
||||
const numberToInvite = item.number;
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(item => {
|
||||
const numberToInvite = item.number;
|
||||
|
||||
return conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(
|
||||
invitee => invitee !== item);
|
||||
})
|
||||
.catch(error =>
|
||||
logger.error('Error inviting phone number:', error));
|
||||
});
|
||||
return conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(
|
||||
invitee => invitee !== item);
|
||||
})
|
||||
.catch(error =>
|
||||
logger.error('Error inviting phone number:', error));
|
||||
});
|
||||
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
|
||||
const usersAndRooms
|
||||
= invitesLeftToSend.filter(
|
||||
|
@ -98,7 +128,10 @@ export function invite(invitees: Array<Object>) {
|
|||
= invitesLeftToSend.filter(
|
||||
({ type }) => type !== 'user' && type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error('Error inviting people:', error));
|
||||
.catch(error => {
|
||||
dispatch(setCalleeInfoVisible(false));
|
||||
logger.error('Error inviting people:', error);
|
||||
});
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
@ -163,3 +196,56 @@ export function updateDialInNumbers() {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of {@code CalleeInfo}.
|
||||
*
|
||||
* @param {boolean|undefined} [calleeInfoVisible] - If {@code CalleeInfo} is
|
||||
* to be displayed/visible, then {@code true}; otherwise, {@code false} or
|
||||
* {@code undefined}.
|
||||
* @param {Object|undefined} [initialCalleeInfo] - Callee information.
|
||||
* @returns {{
|
||||
* type: SET_CALLEE_INFO_VISIBLE,
|
||||
* calleeInfoVisible: (boolean|undefined),
|
||||
* initialCalleeInfo
|
||||
* }}
|
||||
*/
|
||||
export function setCalleeInfoVisible(
|
||||
calleeInfoVisible: boolean,
|
||||
initialCalleeInfo: ?Object) {
|
||||
return {
|
||||
type: SET_CALLEE_INFO_VISIBLE,
|
||||
calleeInfoVisible,
|
||||
initialCalleeInfo
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds pending invite request.
|
||||
*
|
||||
* @param {Object} request - The request.
|
||||
* @returns {{
|
||||
* type: ADD_PENDING_INVITE_REQUEST,
|
||||
* request: Object
|
||||
* }}
|
||||
*/
|
||||
export function addPendingInviteRequest(
|
||||
request: { invitees: Array<Object>, callback: Function }) {
|
||||
return {
|
||||
type: ADD_PENDING_INVITE_REQUEST,
|
||||
request
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all pending invite requests.
|
||||
*
|
||||
* @returns {{
|
||||
* type: REMOVE_PENDING_INVITE_REQUEST
|
||||
* }}
|
||||
*/
|
||||
export function removePendingInviteRequests() {
|
||||
return {
|
||||
type: REMOVE_PENDING_INVITE_REQUESTS
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getParticipants,
|
||||
getParticipantDisplayName,
|
||||
getParticipantPresenceStatus
|
||||
} from '../../../base/participants';
|
||||
import { Container, Text } from '../../../base/react';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks';
|
||||
import { CALLING, PresenceLabel } from '../../../presence-status';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link CalleeInfo}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The callee's information such as avatar and display name.
|
||||
*/
|
||||
_callee: Object,
|
||||
|
||||
_isVideoMuted: boolean
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which depicts the establishment of a
|
||||
* call with a specific remote callee.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class CalleeInfo extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
avatar,
|
||||
name,
|
||||
status = CALLING
|
||||
} = this.props._callee;
|
||||
const className = this.props._isVideoMuted ? 'solidBG' : undefined;
|
||||
|
||||
return (
|
||||
<Container
|
||||
{ ...this._style('ringing', className) }
|
||||
id = 'ringOverlay'>
|
||||
<Container
|
||||
{ ...this._style('ringing__content') }>
|
||||
<Avatar
|
||||
{ ...this._style('ringing__avatar') }
|
||||
uri = { avatar } />
|
||||
<Container { ...this._style('ringing__status') }>
|
||||
<PresenceLabel
|
||||
defaultPresence = { status }
|
||||
styles = { this._style('ringing__text') } />
|
||||
</Container>
|
||||
<Container { ...this._style('ringing__name') }>
|
||||
<Text
|
||||
{ ...this._style('ringing__text') }>
|
||||
{ name }
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert specified CSS class names into React
|
||||
* {@link Component} props {@code style} or {@code className}.
|
||||
*
|
||||
* @param {Array<string>} classNames - The CSS class names to convert
|
||||
* into React {@code Component} props {@code style} or {@code className}.
|
||||
* @returns {{
|
||||
* className: string,
|
||||
* style: Object
|
||||
* }}
|
||||
*/
|
||||
_style(...classNames: Array<?string>) {
|
||||
let className = '';
|
||||
let style;
|
||||
|
||||
for (const aClassName of classNames) {
|
||||
if (aClassName) {
|
||||
// Attemp to convert aClassName into style.
|
||||
if (styles && aClassName in styles) {
|
||||
// React Native will accept an Array as the value of the
|
||||
// style prop. However, I do not know about React.
|
||||
style = {
|
||||
...style,
|
||||
...styles[aClassName]
|
||||
};
|
||||
} else {
|
||||
// Otherwise, leave it as className.
|
||||
className += `${aClassName} `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose which of the className and/or style props has a value and,
|
||||
// consequently, must be returned.
|
||||
const props = {};
|
||||
|
||||
if (className) {
|
||||
props.className = className.trim();
|
||||
}
|
||||
if (style) {
|
||||
props.style = style;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to {@code CalleeInfo}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _callee: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const _isVideoMuted
|
||||
= isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
const poltergeist
|
||||
= getParticipants(state).find(p => p.botType === 'poltergeist');
|
||||
|
||||
if (poltergeist) {
|
||||
const { id } = poltergeist;
|
||||
|
||||
return {
|
||||
_callee: {
|
||||
avatar: getAvatarURL(poltergeist),
|
||||
name: getParticipantDisplayName(state, id),
|
||||
status: getParticipantPresenceStatus(state, id)
|
||||
},
|
||||
_isVideoMuted
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
_callee: state['features/invite'].initialCalleeInfo,
|
||||
_isVideoMuted
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(CalleeInfo);
|
|
@ -57,7 +57,7 @@ function _mapStateToProps(state: Object): Object {
|
|||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
_calleeInfoVisible: state['features/base/jwt'].calleeInfoVisible
|
||||
_calleeInfoVisible: state['features/invite'].calleeInfoVisible
|
||||
};
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
export { default as CalleeInfo } from './CalleeInfo';
|
||||
export { default as CalleeInfoContainer } from './CalleeInfoContainer';
|
|
@ -1,6 +1,6 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorPalette, createStyleSheet } from '../../styles';
|
||||
import { ColorPalette, createStyleSheet } from '../../../base/styles';
|
||||
|
||||
export default createStyleSheet({
|
||||
// XXX The names bellow were preserved for the purposes of compatibility
|
|
@ -2,3 +2,4 @@ export { default as AddPeopleDialog } from './AddPeopleDialog';
|
|||
export { DialInSummary } from './dial-in-summary';
|
||||
export { default as InfoDialogButton } from './InfoDialogButton';
|
||||
export { default as InviteButton } from './InviteButton';
|
||||
export * from './callee-info';
|
||||
|
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
import {
|
||||
CONFERENCE_JOINED
|
||||
} from '../base/conference';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantPresenceStatus,
|
||||
getParticipants,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_JOINED_SOUND_ID,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED
|
||||
PARTICIPANT_UPDATED,
|
||||
pinParticipant
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
|
@ -24,7 +30,15 @@ import {
|
|||
RINGING
|
||||
} from '../presence-status';
|
||||
|
||||
import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes';
|
||||
import {
|
||||
SET_CALLEE_INFO_VISIBLE,
|
||||
UPDATE_DIAL_IN_NUMBERS_FAILED
|
||||
} from './actionTypes';
|
||||
import {
|
||||
invite,
|
||||
removePendingInviteRequests,
|
||||
setCalleeInfoVisible
|
||||
} from './actions';
|
||||
import {
|
||||
OUTGOING_CALL_EXPIRED_SOUND_ID,
|
||||
OUTGOING_CALL_REJECTED_SOUND_ID,
|
||||
|
@ -59,13 +73,22 @@ const statusToRingtone = {
|
|||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
let oldParticipantPresence;
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
if (action.type === PARTICIPANT_UPDATED
|
||||
|| action.type === PARTICIPANT_LEFT) {
|
||||
oldParticipantPresence
|
||||
= getParticipantPresenceStatus(
|
||||
store.getState(),
|
||||
action.participant.id);
|
||||
= getParticipantPresenceStatus(state, action.participant.id);
|
||||
}
|
||||
|
||||
if (action.type === SET_CALLEE_INFO_VISIBLE) {
|
||||
if (action.calleeInfoVisible) {
|
||||
dispatch(pinParticipant(getLocalParticipant(state).id));
|
||||
} else {
|
||||
// unpin participant
|
||||
dispatch(pinParticipant());
|
||||
}
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
@ -73,23 +96,27 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
for (const [ soundId, sound ] of sounds.entries()) {
|
||||
store.dispatch(registerSound(soundId, sound.file, sound.options));
|
||||
dispatch(registerSound(soundId, sound.file, sound.options));
|
||||
}
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
for (const soundId of sounds.keys()) {
|
||||
store.dispatch(unregisterSound(soundId));
|
||||
dispatch(unregisterSound(soundId));
|
||||
}
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
_onConferenceJoined(store);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_LEFT:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
_maybeHideCalleeInfo(action, store);
|
||||
|
||||
const newParticipantPresence
|
||||
= getParticipantPresenceStatus(
|
||||
store.getState(),
|
||||
action.participant.id);
|
||||
= getParticipantPresenceStatus(state, action.participant.id);
|
||||
|
||||
if (oldParticipantPresence === newParticipantPresence) {
|
||||
break;
|
||||
|
@ -108,11 +135,11 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
}
|
||||
|
||||
if (oldSoundId) {
|
||||
store.dispatch(stopSound(oldSoundId));
|
||||
dispatch(stopSound(oldSoundId));
|
||||
}
|
||||
|
||||
if (newSoundId) {
|
||||
store.dispatch(playSound(newSoundId));
|
||||
dispatch(playSound(newSoundId));
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -126,3 +153,50 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Hides the callee info layot if there are more than 1 real
|
||||
* (not poltergeist, shared video, etc.) participants in the call.
|
||||
*
|
||||
* @param {Object} action - The redux action.
|
||||
* @param {ReduxStore} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeHideCalleeInfo(action, store) {
|
||||
const state = store.getState();
|
||||
|
||||
if (!state['features/invite'].calleeInfoVisible) {
|
||||
return;
|
||||
}
|
||||
const participants = getParticipants(state);
|
||||
const numberOfPoltergeists
|
||||
= participants.filter(p => p.botType === 'poltergeist').length;
|
||||
const numberOfRealParticipants = participants.length - numberOfPoltergeists;
|
||||
|
||||
if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
|
||||
|| (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
|
||||
store.dispatch(setCalleeInfoVisible(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the pending invitation requests if any.
|
||||
*
|
||||
* @param {ReduxStore} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConferenceJoined(store) {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
const pendingInviteRequests
|
||||
= getState()['features/invite'].pendingInviteRequests || [];
|
||||
|
||||
pendingInviteRequests.forEach(({ invitees, callback }) => {
|
||||
dispatch(invite(invitees))
|
||||
.then(failedInvitees => {
|
||||
callback(failedInvitees);
|
||||
});
|
||||
});
|
||||
|
||||
dispatch(removePendingInviteRequests());
|
||||
}
|
||||
|
|
|
@ -4,12 +4,24 @@ import { assign, ReducerRegistry } from '../base/redux';
|
|||
|
||||
import {
|
||||
_SET_EMITTER_SUBSCRIPTIONS,
|
||||
ADD_PENDING_INVITE_REQUEST,
|
||||
REMOVE_PENDING_INVITE_REQUESTS,
|
||||
SET_CALLEE_INFO_VISIBLE,
|
||||
UPDATE_DIAL_IN_NUMBERS_FAILED,
|
||||
UPDATE_DIAL_IN_NUMBERS_SUCCESS
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
numbersEnabled: true
|
||||
/**
|
||||
* The indicator which determines whether (the) {@code CalleeInfo} is
|
||||
* visible.
|
||||
*
|
||||
* @type {boolean|undefined}
|
||||
*/
|
||||
calleeInfoVisible: false,
|
||||
|
||||
numbersEnabled: true,
|
||||
pendingInviteRequests: []
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
|
||||
|
@ -17,6 +29,26 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
|
|||
case _SET_EMITTER_SUBSCRIPTIONS:
|
||||
return (
|
||||
assign(state, 'emitterSubscriptions', action.emitterSubscriptions));
|
||||
case ADD_PENDING_INVITE_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
pendingInviteRequests: [
|
||||
...state.pendingInviteRequests,
|
||||
action.request
|
||||
]
|
||||
};
|
||||
case REMOVE_PENDING_INVITE_REQUESTS:
|
||||
return {
|
||||
...state,
|
||||
pendingInviteRequests: []
|
||||
};
|
||||
|
||||
case SET_CALLEE_INFO_VISIBLE:
|
||||
return {
|
||||
...state,
|
||||
calleeInfoVisible: action.calleeInfoVisible,
|
||||
initialCalleeInfo: action.initialCalleeInfo
|
||||
};
|
||||
|
||||
case UPDATE_DIAL_IN_NUMBERS_FAILED:
|
||||
return {
|
||||
|
|
|
@ -139,8 +139,10 @@ export default class AbstractNotificationsContainer<P: Props>
|
|||
export function _abstractMapStateToProps(state: Object) {
|
||||
const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
|
||||
const { enabled, notifications } = state['features/notifications'];
|
||||
const { calleeInfoVisible } = state['features/invite'];
|
||||
|
||||
return {
|
||||
_notifications: enabled && !isAnyOverlayVisible ? notifications : []
|
||||
_notifications: enabled && !isAnyOverlayVisible && !calleeInfoVisible
|
||||
? notifications : []
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { getParticipantById } from '../../base/participants';
|
||||
import { Text } from '../../base/react';
|
||||
|
||||
import { STATUS_TO_I18N_KEY } from '../constants';
|
||||
|
||||
|
@ -35,11 +36,27 @@ class PresenceLabel extends Component {
|
|||
*/
|
||||
_presence: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Default presence status that will be displayed if user's presence
|
||||
* status is not available.
|
||||
*/
|
||||
defaultPresence: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Styles for the case where there isn't any content to be shown.
|
||||
*/
|
||||
noContentStyles: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The ID of the participant whose presence status shoul display.
|
||||
*/
|
||||
participantID: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Styles for the presence label.
|
||||
*/
|
||||
styles: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
@ -53,14 +70,13 @@ class PresenceLabel extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _presence } = this.props;
|
||||
const { _presence, styles, noContentStyles } = this.props;
|
||||
const combinedStyles = _presence ? styles : noContentStyles;
|
||||
|
||||
return (
|
||||
<div
|
||||
className
|
||||
= { `presence-label ${_presence ? '' : 'no-presence'}` }>
|
||||
<Text { ...combinedStyles }>
|
||||
{ this._getPresenceText() }
|
||||
</div>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -102,7 +118,8 @@ function _mapStateToProps(state, ownProps) {
|
|||
const participant = getParticipantById(state, ownProps.participantID);
|
||||
|
||||
return {
|
||||
_presence: participant && participant.presence
|
||||
_presence:
|
||||
(participant && participant.presence) || ownProps.defaultPresence
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -122,5 +122,6 @@ export const STATUS_TO_I18N_KEY = {
|
|||
[CONNECTING]: 'presenceStatus.connecting',
|
||||
[CONNECTING2]: 'presenceStatus.connecting2',
|
||||
[CONNECTED_PHONE_NUMBER]: 'presenceStatus.connected',
|
||||
[CONNECTED_USER]: 'presenceStatus.connected',
|
||||
[DISCONNECTED]: 'presenceStatus.disconnected'
|
||||
};
|
||||
|
|
|
@ -89,7 +89,7 @@ export function hideToolbox(force: boolean = false): Function {
|
|||
|
||||
if (!force
|
||||
&& (hovered
|
||||
|| state['features/base/jwt'].calleeInfoVisible
|
||||
|| state['features/invite'].calleeInfoVisible
|
||||
|| SideContainerToggler.isVisible())) {
|
||||
dispatch(
|
||||
setToolboxTimeout(
|
||||
|
|
Loading…
Reference in New Issue