diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js
index fdcfdf2ba..3dac9614d 100644
--- a/react/features/base/conference/actionTypes.js
+++ b/react/features/base/conference/actionTypes.js
@@ -45,6 +45,18 @@ export const CONFERENCE_LEFT = Symbol('CONFERENCE_LEFT');
*/
export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
+/**
+ * The type of the Redux action which signals that the lock state of a specific
+ * JitsiConference changed.
+ *
+ * {
+ * type: LOCK_STATE_CHANGED,
+ * conference: JitsiConference,
+ * locked: boolean
+ * }
+ */
+export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
+
/**
* The type of the Redux action which sets the password to join or lock a
* specific JitsiConference.
diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js
index 49334ccd3..ece614e1f 100644
--- a/react/features/base/conference/actions.js
+++ b/react/features/base/conference/actions.js
@@ -13,6 +13,7 @@ import {
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
+ LOCK_STATE_CHANGED,
SET_PASSWORD,
SET_ROOM
} from './actionTypes';
@@ -46,6 +47,10 @@ function _addConferenceListeners(conference, dispatch) {
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
(...args) => dispatch(dominantSpeakerChanged(...args)));
+ conference.on(
+ JitsiConferenceEvents.LOCK_STATE_CHANGED,
+ (...args) => dispatch(_lockStateChanged(conference, ...args)));
+
conference.on(
JitsiConferenceEvents.TRACK_ADDED,
t => t && !t.isLocal() && dispatch(trackAdded(t)));
@@ -185,6 +190,27 @@ export function createConference() {
};
}
+/**
+ * Signals that the lock state of a specific JitsiConference changed.
+ *
+ * @param {JitsiConference} conference - The JitsiConference which had its lock
+ * state changed.
+ * @param {boolean} locked - If the specified conference became locked, true;
+ * otherwise, false.
+ * @returns {{
+ * type: LOCK_STATE_CHANGED,
+ * conference: JitsiConference,
+ * locked: boolean
+ * }}
+ */
+function _lockStateChanged(conference, locked) {
+ return {
+ type: LOCK_STATE_CHANGED,
+ conference,
+ locked
+ };
+}
+
/**
* Sets the password to join or lock a specific JitsiConference.
*
diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js
index 958ed1885..7343a5a06 100644
--- a/react/features/base/conference/reducer.js
+++ b/react/features/base/conference/reducer.js
@@ -10,6 +10,7 @@ import {
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
+ LOCK_STATE_CHANGED,
SET_PASSWORD,
SET_ROOM
} from './actionTypes';
@@ -33,6 +34,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case CONFERENCE_WILL_LEAVE:
return _conferenceWillLeave(state, action);
+ case LOCK_STATE_CHANGED:
+ return _lockStateChanged(state, action);
+
case SET_PASSWORD:
return _setPassword(state, action);
@@ -70,6 +74,7 @@ function _conferenceFailed(state, action) {
setStateProperties(state, {
conference: undefined,
leaving: undefined,
+ locked: undefined,
password: undefined,
/**
@@ -92,6 +97,14 @@ function _conferenceFailed(state, action) {
* reduction of the specified action.
*/
function _conferenceJoined(state, action) {
+ const conference = action.conference;
+
+ // FIXME The indicator which determines whether a JitsiConference is locked
+ // i.e. password-protected is private to lib-jitsi-meet. However, the
+ // library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
+ // with a password.
+ const locked = conference.room.locked || undefined;
+
return (
setStateProperties(state, {
/**
@@ -100,8 +113,15 @@ function _conferenceJoined(state, action) {
*
* @type {JitsiConference}
*/
- conference: action.conference,
+ conference,
leaving: undefined,
+
+ /**
+ * The indicator which determines whether the conference is locked.
+ *
+ * @type {boolean}
+ */
+ locked,
passwordRequired: undefined
}));
}
@@ -127,6 +147,7 @@ function _conferenceLeft(state, action) {
setStateProperties(state, {
conference: undefined,
leaving: undefined,
+ locked: undefined,
password: undefined,
passwordRequired: undefined
}));
@@ -162,6 +183,24 @@ function _conferenceWillLeave(state, action) {
}));
}
+/**
+ * Reduces a specific Redux action LOCK_STATE_CHANGED of the feature
+ * base/conference.
+ *
+ * @param {Object} state - The Redux state of the feature base/conference.
+ * @param {Action} action - The Redux action LOCK_STATE_CHANGED to reduce.
+ * @private
+ * @returns {Object} The new state of the feature base/conference after the
+ * reduction of the specified action.
+ */
+function _lockStateChanged(state, action) {
+ if (state.conference !== action.conference) {
+ return state;
+ }
+
+ return setStateProperty(state, 'locked', action.locked || undefined);
+}
+
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*
diff --git a/react/features/base/react/Symbol.js b/react/features/base/react/Symbol.js
index bd152fdd3..cd44f2110 100644
--- a/react/features/base/react/Symbol.js
+++ b/react/features/base/react/Symbol.js
@@ -1,14 +1,24 @@
// FIXME React Native does not polyfill Symbol at versions 0.39.2 or earlier.
export default (global => {
- let s = global.Symbol;
+ let clazz = global.Symbol;
- if (typeof s === 'undefined') {
+ if (typeof clazz === 'undefined') {
// XXX At the time of this writing we use Symbol only as a way to
// prevent collisions in Redux action types. Consequently, the Symbol
// implementation provided bellow is minimal and specific to our
// purpose.
- s = description => (description || '').split('');
+ const toString = function() {
+ return this.join(''); // eslint-disable-line no-invalid-this
+ };
+
+ clazz = description => {
+ const thiz = (description || '').split('');
+
+ thiz.toString = toString;
+
+ return thiz;
+ };
}
- return s;
+ return clazz;
})(global || window || this); // eslint-disable-line no-invalid-this
diff --git a/react/features/toolbar/components/AbstractToolbar.js b/react/features/toolbar/components/AbstractToolbar.js
index 7ced24a57..c43eaba81 100644
--- a/react/features/toolbar/components/AbstractToolbar.js
+++ b/react/features/toolbar/components/AbstractToolbar.js
@@ -1,10 +1,7 @@
import React, { Component } from 'react';
import { appNavigate } from '../../app';
-import {
- toggleAudioMuted,
- toggleVideoMuted
-} from '../../base/media';
+import { toggleAudioMuted, toggleVideoMuted } from '../../base/media';
import { ColorPalette } from '../../base/styles';
import { styles } from './styles';
@@ -23,6 +20,12 @@ export class AbstractToolbar extends Component {
static propTypes = {
audioMuted: React.PropTypes.bool,
dispatch: React.PropTypes.func,
+
+ /**
+ * The indicator which determines whether the conference is
+ * locked/password-protected.
+ */
+ locked: React.PropTypes.bool,
videoMuted: React.PropTypes.bool,
visible: React.PropTypes.bool.isRequired
}
@@ -39,6 +42,7 @@ export class AbstractToolbar extends Component {
// Bind event handlers so they are only bound once for every instance.
this._onHangup = this._onHangup.bind(this);
this._toggleAudio = this._toggleAudio.bind(this);
+ this._toggleLock = this._toggleLock.bind(this);
this._toggleVideo = this._toggleVideo.bind(this);
}
@@ -50,33 +54,32 @@ export class AbstractToolbar extends Component {
* button to get styles for.
* @protected
* @returns {{
- * buttonStyle: Object,
* iconName: string,
- * iconStyle: Object
+ * iconStyle: Object,
+ * style: Object
* }}
*/
_getMuteButtonStyles(mediaType) {
- let buttonStyle;
let iconName;
let iconStyle;
+ let style = styles.primaryToolbarButton;
if (this.props[`${mediaType}Muted`]) {
- buttonStyle = {
- ...styles.toolbarButton,
- backgroundColor: ColorPalette.buttonUnderlay
- };
iconName = this[`${mediaType}MutedIcon`];
iconStyle = styles.whiteIcon;
+ style = {
+ ...style,
+ backgroundColor: ColorPalette.buttonUnderlay
+ };
} else {
- buttonStyle = styles.toolbarButton;
iconName = this[`${mediaType}Icon`];
iconStyle = styles.icon;
}
return {
- buttonStyle,
iconName,
- iconStyle
+ iconStyle,
+ style
};
}
@@ -96,7 +99,7 @@ export class AbstractToolbar extends Component {
}
/**
- * Dispatches action to toggle the mute state of the audio/microphone.
+ * Dispatches an action to toggle the mute state of the audio/microphone.
*
* @protected
* @returns {void}
@@ -106,7 +109,18 @@ export class AbstractToolbar extends Component {
}
/**
- * Dispatches action to toggle the mute state of the video/camera.
+ * Dispatches an action to toggle the lock i.e. password protection of the
+ * conference.
+ *
+ * @protected
+ * @returns {void}
+ */
+ _toggleLock() {
+ // TODO Auto-generated method stub
+ }
+
+ /**
+ * Dispatches an action to toggle the mute state of the video/camera.
*
* @protected
* @returns {void}
@@ -123,10 +137,19 @@ export class AbstractToolbar extends Component {
* @returns {{ audioMuted: boolean, videoMuted: boolean }}
*/
export function mapStateToProps(state) {
+ const conference = state['features/base/conference'];
const media = state['features/base/media'];
return {
audioMuted: media.audio.muted,
+
+ /**
+ * The indicator which determines whether the conference is
+ * locked/password-protected.
+ *
+ * @type {boolean}
+ */
+ locked: conference.locked,
videoMuted: media.video.muted
};
}
diff --git a/react/features/toolbar/components/Toolbar.native.js b/react/features/toolbar/components/Toolbar.native.js
index a01254a6d..537c6dbf0 100644
--- a/react/features/toolbar/components/Toolbar.native.js
+++ b/react/features/toolbar/components/Toolbar.native.js
@@ -77,13 +77,13 @@ class Toolbar extends AbstractToolbar {
iconName = { audioButtonStyles.iconName }
iconStyle = { audioButtonStyles.iconStyle }
onClick = { this._toggleAudio }
- style = { audioButtonStyles.buttonStyle } />
+ style = { audioButtonStyles.style } />
@@ -91,7 +91,7 @@ class Toolbar extends AbstractToolbar {
iconName = { videoButtonStyles.iconName }
iconStyle = { videoButtonStyles.iconStyle }
onClick = { this._toggleVideo }
- style = { videoButtonStyles.buttonStyle } />
+ style = { videoButtonStyles.style } />
);
@@ -106,21 +106,33 @@ class Toolbar extends AbstractToolbar {
* @returns {ReactElement}
*/
_renderSecondaryToolbar() {
- /* eslint-disable react/jsx-handler-names */
+ const iconStyle = styles.whiteIcon;
+ const style = styles.secondaryToolbarButton;
+ const underlayColor = 'transparent';
+
+ /* eslint-disable react/jsx-curly-spacing,react/jsx-handler-names */
// TODO Use an appropriate icon for toggle camera facing mode.
return (
+ style = { style }
+ underlayColor = { underlayColor } />
+
);
- /* eslint-enable react/jsx-handler-names */
+ /* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */
}
/**
diff --git a/react/features/toolbar/components/styles.js b/react/features/toolbar/components/styles.js
index 96cc13f71..004074f67 100644
--- a/react/features/toolbar/components/styles.js
+++ b/react/features/toolbar/components/styles.js
@@ -1,35 +1,23 @@
-import { ColorPalette, createStyleSheet } from '../../base/styles';
+import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
/**
- * Generic styles for a button.
+ * The base style for (toolbar) buttons.
*
* @type {Object}
*/
const button = {
- alignSelf: 'center',
borderRadius: 35,
borderWidth: 0,
+ flex: 0,
flexDirection: 'row',
height: 60,
justifyContent: 'center',
+ margin: BoxModel.margin,
width: 60
};
/**
- * Generic container for buttons.
- *
- * @type {Object}
- */
-const container = {
- flex: 1,
- flexDirection: 'row',
- left: 0,
- position: 'absolute',
- right: 0
-};
-
-/**
- * Generic styles for an icon.
+ * The base style for icons.
*
* @type {Object}
*/
@@ -39,28 +27,45 @@ const icon = {
fontSize: 24
};
+/**
+ * The base style for toolbars.
+ *
+ * @type {Object}
+ */
+const toolbar = {
+ flex: 1,
+ position: 'absolute'
+};
+
/**
* The (conference) toolbar related styles.
- * TODO Make styles more generic and reusable. Use ColorPalette for all colors.
*/
export const styles = createStyleSheet({
/**
* The toolbar button icon style.
*/
- icon: {
- ...icon,
- color: ColorPalette.darkGrey
- },
+ icon,
/**
* The style of the toolbar which contains the primary buttons such as
* hangup, audio and video mute.
*/
primaryToolbar: {
- ...container,
- bottom: 30,
- height: 60,
- justifyContent: 'center'
+ ...toolbar,
+ bottom: 3 * BoxModel.margin,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ left: 0,
+ right: 0
+ },
+
+ /**
+ * The style of button in primaryToolbar.
+ */
+ primaryToolbarButton: {
+ ...button,
+ backgroundColor: ColorPalette.white,
+ opacity: 0.8
},
/**
@@ -68,36 +73,29 @@ export const styles = createStyleSheet({
* toggle camera facing mode.
*/
secondaryToolbar: {
- ...container,
- height: 60,
- justifyContent: 'flex-end'
+ ...toolbar,
+ bottom: 0,
+ flexDirection: 'column',
+ right: 0,
+ top: 0
},
/**
- * The toggle camera facing mode button style.
+ * The style of button in secondaryToolbar.
*/
- toggleCameraFacingModeButton: {
+ secondaryToolbarButton: {
...button,
backgroundColor: 'transparent'
},
/**
- * The toolbar button style.
- */
- toolbarButton: {
- ...button,
- backgroundColor: ColorPalette.white,
- marginLeft: 20,
- marginRight: 20,
- opacity: 0.8
- },
-
- /**
- * The toolbar container style.
+ * The style of the root/top-level Container of Toolbar.
*/
toolbarContainer: {
- ...container,
bottom: 0,
+ left: 0,
+ position: 'absolute',
+ right: 0,
top: 0
},