[RN] Room lock button

This commit is contained in:
Lyubomir Marinov 2016-12-12 13:49:23 -06:00
parent 8c3fb54d3d
commit 9a49a01713
7 changed files with 193 additions and 73 deletions

View File

@ -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
* <tt>JitsiConference</tt> 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.

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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

View File

@ -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
};
}

View File

@ -77,13 +77,13 @@ class Toolbar extends AbstractToolbar {
iconName = { audioButtonStyles.iconName }
iconStyle = { audioButtonStyles.iconStyle }
onClick = { this._toggleAudio }
style = { audioButtonStyles.buttonStyle } />
style = { audioButtonStyles.style } />
<ToolbarButton
iconName = 'hangup'
iconStyle = { styles.whiteIcon }
onClick = { this._onHangup }
style = {{
...styles.toolbarButton,
...styles.primaryToolbarButton,
backgroundColor: ColorPalette.red
}}
underlayColor = { ColorPalette.buttonUnderlay } />
@ -91,7 +91,7 @@ class Toolbar extends AbstractToolbar {
iconName = { videoButtonStyles.iconName }
iconStyle = { videoButtonStyles.iconStyle }
onClick = { this._toggleVideo }
style = { videoButtonStyles.buttonStyle } />
style = { videoButtonStyles.style } />
</View>
);
@ -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 (
<View style = { styles.secondaryToolbar }>
<ToolbarButton
iconName = 'reload'
iconStyle = { styles.whiteIcon }
iconStyle = { iconStyle }
onClick = { this._toggleCameraFacingMode }
style = { styles.toggleCameraFacingModeButton }
underlayColor = 'transparent' />
style = { style }
underlayColor = { underlayColor } />
<ToolbarButton
iconName = {
this.props.locked ? 'security-locked' : 'security'
}
iconStyle = { iconStyle }
onClick = { this._toggleLock }
style = { style }
underlayColor = { underlayColor } />
</View>
);
/* eslint-enable react/jsx-handler-names */
/* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */
}
/**

View File

@ -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
},