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);