From 9a49a017130b103548026861e334d17f1b979f92 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Mon, 12 Dec 2016 13:49:23 -0600 Subject: [PATCH] [RN] Room lock button --- react/features/base/conference/actionTypes.js | 12 +++ react/features/base/conference/actions.js | 26 ++++++ react/features/base/conference/reducer.js | 41 ++++++++- react/features/base/react/Symbol.js | 18 +++- .../toolbar/components/AbstractToolbar.js | 55 ++++++++---- .../toolbar/components/Toolbar.native.js | 28 ++++-- react/features/toolbar/components/styles.js | 86 +++++++++---------- 7 files changed, 193 insertions(+), 73 deletions(-) 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 },