diff --git a/package.json b/package.json
index f2a21e2cb..6352e660c 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"react": "15.4.1",
"react-dom": "15.4.1",
"react-native": "0.39.0",
+ "react-native-prompt": "^1.0.0",
"react-native-vector-icons": "^3.0.0",
"react-native-webrtc": "jitsi/react-native-webrtc",
"react-redux": "^4.4.6",
diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js
index 3d3da4e95..fdcfdf2ba 100644
--- a/react/features/base/conference/actionTypes.js
+++ b/react/features/base/conference/actionTypes.js
@@ -1,5 +1,17 @@
import { Symbol } from '../react';
+/**
+ * The type of the Redux action which signals that a specific conference has
+ * failed.
+ *
+ * {
+ * type: CONFERENCE_FAILED,
+ * conference: JitsiConference,
+ * error: string
+ * }
+ */
+export const CONFERENCE_FAILED = Symbol('CONFERENCE_FAILED');
+
/**
* The type of the Redux action which signals that a specific conference has
* been joined.
@@ -33,6 +45,19 @@ export const CONFERENCE_LEFT = Symbol('CONFERENCE_LEFT');
*/
export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
+/**
+ * The type of the Redux action which sets the password to join or lock a
+ * specific JitsiConference.
+ *
+ * {
+ * type: SET_PASSWORD,
+ * conference: JitsiConference,
+ * method: Function
+ * password: string
+ * }
+ */
+export const SET_PASSWORD = Symbol('SET_PASSWORD');
+
/**
* The type of the Redux action which sets the name of the room of the
* conference to be joined.
diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js
index e992be685..49334ccd3 100644
--- a/react/features/base/conference/actions.js
+++ b/react/features/base/conference/actions.js
@@ -9,9 +9,11 @@ import {
import { trackAdded, trackRemoved } from '../tracks';
import {
+ CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
+ SET_PASSWORD,
SET_ROOM
} from './actionTypes';
import { EMAIL_COMMAND } from './constants';
@@ -30,6 +32,9 @@ import './reducer';
function _addConferenceListeners(conference, dispatch) {
const JitsiConferenceEvents = JitsiMeetJS.events.conference;
+ conference.on(
+ JitsiConferenceEvents.CONFERENCE_FAILED,
+ (...args) => dispatch(_conferenceFailed(conference, ...args)));
conference.on(
JitsiConferenceEvents.CONFERENCE_JOINED,
(...args) => dispatch(_conferenceJoined(conference, ...args)));
@@ -67,6 +72,26 @@ function _addConferenceListeners(conference, dispatch) {
(data, id) => dispatch(changeParticipantEmail(id, data.value)));
}
+/**
+ * Signals that a specific conference has failed.
+ *
+ * @param {JitsiConference} conference - The JitsiConference that has failed.
+ * @param {string} error - The error describing/detailing the cause of the
+ * failure.
+ * @returns {{
+ * type: CONFERENCE_FAILED,
+ * conference: JitsiConference,
+ * error: string
+ * }}
+ */
+function _conferenceFailed(conference, error) {
+ return {
+ type: CONFERENCE_FAILED,
+ conference,
+ error
+ };
+}
+
/**
* Attach any pre-existing local media to the conference once the conference has
* been joined.
@@ -144,7 +169,7 @@ export function createConference() {
throw new Error('Cannot create conference without connection');
}
- const room = state['features/base/conference'].room;
+ const { password, room } = state['features/base/conference'];
if (typeof room === 'undefined' || room === '') {
throw new Error('Cannot join conference without room name');
@@ -156,7 +181,32 @@ export function createConference() {
_addConferenceListeners(conference, dispatch);
- conference.join();
+ conference.join(password);
+ };
+}
+
+/**
+ * Sets the password to join or lock a specific JitsiConference.
+ *
+ * @param {JitsiConference} conference - The JitsiConference which requires a
+ * password to join or is to be locked with the specified password.
+ * @param {Function} method - The JitsiConference method of password protection
+ * such as join or lock.
+ * @param {string} password - The password with which the specified conference
+ * is to be joined or locked.
+ * @returns {{
+ * type: SET_PASSWORD,
+ * conference: JitsiConference,
+ * method: Function,
+ * password: string
+ * }}
+ */
+export function setPassword(conference, method, password) {
+ return {
+ type: SET_PASSWORD,
+ conference,
+ method,
+ password
};
}
diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js
index eb7c538b8..e335bdf40 100644
--- a/react/features/base/conference/middleware.js
+++ b/react/features/base/conference/middleware.js
@@ -8,6 +8,7 @@ import { MiddlewareRegistry } from '../redux';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import { createConference } from './actions';
+import { SET_PASSWORD } from './actionTypes';
import {
_addLocalTracksToConference,
_handleParticipantError,
@@ -28,6 +29,9 @@ MiddlewareRegistry.register(store => next => action => {
case PIN_PARTICIPANT:
return _pinParticipant(store, next, action);
+ case SET_PASSWORD:
+ return _setPassword(store, next, action);
+
case TRACK_ADDED:
case TRACK_REMOVED:
return _trackAddedOrRemoved(store, next, action);
@@ -107,6 +111,56 @@ function _pinParticipant(store, next, action) {
return next(action);
}
+/**
+ * Notifies the feature base/conference that the action SET_PASSWORD is
+ * being dispatched within a specific Redux store. Joins or locks a specific
+ * JitsiConference with a specific password.
+ *
+ * @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 SET_PASSWORD which is
+ * being dispatched in the specified store.
+ * @private
+ * @returns {Object} The new state that is the result of the reduction of the
+ * specified action.
+ */
+function _setPassword(store, next, action) {
+ const { conference, method } = action;
+
+ switch (method) {
+ case conference.join: {
+ let state = store.getState()['features/base/conference'];
+
+ // Make sure that the action will set a password for a conference that
+ // the application wants joined.
+ if (state.passwordRequired === conference) {
+ const result = next(action);
+
+ // Join the conference with the newly-set password.
+ const password = action.password;
+
+ // Make sure that the action did set the password.
+ state = store.getState()['features/base/conference'];
+ if (state.password === password
+ && !state.passwordRequired
+
+ // Make sure that the application still wants the conference
+ // joined.
+ && !state.conference) {
+ method.call(conference, password);
+ }
+
+ return result;
+ }
+ break;
+ }
+ }
+
+ return next(action);
+}
+
/**
* Synchronizes local tracks from state with local tracks in JitsiConference
* instance.
diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js
index 43c9032e1..958ed1885 100644
--- a/react/features/base/conference/reducer.js
+++ b/react/features/base/conference/reducer.js
@@ -6,9 +6,11 @@ import {
} from '../redux';
import {
+ CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
+ SET_PASSWORD,
SET_ROOM
} from './actionTypes';
import { isRoomValid } from './functions';
@@ -19,6 +21,9 @@ import { isRoomValid } from './functions';
*/
ReducerRegistry.register('features/base/conference', (state = {}, action) => {
switch (action.type) {
+ case CONFERENCE_FAILED:
+ return _conferenceFailed(state, action);
+
case CONFERENCE_JOINED:
return _conferenceJoined(state, action);
@@ -28,6 +33,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case CONFERENCE_WILL_LEAVE:
return _conferenceWillLeave(state, action);
+ case SET_PASSWORD:
+ return _setPassword(state, action);
+
case SET_ROOM:
return _setRoom(state, action);
}
@@ -35,6 +43,44 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
return state;
});
+/**
+ * Reduces a specific Redux action CONFERENCE_FAILED of the feature
+ * base/conference.
+ *
+ * @param {Object} state - The Redux state of the feature base/conference.
+ * @param {Action} action - The Redux action CONFERENCE_FAILED to reduce.
+ * @private
+ * @returns {Object} The new state of the feature base/conference after the
+ * reduction of the specified action.
+ */
+function _conferenceFailed(state, action) {
+ const conference = action.conference;
+
+ if (state.conference && state.conference !== conference) {
+ return state;
+ }
+
+ const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
+ const passwordRequired
+ = JitsiConferenceErrors.PASSWORD_REQUIRED === action.error
+ ? conference
+ : undefined;
+
+ return (
+ setStateProperties(state, {
+ conference: undefined,
+ leaving: undefined,
+ password: undefined,
+
+ /**
+ * The JitsiConference instance which requires a password to join.
+ *
+ * @type {JitsiConference}
+ */
+ passwordRequired
+ }));
+}
+
/**
* Reduces a specific Redux action CONFERENCE_JOINED of the feature
* base/conference.
@@ -55,7 +101,8 @@ function _conferenceJoined(state, action) {
* @type {JitsiConference}
*/
conference: action.conference,
- leaving: undefined
+ leaving: undefined,
+ passwordRequired: undefined
}));
}
@@ -79,7 +126,9 @@ function _conferenceLeft(state, action) {
return (
setStateProperties(state, {
conference: undefined,
- leaving: undefined
+ leaving: undefined,
+ password: undefined,
+ passwordRequired: undefined
}));
}
@@ -108,10 +157,43 @@ function _conferenceWillLeave(state, action) {
*
* @type {JitsiConference}
*/
- leaving: conference
+ leaving: conference,
+ passwordRequired: undefined
}));
}
+/**
+ * Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
+ *
+ * @param {Object} state - The Redux state of the feature base/conference.
+ * @param {Action} action - The Redux action SET_PASSWORD to reduce.
+ * @private
+ * @returns {Object} The new state of the feature base/conference after the
+ * reduction of the specified action.
+ */
+function _setPassword(state, action) {
+ const conference = action.conference;
+
+ switch (action.method) {
+ case conference.join:
+ if (state.passwordRequired === conference) {
+ return (
+ setStateProperties(state, {
+ /**
+ * The password with which the conference is to be joined.
+ *
+ * @type {string}
+ */
+ password: action.password,
+ passwordRequired: undefined
+ }));
+ }
+ break;
+ }
+
+ return state;
+}
+
/**
* Reduces a specific Redux action SET_ROOM of the feature base/conference.
*
diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js
index 28bb756ec..e783f90e6 100644
--- a/react/features/conference/components/Conference.native.js
+++ b/react/features/conference/components/Conference.native.js
@@ -7,6 +7,7 @@ import { FilmStrip } from '../../filmStrip';
import { LargeVideo } from '../../largeVideo';
import { Toolbar } from '../../toolbar';
+import PasswordRequiredPrompt from './PasswordRequiredPrompt';
import { styles } from './styles';
/**
@@ -24,6 +25,14 @@ class Conference extends Component {
* @static
*/
static propTypes = {
+ /**
+ * The indicator which determines whether a password is required to join
+ * the conference and has not been provided yet.
+ *
+ * @private
+ * @type {JitsiConference}
+ */
+ _passwordRequired: React.PropTypes.object,
dispatch: React.PropTypes.func
}
@@ -92,6 +101,10 @@ class Conference extends Component {
+
+ {
+ this._renderPrompt()
+ }
);
}
@@ -128,6 +141,46 @@ class Conference extends Component {
= setTimeout(this._onClick, TOOLBAR_TIMEOUT_MS);
}
}
+
+ /**
+ * Renders a prompt if necessary such as when a password is required to join
+ * the conference.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderPrompt() {
+ const passwordRequired = this.props._passwordRequired;
+
+ if (passwordRequired) {
+ return (
+
+ );
+ }
+
+ return null;
+ }
}
-export default reactReduxConnect()(Conference);
+/**
+ * Maps (parts of) the Redux state to the associated Conference's props.
+ *
+ * @param {Object} state - The Redux state.
+ * @returns {{
+ * _passwordRequired: boolean
+ * }}
+ */
+function mapStateToProps(state) {
+ return {
+ /**
+ * The indicator which determines whether a password is required to join
+ * the conference and has not been provided yet.
+ *
+ * @private
+ * @type {JitsiConference}
+ */
+ _passwordRequired: state['features/base/conference'].passwordRequired
+ };
+}
+
+export default reactReduxConnect(mapStateToProps)(Conference);
diff --git a/react/features/conference/components/PasswordRequiredPrompt.native.js b/react/features/conference/components/PasswordRequiredPrompt.native.js
new file mode 100644
index 000000000..7944cb513
--- /dev/null
+++ b/react/features/conference/components/PasswordRequiredPrompt.native.js
@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+import Prompt from 'react-native-prompt';
+import { connect } from 'react-redux';
+
+import { setPassword } from '../../base/conference';
+
+/**
+ * Implements a React Component which prompts the user when a password is
+ * required to join a conference.
+ */
+class PasswordRequiredPrompt extends Component {
+ /**
+ * PasswordRequiredPrompt component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * The JitsiConference which requires a password.
+ *
+ * @type {JitsiConference}
+ */
+ conference: React.PropTypes.object,
+ dispatch: React.PropTypes.func
+ }
+
+ /**
+ * Initializes a new PasswordRequiredPrompt instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onCancel = this._onCancel.bind(this);
+ this._onSubmit = this._onSubmit.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+
+ /**
+ * Notifies this prompt that it has been dismissed by cancel.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCancel() {
+ // XXX The user has canceled this prompt for a password so we are to
+ // attempt joining the conference without a password. If the conference
+ // still requires a password to join, the user will be prompted again
+ // later.
+ this._onSubmit(undefined);
+ }
+
+ /**
+ * Notifies this prompt that it has been dismissed by submitting a specific
+ * value.
+ *
+ * @param {string} value - The submitted value.
+ * @private
+ * @returns {void}
+ */
+ _onSubmit(value) {
+ const conference = this.props.conference;
+
+ this.props.dispatch(setPassword(conference, conference.join, value));
+ }
+}
+
+export default connect()(PasswordRequiredPrompt);