[RN] Support XMPP authentication

This commit is contained in:
Lyubo Marinov 2017-09-18 02:09:43 -05:00
parent 141acea194
commit 241dc3b147
16 changed files with 494 additions and 566 deletions

View File

@ -264,7 +264,7 @@
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?", "removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
"alreadySharedVideoMsg": "Another member is already sharing video. This conference allows only one shared video at a time.", "alreadySharedVideoMsg": "Another member is already sharing video. This conference allows only one shared video at a time.",
"WaitingForHost": "Waiting for the host ...", "WaitingForHost": "Waiting for the host ...",
"WaitForHostMsg": "The conference '__room__' has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "WaitForHostMsg": "The conference <b>__room__ </b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"IamHost": "I am the host", "IamHost": "I am the host",
"Cancel": "Cancel", "Cancel": "Cancel",
"Submit": "Submit", "Submit": "Submit",

View File

@ -140,37 +140,40 @@ function initJWTTokenListener(room) {
* @param {string} [lockPassword] password to use if the conference is locked * @param {string} [lockPassword] password to use if the conference is locked
*/ */
function doXmppAuth(room, lockPassword) { function doXmppAuth(room, lockPassword) {
const loginDialog = LoginDialog.showAuthDialog(function (id, password) { const loginDialog = LoginDialog.showAuthDialog(
const authConnection = room.createAuthenticationConnection(); /* successCallback */ (id, password) => {
room.authenticateAndUpgradeRole({
authConnection.authenticateAndUpgradeRole({
id, id,
password, password,
roomPassword: lockPassword, roomPassword: lockPassword,
onLoginSuccessful: () => { /* Called when XMPP login succeeds */
/** Called when the XMPP login succeeds. */
onLoginSuccessful() {
loginDialog.displayConnectionStatus( loginDialog.displayConnectionStatus(
'connection.FETCH_SESSION_ID'); 'connection.FETCH_SESSION_ID');
} }
}) })
.then(() => { .then(
/* onFulfilled */ () => {
loginDialog.displayConnectionStatus( loginDialog.displayConnectionStatus(
'connection.GOT_SESSION_ID'); 'connection.GOT_SESSION_ID');
loginDialog.close(); loginDialog.close();
}) },
.catch(error => { /* onRejected */ error => {
logger.error('authenticateAndUpgradeRole failed', error); logger.error('authenticateAndUpgradeRole failed', error);
if (error.authenticationError) {
const { authenticationError, connectionError } = error;
if (authenticationError) {
loginDialog.displayError( loginDialog.displayError(
'connection.GET_SESSION_ID_ERROR', { 'connection.GET_SESSION_ID_ERROR',
msg: error.authenticationError { msg: authenticationError });
}); } else if (connectionError) {
} else { loginDialog.displayError(connectionError);
loginDialog.displayError(error.connectionError);
} }
}); });
}, function () { },
loginDialog.close(); /* cancelCallback */ () => loginDialog.close());
});
} }
/** /**

View File

@ -1,4 +1,5 @@
/* global $, APP, config, JitsiMeetJS */ /* global $, APP, config, JitsiMeetJS */
import { toJid } from '../../../react/features/base/connection'; import { toJid } from '../../../react/features/base/connection';
const ConnectionErrors = JitsiMeetJS.errors.connection; const ConnectionErrors = JitsiMeetJS.errors.connection;
@ -196,34 +197,38 @@ export default {
}, },
/** /**
* Show notification that authentication is required * Shows a notification that authentication is required to create the
* to create the conference, so he should authenticate or wait for a host. * conference, so the local participant should authenticate or wait for a
* @param {string} roomName name of the conference * host.
* @param {function} onAuthNow callback to invoke if *
* user want to authenticate. * @param {string} room - The name of the conference.
* @param {function} onAuthNow - The callback to invoke if the local
* participant wants to authenticate.
* @returns dialog * @returns dialog
*/ */
showAuthRequiredDialog: function (roomName, onAuthNow) { showAuthRequiredDialog(room, onAuthNow) {
var msg = APP.translation.generateTranslationHTML( const msg = APP.translation.generateTranslationHTML(
"dialog.WaitForHostMsg", {room: roomName} '[html]dialog.WaitForHostMsg',
{ room }
); );
const buttonTxt = APP.translation.generateTranslationHTML(
var buttonTxt = APP.translation.generateTranslationHTML( 'dialog.IamHost'
"dialog.IamHost"
); );
var buttons = [{title: buttonTxt, value: "authNow"}]; const buttons = [{
title: buttonTxt,
value: 'authNow'
}];
return APP.UI.messageHandler.openDialog( return APP.UI.messageHandler.openDialog(
"dialog.WaitingForHost", 'dialog.WaitingForHost',
msg, msg,
true, true,
buttons, buttons,
function (e, submitValue) { (e, submitValue) => {
// Do not close the dialog yet.
// Do not close the dialog yet
e.preventDefault(); e.preventDefault();
// Open login popup // Open login popup.
if (submitValue === 'authNow') { if (submitValue === 'authNow') {
onAuthNow(); onAuthNow();
} }

View File

@ -4,6 +4,7 @@ import React from 'react';
import { Linking } from 'react-native'; import { Linking } from 'react-native';
import '../../analytics'; import '../../analytics';
import '../../authentication';
import { Platform } from '../../base/react'; import { Platform } from '../../base/react';
import '../../mobile/audio-mode'; import '../../mobile/audio-mode';
import '../../mobile/background'; import '../../mobile/background';
@ -15,7 +16,6 @@ import '../../mobile/proximity';
import '../../mobile/wake-lock'; import '../../mobile/wake-lock';
import { AbstractApp } from './AbstractApp'; import { AbstractApp } from './AbstractApp';
import '../../authentication';
/** /**
* Root application component. * Root application component.

View File

@ -28,38 +28,31 @@ export const CANCEL_WAIT_FOR_OWNER = Symbol('CANCEL_WAIT_FOR_OWNER');
*/ */
export const STOP_WAIT_FOR_OWNER = Symbol('STOP_WAIT_FOR_OWNER'); export const STOP_WAIT_FOR_OWNER = Symbol('STOP_WAIT_FOR_OWNER');
/**
* The type of (redux) action which signals that the process of authenticating
* and upgrading the current conference user's role has been started.
*
* {
* type: UPGRADE_ROLE_STARTED,
* authConnection: JitsiAuthConnection
* }
*/
export const UPGRADE_ROLE_STARTED = Symbol('UPGRADE_ROLE_STARTED');
/** /**
* The type of (redux) action which informs that the authentication and role * The type of (redux) action which informs that the authentication and role
* upgrade process has been completed successfully. * upgrade process has finished either with success or with a specific error.
* If <tt>error</tt> is <tt>undefined</tt>, then the process succeeded;
* otherwise, it failed. Refer to
* {@link JitsiConference#authenticateAndUpgradeRole} in lib-jitsi-meet for the
* error details.
* *
* { * {
* type: UPGRADE_ROLE_SUCCESS * type: UPGRADE_ROLE_FINISHED,
* }
*/
export const UPGRADE_ROLE_SUCCESS = Symbol('UPGRADE_ROLE_SUCCESS');
/**
* The type of (redux) action which informs that the authentication and role
* upgrade process has failed with an error. Check the docs of
* {@link JitsiAuthConnection} for more details about the error structure.
*
* {
* type: UPGRADE_ROLE_SUCCESS,
* error: Object * error: Object
* } * }
*/ */
export const UPGRADE_ROLE_FAILED = Symbol('UPGRADE_ROLE_FAILED'); export const UPGRADE_ROLE_FINISHED = Symbol('UPGRADE_ROLE_FINISHED');
/**
* The type of (redux) action which signals that the process of authenticating
* and upgrading the local participant's role has been started.
*
* {
* type: UPGRADE_ROLE_STARTED,
* thenableWithCancel: Object
* }
*/
export const UPGRADE_ROLE_STARTED = Symbol('UPGRADE_ROLE_STARTED');
/** /**
* The type of (redux) action that sets delayed handler which will check if * The type of (redux) action that sets delayed handler which will check if

View File

@ -1,52 +1,56 @@
import { openDialog } from '../base/dialog/actions'; /* @flow */
import { checkIfCanJoin } from '../base/conference/actions';
import { checkIfCanJoin } from '../base/conference';
import { openDialog } from '../base/dialog';
import { import {
CANCEL_LOGIN, CANCEL_LOGIN,
CANCEL_WAIT_FOR_OWNER, CANCEL_WAIT_FOR_OWNER,
STOP_WAIT_FOR_OWNER, STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FAILED, UPGRADE_ROLE_FINISHED,
UPGRADE_ROLE_STARTED, UPGRADE_ROLE_STARTED,
UPGRADE_ROLE_SUCCESS,
WAIT_FOR_OWNER WAIT_FOR_OWNER
} from './actionTypes'; } from './actionTypes';
import { LoginDialog, WaitForOwnerDialog } from './components'; import { LoginDialog, WaitForOwnerDialog } from './components';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/** /**
* Instantiates new {@link JitsiAuthConnection} and uses it to authenticate and * Initiates authenticating and upgrading the role of the local participant to
* upgrade role of the current conference user to moderator which will allow to * moderator which will allow to create and join a new conference on an XMPP
* create and join new conference on XMPP password + guest access configuration. * password + guest access configuration. Refer to {@link LoginDialog} for more
* See {@link LoginDialog} description for more info. * info.
* *
* @param {string} id - XMPP user's id eg. user@domain.com. * @param {string} id - The XMPP user's ID (e.g. user@domain.com).
* @param {string} userPassword - The user's password. * @param {string} password - The XMPP user's password.
* @param {JitsiConference} conference - The conference for which user's role * @param {JitsiConference} conference - The conference for which the local
* will be upgraded. * participant's role will be upgraded.
* @returns {function({dispatch: Function, getState: Function})} * @returns {function({ dispatch: Dispatch, getState: Function })}
*/ */
export function authenticateAndUpgradeRole(id, userPassword, conference) { export function authenticateAndUpgradeRole(
return (dispatch, getState) => { id: string,
const authConnection = conference.createAuthenticationConnection(); password: string,
conference: Object) {
dispatch(_upgradeRoleStarted(authConnection)); return (dispatch: Dispatch, getState: Function) => {
const { password: roomPassword } const { password: roomPassword }
= getState()['features/base/conference']; = getState()['features/base/conference'];
const process
authConnection.authenticateAndUpgradeRole({ = conference.authenticateAndUpgradeRole({
id, id,
password: userPassword, password,
roomPassword roomPassword
}) });
.then(() => {
dispatch(_upgradeRoleSuccess()); dispatch(_upgradeRoleStarted(process));
}) process.then(
.catch(error => { /* onFulfilled */ () => dispatch(_upgradeRoleFinished()),
// Lack of error means the operation was canceled, so no need to log /* onRejected */ error => {
// that on error level. // The lack of an error signals a cancellation.
if (error.error) { if (error.authenticationError || error.connectionError) {
console.error('upgradeRoleFailed', error); logger.error('authenticateAndUpgradeRole failed', error);
} }
dispatch(_upgradeRoleFailed(error));
dispatch(_upgradeRoleFinished(error));
}); });
}; };
} }
@ -78,118 +82,77 @@ export function cancelWaitForOwner() {
} }
/** /**
* Stops waiting for conference owner and clears any pending timeout. * Opens {@link LoginDialog} which will ask to enter username and password
* for the current conference.
*
* @protected
* @returns {Action}
*/
export function _openLoginDialog() {
return openDialog(LoginDialog);
}
/**
* Opens {@link WaitForOnwerDialog}.
*
* @protected
* @returns {Action}
*/
export function _openWaitForOwnerDialog() {
return openDialog(WaitForOwnerDialog);
}
/**
* Stops waiting for the conference owner.
* *
* @returns {{ * @returns {{
* type: STOP_WAIT_FOR_OWNER * type: STOP_WAIT_FOR_OWNER
* }} * }}
*/ */
export function clearWaitForOwnerTimeout() { export function stopWaitForOwner() {
return { return {
type: STOP_WAIT_FOR_OWNER type: STOP_WAIT_FOR_OWNER
}; };
} }
/** /**
* Sets a delayed "wait for owner" handler function. * Signals that the process of authenticating and upgrading the local
* * participant's role has finished either with success or with a specific error.
* @param {Function} handler - The "wait for owner" handler function.
* @param {number} waitMs - The delay in milliseconds.
* *
* @param {Object} error - If <tt>undefined</tt>, then the process of
* authenticating and upgrading the local participant's role has succeeded;
* otherwise, it has failed with the specified error. Refer to
* {@link JitsiConference#authenticateAndUpgradeRole} in lib-jitsi-meet for the
* error details.
* @private * @private
* @returns {{ * @returns {{
* type: WAIT_FOR_OWNER, * type: UPGRADE_ROLE_FINISHED,
* handler: Function,
* timeoutMs: number
* }}
*/
function _setWaitForOwnerTimeout(handler, waitMs) {
return {
type: WAIT_FOR_OWNER,
handler,
timeoutMs: waitMs
};
}
/**
* Displays {@link LoginDialog} which will ask to enter username and password
* for the current conference.
*
* @protected
* @returns {{
* type: OPEN_DIALOG,
* component: LoginDialog,
* props: React.PropTypes
* }}
*/
export function _showLoginDialog() {
return openDialog(LoginDialog, { });
}
/**
* Displays {@link WaitForOnwerDialog}.
*
* @protected
* @returns {{
* type: OPEN_DIALOG,
* component: WaitForOwnerDialog,
* props: React.PropTypes
* }}
*/
export function _showWaitForOwnerDialog() {
return openDialog(WaitForOwnerDialog, { });
}
/**
* Emits an error which occurred during {@link authenticateAndUpgradeRole}.
*
* @param {Object} error - Check the docs of {@link JitsiAuthConnection} in
* lib-jitsi-meet for more details about the error's structure.
*
* @private
* @returns {{
* type: UPGRADE_ROLE_FAILED,
* error: Object * error: Object
* }} * }}
*/ */
function _upgradeRoleFailed(error) { function _upgradeRoleFinished(error) {
return { return {
type: UPGRADE_ROLE_FAILED, type: UPGRADE_ROLE_FINISHED,
error error
}; };
} }
/** /**
* Signals that the role upgrade process has been started using given * Signals that a process of authenticating and upgrading the local
* {@link JitsiAuthConnection} instance. * participant's role has started.
*
* @param {JitsiAuthConnection} authenticationConnection - The authentication
* connection instance that can be used to cancel the process.
* *
* @param {Object} thenableWithCancel - The process of authenticating and
* upgrading the local participant's role.
* @private * @private
* @returns {{ * @returns {{
* type: UPGRADE_ROLE_STARTED, * type: UPGRADE_ROLE_STARTED,
* authConnection: JitsiAuthConnection * thenableWithCancel: Object
* }} * }}
*/ */
function _upgradeRoleStarted(authenticationConnection) { function _upgradeRoleStarted(thenableWithCancel) {
return { return {
type: UPGRADE_ROLE_STARTED, type: UPGRADE_ROLE_STARTED,
authConnection: authenticationConnection thenableWithCancel
};
}
/**
* Signals that the role upgrade process has been completed successfully.
*
* @private
* @returns {{
* type: UPGRADE_ROLE_SUCCESS
* }}
*/
function _upgradeRoleSuccess() {
return {
type: UPGRADE_ROLE_SUCCESS
}; };
} }
@ -198,13 +161,13 @@ function _upgradeRoleSuccess() {
* start the process of "waiting for the owner" by periodically trying to join * start the process of "waiting for the owner" by periodically trying to join
* the room every five seconds. * the room every five seconds.
* *
* @returns {function({ dispatch: Function})} * @returns {function({ dispatch: Dispatch })}
*/ */
export function waitForOwner() { export function waitForOwner() {
return dispatch => { return (dispatch: Dispatch) =>
dispatch( dispatch({
_setWaitForOwnerTimeout( type: WAIT_FOR_OWNER,
() => dispatch(checkIfCanJoin()), handler: () => dispatch(checkIfCanJoin()),
5000)); timeoutMs: 5000
}; });
} }

View File

@ -1,22 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Text, TextInput, View } from 'react-native';
import { connect as reduxConnect } from 'react-redux'; import { connect as reduxConnect } from 'react-redux';
import {
Button, import { connect, toJid } from '../../base/connection';
Modal, import { Dialog } from '../../base/dialog';
Text,
TextInput,
View
} from 'react-native';
import {
authenticateAndUpgradeRole,
cancelLogin
} from '../actions';
import {
connect,
toJid
} from '../../base/connection';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet'; import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
import styles from './styles'; import styles from './styles';
/** /**
@ -36,15 +28,15 @@ import styles from './styles';
* yet, Jicofo will not allow to start new conference. This will trigger * yet, Jicofo will not allow to start new conference. This will trigger
* 'CONFERENCE_FAILED' action with JitsiConferenceErrors.AUTHENTICATION_REQUIRED * 'CONFERENCE_FAILED' action with JitsiConferenceErrors.AUTHENTICATION_REQUIRED
* error and 'authRequired' value of 'features/base/conference' will hold * error and 'authRequired' value of 'features/base/conference' will hold
* the {@link JitsiConference} instance. If user decides to authenticate a new * the {@link JitsiConference} instance. If user decides to authenticate, a
* {@link JitsiAuthConnection} will be created from which separate XMPP * new/separate XMPP connection is established and authentication is performed.
* connection is established and authentication is performed. In case it * In case it succeeds, Jicofo will assign new session ID which then can be used
* succeeds Jicofo will assign new session ID which then can be used from * from the anonymous domain connection to create and join the room. This part
* the anonymous domain connection to create and join the room. This part is * is done by {@link JitsiConference#authenticateAndUpgradeRole} in
* done by {@link JitsiAuthConnection} from lib-jitsi-meet. * lib-jitsi-meet.
* *
* See https://github.com/jitsi/jicofo#secure-domain for configuration * See {@link https://github.com/jitsi/jicofo#secure-domain} for a description
* parameters description. * of the configuration parameters.
*/ */
class LoginDialog extends Component { class LoginDialog extends Component {
/** /**
@ -57,37 +49,37 @@ class LoginDialog extends Component {
* {@link JitsiConference} that needs authentication - will hold a valid * {@link JitsiConference} that needs authentication - will hold a valid
* value in XMPP login + guest access mode. * value in XMPP login + guest access mode.
*/ */
conference: React.PropTypes.object, _conference: PropTypes.object,
/** /**
* *
*/ */
configHosts: React.PropTypes.object, _configHosts: PropTypes.object,
/** /**
* Indicates if the dialog should display "connecting" status message. * Indicates if the dialog should display "connecting" status message.
*/ */
connecting: React.PropTypes.bool, _connecting: PropTypes.bool,
/**
* Redux store dispatch method.
*/
dispatch: React.PropTypes.func,
/** /**
* The error which occurred during login/authentication. * The error which occurred during login/authentication.
*/ */
error: React.PropTypes.string, _error: PropTypes.string,
/** /**
* Any extra details about the error provided by lib-jitsi-meet. * Any extra details about the error provided by lib-jitsi-meet.
*/ */
errorDetails: React.PropTypes.string, _errorDetails: PropTypes.string,
/**
* Redux store dispatch method.
*/
dispatch: PropTypes.func,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
t: React.PropTypes.func t: PropTypes.func
}; };
/** /**
@ -99,16 +91,16 @@ class LoginDialog extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
// Bind event handlers so they are only bound once for every instance.
this._onCancel = this._onCancel.bind(this);
this._onLogin = this._onLogin.bind(this);
this._onUsernameChange = this._onUsernameChange.bind(this);
this._onPasswordChange = this._onPasswordChange.bind(this);
this.state = { this.state = {
username: '', username: '',
password: '' password: ''
}; };
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onLogin = this._onLogin.bind(this);
this._onPasswordChange = this._onPasswordChange.bind(this);
this._onUsernameChange = this._onUsernameChange.bind(this);
} }
/** /**
@ -119,9 +111,9 @@ class LoginDialog extends Component {
*/ */
render() { render() {
const { const {
error, _connecting: connecting,
errorDetails, _error: error,
connecting, _errorDetails: errorDetails,
t t
} = this.props; } = this.props;
@ -132,43 +124,38 @@ class LoginDialog extends Component {
messageKey = 'dialog.incorrectPassword'; messageKey = 'dialog.incorrectPassword';
} else if (error) { } else if (error) {
messageKey = 'dialog.connectErrorWithMsg'; messageKey = 'dialog.connectErrorWithMsg';
messageOptions.msg = `${error} ${errorDetails}`; messageOptions.msg = `${error} ${errorDetails}`;
} }
return ( return (
<Modal <Dialog
onRequestClose = { this._onCancel } okDisabled = { connecting }
style = { styles.outerArea } onCancel = { this._onCancel }
transparent = { true } > onSubmit = { this._onLogin }
<View style = { styles.dialogBox }> titleKey = 'dialog.passwordRequired'>
<Text>Username:</Text> <View style = { styles.loginDialog }>
<TextInput <TextInput
onChangeText = { this._onUsernameChange } onChangeText = { this._onUsernameChange }
placeholder = { 'user@domain.com' } placeholder = { 'user@domain.com' }
style = { styles.textInput } style = { styles.loginDialogTextInput }
value = { this.state.username } /> value = { this.state.username } />
<Text>Password:</Text>
<TextInput <TextInput
onChangeText = { this._onPasswordChange } onChangeText = { this._onPasswordChange }
placeholder = { t('dialog.userPassword') } placeholder = { t('dialog.userPassword') }
secureTextEntry = { true } secureTextEntry = { true }
style = { styles.textInput } style = { styles.loginDialogTextInput }
value = { this.state.password } /> value = { this.state.password } />
<Text> <Text style = { styles.loginDialogText }>
{error ? t(messageKey, messageOptions) : ''} {
{connecting && !error error
? t('connection.CONNECTING') : ''} ? t(messageKey, messageOptions)
: connecting
? t('connection.CONNECTING')
: ''
}
</Text> </Text>
<Button
disabled = { connecting }
onPress = { this._onLogin }
title = { t('dialog.Ok') } />
<Button
onPress = { this._onCancel }
title = { t('dialog.Cancel') } />
</View> </View>
</Modal> </Dialog>
); );
} }
@ -216,9 +203,9 @@ class LoginDialog extends Component {
* @returns {void} * @returns {void}
*/ */
_onLogin() { _onLogin() {
const conference = this.props.conference; const { _conference: conference } = this.props;
const { username, password } = this.state; const { username, password } = this.state;
const jid = toJid(username, this.props.configHosts); const jid = toJid(username, this.props._configHosts);
// If there's a conference it means that the connection has succeeded, // If there's a conference it means that the connection has succeeded,
// but authentication is required in order to join the room. // but authentication is required in order to join the room.
@ -238,42 +225,45 @@ class LoginDialog extends Component {
* @param {Object} state - The Redux state. * @param {Object} state - The Redux state.
* @private * @private
* @returns {{ * @returns {{
* configHosts: Object, * _conference: JitsiConference,
* connecting: boolean, * _configHosts: Object,
* error: string, * _connecting: boolean,
* errorDetails: string, * _error: string,
* conference: JitsiConference * _errorDetails: string
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const {
upgradeRoleError,
upgradeRoleInProgress
} = state['features/authentication'];
const { authRequired } = state['features/base/conference'];
const { hosts: configHosts } = state['features/base/config']; const { hosts: configHosts } = state['features/base/config'];
const { const {
connecting, connecting,
error: connectionError, error: connectionError,
errorMessage: connectionErrorMessage errorMessage: connectionErrorMessage
} = state['features/base/connection']; } = state['features/base/connection'];
const {
authRequired
} = state['features/base/conference'];
const {
upgradeRoleError,
upgradeRoleInProgress
} = state['features/authentication'];
const error let error;
= connectionError let errorDetails;
|| (upgradeRoleError
&& (upgradeRoleError.connectionError if (connectionError) {
|| upgradeRoleError.authenticationError)); error = connectionError;
errorDetails = connectionErrorMessage;
} else if (upgradeRoleError) {
error
= upgradeRoleError.connectionError
|| upgradeRoleError.authenticationError;
errorDetails = upgradeRoleError.message;
}
return { return {
configHosts, _conference: authRequired,
connecting: Boolean(connecting) || Boolean(upgradeRoleInProgress), _configHosts: configHosts,
error, _connecting: Boolean(connecting) || Boolean(upgradeRoleInProgress),
errorDetails: _error: error,
(connectionError && connectionErrorMessage) _errorDetails: errorDetails
|| (upgradeRoleError && upgradeRoleError.message),
conference: authRequired
}; };
} }

View File

@ -1,9 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Button, Modal, Text, View } from 'react-native';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { _showLoginDialog, cancelWaitForOwner } from '../actions';
import { cancelWaitForOwner, _openLoginDialog } from '../actions';
import styles from './styles'; import styles from './styles';
/** /**
@ -19,20 +22,20 @@ class WaitForOwnerDialog extends Component {
* @static * @static
*/ */
static propTypes = { static propTypes = {
/**
* Redux store dispatch function.
*/
dispatch: React.PropTypes.func,
/** /**
* The name of the conference room (without the domain part). * The name of the conference room (without the domain part).
*/ */
roomName: React.PropTypes.string, _room: PropTypes.string,
/**
* Redux store dispatch function.
*/
dispatch: PropTypes.func,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
t: React.PropTypes.func t: PropTypes.func
}; };
/** /**
@ -44,8 +47,9 @@ class WaitForOwnerDialog extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._onLogin = this._onLogin.bind(this); // Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this); this._onCancel = this._onCancel.bind(this);
this._onLogin = this._onLogin.bind(this);
} }
/** /**
@ -56,43 +60,25 @@ class WaitForOwnerDialog extends Component {
*/ */
render() { render() {
const { const {
roomName, _room: room,
t t
} = this.props; } = this.props;
return ( return (
<Modal <Dialog
onRequestClose = { this._onCancel } okTitleKey = { 'dialog.IamHost' }
style = { styles.outerArea } onCancel = { this._onCancel }
transparent = { true } > onSubmit = { this._onLogin }
<View style = { styles.dialogBox } > titleKey = 'dialog.WaitingForHost'>
<Text> <Text style = { styles.waitForOwnerDialog }>
{ t( {
'dialog.WaitForHostMsg', this.renderHTML(t('dialog.WaitForHostMsg', { room }))
{ room: roomName })
} }
</Text> </Text>
<Button </Dialog>
onPress = { this._onLogin }
title = { t('dialog.IamHost') } />
<Button
onPress = { this._onCancel }
title = { t('dialog.Cancel') } />
</View>
</Modal>
); );
} }
/**
* Called when the OK button is clicked.
*
* @private
* @returns {void}
*/
_onLogin() {
this.props.dispatch(_showLoginDialog());
}
/** /**
* Called when the cancel button is clicked. * Called when the cancel button is clicked.
* *
@ -102,6 +88,33 @@ class WaitForOwnerDialog extends Component {
_onCancel() { _onCancel() {
this.props.dispatch(cancelWaitForOwner()); this.props.dispatch(cancelWaitForOwner());
} }
/**
* Called when the OK button is clicked.
*
* @private
* @returns {void}
*/
_onLogin() {
this.props.dispatch(_openLoginDialog());
}
/**
* Renders a specific <tt>string</tt> which may contain HTML.
*
* @param {string} html - The <tt>string</tt> which may contain HTML to
* render.
* @returns {string}
*/
_renderHTML(html) {
if (typeof html === 'string') {
// TODO Limited styling may easily be provided by utilizing Text
// with style.
return html.replace(/<\\?b>/gi, '');
}
return html;
}
} }
/** /**
@ -111,7 +124,7 @@ class WaitForOwnerDialog extends Component {
* @param {Object} state - The Redux state. * @param {Object} state - The Redux state.
* @private * @private
* @returns {{ * @returns {{
* roomName: string * _room: string
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
@ -120,7 +133,7 @@ function _mapStateToProps(state) {
} = state['features/base/conference']; } = state['features/base/conference'];
return { return {
roomName: authRequired && authRequired.getName() _room: authRequired && authRequired.getName()
}; };
} }

View File

@ -1,23 +1,55 @@
import { import { BoxModel, createStyleSheet } from '../../base/styles';
ColorPalette,
createStyleSheet /**
} from '../../base/styles'; * The style common to <tt>LoginDialog</tt> and <tt>WaitForOwnerDialog</tt>.
*/
const dialog = {
marginBottom: BoxModel.margin,
marginTop: BoxModel.margin
};
/**
* The style common to <tt>Text</tt> rendered by <tt>LoginDialog</tt> and
* <tt>WaitForOwnerDialog</tt>.
*/
const text = {
};
/** /**
* The styles of the authentication feature. * The styles of the authentication feature.
*/ */
export default createStyleSheet({ export default createStyleSheet({
outerArea: { /**
flex: 1 * The style of <tt>LoginDialog</tt>.
*/
loginDialog: {
...dialog,
flex: 0,
flexDirection: 'column'
}, },
dialogBox: {
marginLeft: '10%', /**
marginRight: '10%', * The style of <tt>Text</tt> rendered by <tt>LoginDialog</tt>.
marginTop: '10%', */
backgroundColor: ColorPalette.white loginDialogText: {
...text
}, },
textInput: {
height: 25, /**
fontSize: 16 * The style of <tt>TextInput</tt> rendered by <tt>LoginDialog</tt>.
*/
loginDialogTextInput: {
// XXX Matches react-native-prompt's dialogInput because base/dialog's
// Dialog is implemented using react-native-prompt.
fontSize: 18,
height: 50
},
/**
* The style of <tt>WaitForOwnerDialog</tt>.
*/
waitForOwnerDialog: {
...dialog,
...text
} }
}); });

View File

@ -1,74 +0,0 @@
import {
LoginDialog,
WaitForOwnerDialog
} from './components/index';
import { hideDialog } from '../base/dialog/actions';
/**
* Will clear the wait for conference owner timeout handler if any is currently
* set.
*
* @param {Object} store - The Redux store instance.
* @returns {void}
*/
export function clearExistingWaitForOwnerTimeout(store) {
const { waitForOwnerTimeoutID }
= store.getState()['features/authentication'];
if (waitForOwnerTimeoutID) {
clearTimeout(waitForOwnerTimeoutID);
}
}
/**
* Checks if {@link LoginDialog} is currently open.
*
* @param {Object|Function} getStateOrState - The Redux store instance or
* store's get state method.
* @returns {boolean}
*/
export function isLoginDialogOpened(getStateOrState) {
const state
= typeof getStateOrState === 'function'
? getStateOrState() : getStateOrState;
const dialogState = state['features/base/dialog'];
return dialogState.component && dialogState.component === LoginDialog;
}
/**
* Hides {@link LoginDialog} if it's currently displayed.
*
* @param {Object} store - The Redux store instance.
* @returns {void}
*/
export function hideLoginDialog({ dispatch, getState }) {
if (isLoginDialogOpened(getState)) {
dispatch(hideDialog());
}
}
/**
* Checks if {@link WaitForOwnerDialog} is currently open.
*
* @param {Object} store - The Redux store instance.
* @returns {boolean}
*/
export function isWaitForOwnerDialogOpened({ getState }) {
const dialogState = getState()['features/base/dialog'];
return dialogState.component
&& dialogState.component === WaitForOwnerDialog;
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
*
* @param {Object} store - The Redux store instance.
* @returns {boolean}
*/
export function isWaitingForOwner({ getState }) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
return Boolean(waitForOwnerTimeoutID);
}

View File

@ -1,7 +1,6 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './functions'; export * from './components';
import './middleware'; import './middleware';
import './reducer'; import './reducer';

View File

@ -1,40 +1,33 @@
import { appNavigate } from '../app';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection';
import { hideDialog, isDialogOpen } from '../base/dialog';
import {
JitsiConferenceErrors,
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { import {
clearWaitForOwnerTimeout, _openLoginDialog,
_showLoginDialog, _openWaitForOwnerDialog,
_showWaitForOwnerDialog, stopWaitForOwner,
waitForOwner waitForOwner
} from './actions'; } from './actions';
import { appNavigate } from '../app/actions';
import { import {
CANCEL_LOGIN, CANCEL_LOGIN,
CANCEL_WAIT_FOR_OWNER, CANCEL_WAIT_FOR_OWNER,
STOP_WAIT_FOR_OWNER, STOP_WAIT_FOR_OWNER,
WAIT_FOR_OWNER WAIT_FOR_OWNER
} from './actionTypes'; } from './actionTypes';
import { import { LoginDialog, WaitForOwnerDialog } from './components';
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference/actionTypes';
import { hideDialog } from '../base/dialog/actions';
import { CONNECTION_ESTABLISHED } from '../base/connection/actionTypes';
import { CONNECTION_FAILED } from '../base/connection';
import {
clearExistingWaitForOwnerTimeout,
hideLoginDialog,
isLoginDialogOpened,
isWaitForOwnerDialogOpened,
isWaitingForOwner
} from './functions';
import {
JitsiConferenceErrors,
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
/** /**
* Middleware that captures connection or conference failed errors and controlls * Middleware that captures connection or conference failed errors and controls
* {@link WaitForOwnerDialog} and {@link LoginDialog}. * {@link WaitForOwnerDialog} and {@link LoginDialog}.
* *
* FIXME Some of the complexity was introduced by the lack of dialog stacking. * FIXME Some of the complexity was introduced by the lack of dialog stacking.
@ -44,93 +37,121 @@ import {
*/ */
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case CONNECTION_FAILED: {
if (action.error === JitsiConnectionErrors.PASSWORD_REQUIRED) {
store.dispatch(_showLoginDialog());
}
break;
}
case CONNECTION_ESTABLISHED: {
hideLoginDialog(store);
break;
}
case CONFERENCE_FAILED: {
if (action.error === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
store.dispatch(waitForOwner());
} else {
store.dispatch(clearWaitForOwnerTimeout());
}
break;
}
case CONFERENCE_JOINED: {
if (isWaitingForOwner(store)) {
store.dispatch(clearWaitForOwnerTimeout());
}
hideLoginDialog(store);
break;
}
case CONFERENCE_LEFT: {
store.dispatch(clearWaitForOwnerTimeout());
break;
}
case WAIT_FOR_OWNER: {
clearExistingWaitForOwnerTimeout(store);
const { handler, timeoutMs } = action;
const newTimeoutId = setTimeout(handler, timeoutMs);
action.waitForOwnerTimeoutID = newTimeoutId;
// The WAIT_FOR_OWNER action is cyclic and we don't want to hide
// the login dialog every few seconds...
if (!isLoginDialogOpened(store.getState())) {
store.dispatch(_showWaitForOwnerDialog());
}
break;
}
case STOP_WAIT_FOR_OWNER: {
clearExistingWaitForOwnerTimeout(store);
if (isWaitForOwnerDialogOpened(store)) {
store.dispatch(hideDialog());
}
break;
}
case CANCEL_LOGIN: { case CANCEL_LOGIN: {
const { upgradeRoleInProgress } const { upgradeRoleInProgress }
= store.getState()['features/authentication']; = store.getState()['features/authentication'];
if (upgradeRoleInProgress) { upgradeRoleInProgress && upgradeRoleInProgress.cancel();
upgradeRoleInProgress.cancel();
}
const waitingForOwner = isWaitingForOwner(store);
// The LoginDialog can be opened on top of "wait for owner". The app // The LoginDialog can be opened on top of "wait for owner". The app
// should navigate only if LoginDialog was open without // should navigate only if LoginDialog was open without the
// the WaitForOwnerDialog. // WaitForOwnerDialog.
if (!isWaitForOwnerDialogOpened(store) && !waitingForOwner) { if (!isDialogOpen(store, WaitForOwnerDialog)) {
// Go back to app entry point if (_isWaitingForOwner(store)) {
hideLoginDialog(store);
store.dispatch(appNavigate(undefined));
} else if (!isWaitForOwnerDialogOpened(store) && waitingForOwner) {
// Instead of hiding show the new one. // Instead of hiding show the new one.
const result = next(action); const result = next(action);
store.dispatch(_showWaitForOwnerDialog()); store.dispatch(_openWaitForOwnerDialog());
return result; return result;
} }
// Go back to the app's entry point.
_hideLoginDialog(store);
store.dispatch(appNavigate(undefined));
}
break; break;
} }
case CANCEL_WAIT_FOR_OWNER: { case CANCEL_WAIT_FOR_OWNER: {
const result = next(action); const result = next(action);
store.dispatch(clearWaitForOwnerTimeout()); store.dispatch(stopWaitForOwner());
store.dispatch(appNavigate(undefined)); store.dispatch(appNavigate(undefined));
return result; return result;
} }
case CONFERENCE_FAILED:
if (action.error === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
store.dispatch(waitForOwner());
} else {
store.dispatch(stopWaitForOwner());
}
break;
case CONFERENCE_JOINED:
if (_isWaitingForOwner(store)) {
store.dispatch(stopWaitForOwner());
}
_hideLoginDialog(store);
break;
case CONFERENCE_LEFT:
store.dispatch(stopWaitForOwner());
break;
case CONNECTION_ESTABLISHED:
_hideLoginDialog(store);
break;
case CONNECTION_FAILED:
action.error === JitsiConnectionErrors.PASSWORD_REQUIRED
&& store.dispatch(_openLoginDialog());
break;
case STOP_WAIT_FOR_OWNER:
_clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
break;
case WAIT_FOR_OWNER: {
_clearExistingWaitForOwnerTimeout(store);
const { handler, timeoutMs } = action;
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
// The WAIT_FOR_OWNER action is cyclic and we don't want to hide the
// login dialog every few seconds...
isDialogOpen(store, LoginDialog)
|| store.dispatch(_openWaitForOwnerDialog());
break;
}
} }
return next(action); return next(action);
}); });
/**
* Will clear the wait for conference owner timeout handler if any is currently
* set.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _clearExistingWaitForOwnerTimeout({ getState }) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
}
/**
* Hides {@link LoginDialog} if it's currently displayed.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _hideLoginDialog({ dispatch }) {
dispatch(hideDialog(LoginDialog));
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
*
* @param {Object} store - The redux store.
* @returns {boolean}
*/
function _isWaitingForOwner({ getState }) {
return Boolean(getState()['features/authentication'].waitForOwnerTimeoutID);
}

View File

@ -1,43 +1,39 @@
import { assign } from '../base/redux/functions'; /* @flow */
import { ReducerRegistry } from '../base/redux';
import { assign, ReducerRegistry } from '../base/redux';
import { import {
CANCEL_LOGIN, CANCEL_LOGIN,
STOP_WAIT_FOR_OWNER, STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FAILED, UPGRADE_ROLE_STARTED, UPGRADE_ROLE_SUCCESS, UPGRADE_ROLE_FINISHED,
UPGRADE_ROLE_STARTED,
WAIT_FOR_OWNER WAIT_FOR_OWNER
} from './actionTypes'; } from './actionTypes';
ReducerRegistry.register('features/authentication', (state = {}, action) => { ReducerRegistry.register('features/authentication', (state = {}, action) => {
switch (action.type) { switch (action.type) {
case WAIT_FOR_OWNER:
return assign(state, {
waitForOwnerTimeoutID: action.waitForOwnerTimeoutID
});
case UPGRADE_ROLE_STARTED:
return assign(state, {
upgradeRoleError: undefined,
upgradeRoleInProgress: action.authConnection
});
case UPGRADE_ROLE_SUCCESS:
return assign(state, {
upgradeRoleError: undefined,
upgradeRoleInProgress: undefined
});
case UPGRADE_ROLE_FAILED:
return assign(state, {
upgradeRoleError: action.error,
upgradeRoleInProgress: undefined
});
case CANCEL_LOGIN: case CANCEL_LOGIN:
return assign(state, { return assign(state, {
upgradeRoleError: undefined, upgradeRoleError: undefined,
upgradeRoleInProgress: undefined upgradeRoleInProgress: undefined
}); });
case STOP_WAIT_FOR_OWNER: case STOP_WAIT_FOR_OWNER:
return assign(state, { return assign(state, {
waitForOwnerTimeoutID: undefined, upgradeRoleError: undefined,
upgradeRoleError: undefined waitForOwnerTimeoutID: undefined
});
case UPGRADE_ROLE_FINISHED:
case UPGRADE_ROLE_STARTED:
return assign(state, {
upgradeRoleError: action.error,
upgradeRoleInProgress: action.thenableWithCancel
});
case WAIT_FOR_OWNER:
return assign(state, {
waitForOwnerTimeoutID: action.waitForOwnerTimeoutID
}); });
} }

View File

@ -298,12 +298,10 @@ export function createConference() {
*/ */
export function checkIfCanJoin() { export function checkIfCanJoin() {
return (dispatch, getState) => { return (dispatch, getState) => {
const { password, authRequired } const { authRequired, password }
= getState()['features/base/conference']; = getState()['features/base/conference'];
if (authRequired) { authRequired && authRequired.join(password);
authRequired.join(password);
}
}; };
} }

View File

@ -18,12 +18,11 @@ import {
/** /**
* Opens new connection. * Opens new connection.
* *
* @param {string} [username] - The XMPP user id eg. user@server.com. * @param {string} [id] - The XMPP user's ID (e.g. user@server.com).
* @param {string} [password] - The user's password. * @param {string} [password] - The XMPP user's password.
*
* @returns {Function} * @returns {Function}
*/ */
export function connect(username: ?string, password: ?string) { export function connect(id: ?string, password: ?string) {
return (dispatch: Dispatch<*>, getState: Function) => { return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState(); const state = getState();
const options = _constructOptions(state); const options = _constructOptions(state);
@ -47,7 +46,7 @@ export function connect(username: ?string, password: ?string) {
_onConnectionFailed); _onConnectionFailed);
connection.connect({ connection.connect({
id: username, id,
password password
}); });

View File

@ -53,24 +53,14 @@ export function getURLWithoutParams(url: URL): URL {
} }
/** /**
* Convert provided id to jid if it's not jid yet. * Converts a specific id to jid if it's not jid yet.
* *
* @param {string} id - User id or jid. * @param {string} id - User id or jid.
* @param {Object} configHosts - The 'hosts' part of the config object. * @param {Object} configHosts - The <tt>hosts</tt> part of the <tt>config</tt>
* @returns {string} jid - A string in the form of user@server.com. * object.
* @returns {string} A string in the form of a JID (i.e.
* <tt>user@server.com</tt>).
*/ */
export function toJid(id: string, configHosts: Object): string { export function toJid(id: string, { authdomain, domain }: Object): string {
if (id.indexOf('@') >= 0) { return id.indexOf('@') >= 0 ? id : `${id}@${authdomain || domain}`;
return id;
}
let jid = id.concat('@');
if (configHosts.authdomain) {
jid += configHosts.authdomain;
} else {
jid += configHosts.domain;
}
return jid;
} }