[RN] Refactor Toolbox

Create standalone components for each feature and move all state to them.
Toolbars are now dummy containers.
This commit is contained in:
Saúl Ibarra Corretgé 2018-04-18 16:34:40 +02:00 committed by Lyubo Marinov
parent 450400b768
commit a2834a2495
19 changed files with 599 additions and 564 deletions

View File

@ -74,6 +74,7 @@
"toolbar": { "toolbar": {
"addPeople": "Add people to your call", "addPeople": "Add people to your call",
"audioonly": "Enable / Disable audio only mode (saves bandwidth)", "audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"audioRoute": "Select the audio route",
"callQuality": "Manage call quality", "callQuality": "Manage call quality",
"enterFullScreen": "View full screen", "enterFullScreen": "View full screen",
"exitFullScreen": "Exit full screen", "exitFullScreen": "Exit full screen",
@ -87,6 +88,7 @@
"etherpad": "Open / Close shared document", "etherpad": "Open / Close shared document",
"documentOpen": "Open shared document", "documentOpen": "Open shared document",
"documentClose": "Close shared document", "documentClose": "Close shared document",
"shareRoom": "Share room",
"sharedvideo": "Share a YouTube video", "sharedvideo": "Share a YouTube video",
"stopSharedVideo": "Stop YouTube video", "stopSharedVideo": "Stop YouTube video",
"fullscreen": "View / Exit full screen", "fullscreen": "View / Exit full screen",
@ -102,6 +104,7 @@
"cameraDisabled": "Camera is not available", "cameraDisabled": "Camera is not available",
"micDisabled": "Microphone is not available", "micDisabled": "Microphone is not available",
"filmstrip": "Show / Hide videos", "filmstrip": "Show / Hide videos",
"pip": "Enter Picture-in-Picture mode",
"profile": "Edit your profile", "profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand", "raiseHand": "Raise / Lower your hand",
"shortcuts": "View shortcuts", "shortcuts": "View shortcuts",

View File

@ -1,4 +1,3 @@
export { default as AddPeopleDialog } from './AddPeopleDialog'; export { default as AddPeopleDialog } from './AddPeopleDialog';
export { default as InfoDialogButton } from './InfoDialogButton'; export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';
export { DialInSummary } from './dial-in-summary'; export { DialInSummary } from './dial-in-summary';

View File

@ -1,111 +0,0 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getAppProp } from '../../../app';
import { ToolbarButton } from '../../../toolbox';
import { enterPictureInPicture } from '../actions';
/**
* The type of {@link EnterPictureInPictureToobarButton}'s React
* {@code Component} props.
*/
type Props = {
/**
* Enters (or rather initiates entering) picture-in-picture.
*
* @protected
*/
_onEnterPictureInPicture: Function,
/**
* The indicator which determines whether Picture-in-Picture is enabled.
*
* @protected
*/
_pictureInPictureEnabled: boolean
};
/**
* Implements a {@link ToolbarButton} to enter Picture-in-Picture.
*/
class EnterPictureInPictureToolbarButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
_onEnterPictureInPicture,
_pictureInPictureEnabled,
...props
} = this.props;
if (!_pictureInPictureEnabled) {
return null;
}
return (
<ToolbarButton
iconName = { 'menu-down' }
onClick = { _onEnterPictureInPicture }
{ ...props } />
);
}
}
/**
* Maps redux actions to {@link EnterPictureInPictureToolbarButton}'s React
* {@code Component} props.
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @returns {{
* }}
* @private
*/
function _mapDispatchToProps(dispatch) {
return {
/**
* Requests Picture-in-Picture mode.
*
* @private
* @returns {void}
* @type {Function}
*/
_onEnterPictureInPicture() {
dispatch(enterPictureInPicture());
}
};
}
/**
* Maps (parts of) the redux state to
* {@link EnterPictureInPictureToolbarButton}'s React {@code Component} props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* }}
*/
function _mapStateToProps(state) {
return {
/**
* The indicator which determines whether Picture-in-Picture is enabled.
*
* @protected
* @type {boolean}
*/
_pictureInPictureEnabled:
Boolean(getAppProp(state, 'pictureInPictureEnabled'))
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(
EnterPictureInPictureToolbarButton);

View File

@ -1,2 +0,0 @@
export { default as EnterPictureInPictureToolbarButton }
from './EnterPictureInPictureToolbarButton';

View File

@ -1,3 +1,2 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components';

View File

@ -4,91 +4,75 @@ import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toggleAudioOnly } from '../../base/conference';
import {
MEDIA_TYPE,
toggleCameraFacingMode
} from '../../base/media';
import { Container } from '../../base/react'; import { Container } from '../../base/react';
import { import {
isNarrowAspectRatio, isNarrowAspectRatio,
makeAspectRatioAware makeAspectRatioAware
} from '../../base/responsive-ui'; } from '../../base/responsive-ui';
import { ColorPalette } from '../../base/styles'; import { ColorPalette } from '../../base/styles';
import { InviteButton } from '../../invite';
import {
EnterPictureInPictureToolbarButton
} from '../../mobile/picture-in-picture';
import { beginRoomLockRequest } from '../../room-lock';
import {
abstractMapDispatchToProps,
abstractMapStateToProps
} from '../functions';
import AudioRouteButton from './AudioRouteButton';
import styles from './styles'; import styles from './styles';
import ToolbarButton from './ToolbarButton';
import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons'; import {
AudioMuteButton,
AudioOnlyButton,
AudioRouteButton,
HangupButton,
PictureInPictureButton,
RoomLockButton,
InviteButton,
ToggleCameraButton,
VideoMuteButton
} from './buttons';
/**
* Styles for the hangup button.
*/
const hangupButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.hangup,
underlayColor: ColorPalette.buttonUnderlay
};
/**
* Styles for buttons in the primary toolbar.
*/
const primaryToolbarButtonStyles = {
iconStyle: styles.primaryToolbarButtonIcon,
style: styles.primaryToolbarButton
};
/**
* Styles for buttons in the primary toolbar.
*/
const primaryToolbarToggledButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.whitePrimaryToolbarButton
};
/**
* Styles for buttons in the secondary toolbar.
*/
const secondaryToolbarButtonStyles = {
iconStyle: styles.secondaryToolbarButtonIcon,
style: styles.secondaryToolbarButton,
underlayColor: 'transparent'
};
/** /**
* The type of {@link Toolbox}'s React {@code Component} props. * The type of {@link Toolbox}'s React {@code Component} props.
*/ */
type Props = { type Props = {
/**
* Flag showing that audio is muted.
*/
_audioMuted: boolean,
/**
* Flag showing whether the audio-only mode is in use.
*/
_audioOnly: boolean,
/** /**
* The indicator which determines whether the toolbox is enabled. * The indicator which determines whether the toolbox is enabled.
*/ */
_enabled: boolean, _enabled: boolean,
/**
* Flag showing whether room is locked.
*/
_locked: boolean,
/**
* Handler for hangup.
*/
_onHangup: Function,
/**
* Sets the lock i.e. password protection of the conference/room.
*/
_onRoomLock: Function,
/**
* Toggles the audio-only flag of the conference.
*/
_onToggleAudioOnly: Function,
/**
* Switches between the front/user-facing and back/environment-facing
* cameras.
*/
_onToggleCameraFacingMode: Function,
/**
* Flag showing whether video is muted.
*/
_videoMuted: boolean,
/** /**
* Flag showing whether toolbar is visible. * Flag showing whether toolbar is visible.
*/ */
_visible: boolean, _visible: boolean
dispatch: Function
}; };
/** /**
@ -120,36 +104,6 @@ class Toolbox extends Component<Props> {
); );
} }
/**
* Gets the styles for a button that toggles the mute state of a specific
* media type.
*
* @param {string} mediaType - The {@link MEDIA_TYPE} associated with the
* button to get styles for.
* @protected
* @returns {{
* iconStyle: Object,
* style: Object
* }}
*/
_getMuteButtonStyles(mediaType) {
let iconStyle;
let style;
if (this.props[`_${mediaType}Muted`]) {
iconStyle = styles.whitePrimaryToolbarButtonIcon;
style = styles.whitePrimaryToolbarButton;
} else {
iconStyle = styles.primaryToolbarButtonIcon;
style = styles.primaryToolbarButton;
}
return {
iconStyle,
style
};
}
/** /**
* Renders the toolbar which contains the primary buttons such as hangup, * Renders the toolbar which contains the primary buttons such as hangup,
* audio and video mute. * audio and video mute.
@ -158,28 +112,20 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
_renderPrimaryToolbar() { _renderPrimaryToolbar() {
const audioButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.AUDIO);
const videoButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.VIDEO);
const hangupButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.hangup,
underlayColor: ColorPalette.buttonUnderlay
};
/* eslint-disable react/jsx-handler-names */
return ( return (
<View <View
key = 'primaryToolbar' key = 'primaryToolbar'
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { styles.primaryToolbar }> style = { styles.primaryToolbar }>
<AudioMuteButton styles = { audioButtonStyles } /> <AudioMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
<HangupButton styles = { hangupButtonStyles } /> <HangupButton styles = { hangupButtonStyles } />
<VideoMuteButton styles = { videoButtonStyles } /> <VideoMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
</View> </View>
); );
/* eslint-enable react/jsx-handler-names */
} }
/** /**
@ -190,62 +136,20 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
_renderSecondaryToolbar() { _renderSecondaryToolbar() {
const iconStyle = styles.secondaryToolbarButtonIcon;
const style = styles.secondaryToolbarButton;
const underlayColor = 'transparent';
const {
_audioOnly: audioOnly,
_videoMuted: videoMuted
} = this.props;
/* eslint-disable react/jsx-curly-spacing,react/jsx-handler-names */
return ( return (
<View <View
key = 'secondaryToolbar' key = 'secondaryToolbar'
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { styles.secondaryToolbar }> style = { styles.secondaryToolbar }>
{ <AudioRouteButton styles = { secondaryToolbarButtonStyles } />
AudioRouteButton <ToggleCameraButton styles = { secondaryToolbarButtonStyles } />
&& <AudioRouteButton <AudioOnlyButton styles = { secondaryToolbarButtonStyles } />
iconName = { 'volume' } <RoomLockButton styles = { secondaryToolbarButtonStyles } />
iconStyle = { iconStyle } <InviteButton styles = { secondaryToolbarButtonStyles } />
style = { style } <PictureInPictureButton
underlayColor = { underlayColor } /> styles = { secondaryToolbarButtonStyles } />
}
<ToolbarButton
disabled = { audioOnly || videoMuted }
iconName = 'switch-camera'
iconStyle = { iconStyle }
onClick = { this.props._onToggleCameraFacingMode }
style = { style }
underlayColor = { underlayColor } />
<ToolbarButton
iconName = { audioOnly ? 'visibility-off' : 'visibility' }
iconStyle = { iconStyle }
onClick = { this.props._onToggleAudioOnly }
style = { style }
underlayColor = { underlayColor } />
<ToolbarButton
iconName = {
this.props._locked ? 'security-locked' : 'security'
}
iconStyle = { iconStyle }
onClick = { this.props._onRoomLock }
style = { style }
underlayColor = { underlayColor } />
<InviteButton
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
<EnterPictureInPictureToolbarButton
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
</View> </View>
); );
/* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */
} }
/** /**
@ -263,84 +167,21 @@ class Toolbox extends Component<Props> {
} }
/** /**
* Maps redux actions to {@link Toolbox}'s React {@code Component} props. * Maps parts of the redux state to {@link Toolbox} (React {@code Component})
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @private
* @returns {{
* _onRoomLock: Function,
* _onToggleAudioOnly: Function,
* _onToggleCameraFacingMode: Function,
* }}
*/
function _mapDispatchToProps(dispatch) {
return {
...abstractMapDispatchToProps(dispatch),
/**
* Sets the lock i.e. password protection of the conference/room.
*
* @private
* @returns {void}
* @type {Function}
*/
_onRoomLock() {
dispatch(beginRoomLockRequest());
},
/**
* Toggles the audio-only flag of the conference.
*
* @private
* @returns {void}
* @type {Function}
*/
_onToggleAudioOnly() {
dispatch(toggleAudioOnly());
},
/**
* Switches between the front/user-facing and back/environment-facing
* cameras.
*
* @private
* @returns {void}
* @type {Function}
*/
_onToggleCameraFacingMode() {
dispatch(toggleCameraFacingMode());
}
};
}
/**
* Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component}
* props. * props.
* *
* @param {Object} state - The redux store/state. * @param {Object} state - The redux state of which parts are to be mapped to
* @private * {@code Toolbox} props.
* @protected
* @returns {{ * @returns {{
* _audioOnly: boolean,
* _enabled: boolean, * _enabled: boolean,
* _locked: boolean * _visible: boolean
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state: Object): Object {
const conference = state['features/base/conference']; const { enabled, visible } = state['features/toolbox'];
const { enabled } = state['features/toolbox'];
return { return {
...abstractMapStateToProps(state),
/**
* The indicator which determines whether the conference is in
* audio-only mode.
*
* @protected
* @type {boolean}
*/
_audioOnly: Boolean(conference.audioOnly),
/** /**
* The indicator which determines whether the toolbox is enabled. * The indicator which determines whether the toolbox is enabled.
* *
@ -350,15 +191,13 @@ function _mapStateToProps(state) {
_enabled: enabled, _enabled: enabled,
/** /**
* The indicator which determines whether the conference is * Flag showing whether toolbox is visible.
* locked/password-protected.
* *
* @protected * @protected
* @type {boolean} * @type {boolean}
*/ */
_locked: Boolean(conference.locked) _visible: visible
}; };
} }
export default connect(_mapStateToProps, _mapDispatchToProps)( export default connect(_mapStateToProps)(makeAspectRatioAware(Toolbox));
makeAspectRatioAware(Toolbox));

View File

@ -37,7 +37,7 @@ export type Props = {
/** /**
* An abstract implementation of a button. * An abstract implementation of a button.
*/ */
export default class AbstractButton<P: Props, S : *> extends Component<P, S> { export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
static defaultProps = { static defaultProps = {
showLabel: false, showLabel: false,
styles: undefined, styles: undefined,
@ -173,9 +173,9 @@ export default class AbstractButton<P: Props, S : *> extends Component<P, S> {
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
* @inheritdoc * @inheritdoc
* @returns {ReactElement} * @returns {React$Node}
*/ */
render() { render(): React$Node {
const props = { const props = {
...this.props, ...this.props,
accessibilityLabel: this.accessibilityLabel, accessibilityLabel: this.accessibilityLabel,

View File

@ -0,0 +1,84 @@
// @flow
import { connect } from 'react-redux';
import { toggleAudioOnly } from '../../../../base/conference';
import { translate } from '../../../../base/i18n';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
type Props = AbstractButtonProps & {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* An implementation of a button for toggling the audio-only mode.
*/
class AudioOnlyButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Audio only mode';
iconName = 'visibility';
label = 'toolbar.audioonly';
toggledIconName = 'visibility-off';
/**
* Handles clicking / pressing the button.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleAudioOnly());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @private
* @returns {boolean}
*/
_isToggled() {
return this.props._audioOnly;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioOnlyButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference'];
return {
_audioOnly: Boolean(audioOnly)
};
}
export default translate(connect(_mapStateToProps)(AudioOnlyButton));

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React, { Component } from 'react'; import React from 'react';
import { import {
findNodeHandle, findNodeHandle,
NativeModules, NativeModules,
@ -9,10 +9,13 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openDialog } from '../../base/dialog'; import { openDialog } from '../../../../base/dialog';
import { AudioRoutePickerDialog } from '../../mobile/audio-mode'; import { translate } from '../../../../base/i18n';
import { AudioRoutePickerDialog } from '../../../../mobile/audio-mode';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
import ToolbarButton from './ToolbarButton';
/** /**
* The {@code MPVolumeView} React {@code Component}. It will only be available * The {@code MPVolumeView} React {@code Component}. It will only be available
@ -28,48 +31,32 @@ const MPVolumeView
*/ */
const HIDE_VIEW_STYLE = { display: 'none' }; const HIDE_VIEW_STYLE = { display: 'none' };
type Props = { type Props = AbstractButtonProps & {
/** /**
* The redux {@code dispatch} function used to open/show the * The redux {@code dispatch} function used to open/show the
* {@code AudioRoutePickerDialog}. * {@code AudioRoutePickerDialog}.
*/ */
dispatch: Function, dispatch: Function
/**
* The name of the Icon of this {@code AudioRouteButton}.
*/
iconName: string,
/**
* The style of the Icon of this {@code AudioRouteButton}.
*/
iconStyle: Object,
/**
* The style(s) of {@code AudioRouteButton}.
*/
style: Array<*> | Object,
/**
* The color underlaying the button.
*/
underlayColor: string
}; };
/** /**
* A toolbar button which triggers an audio route picker when pressed. * A toolbar button which triggers an audio route picker when pressed.
*/ */
class AudioRouteButton extends Component<Props> { class AudioRouteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Audio route';
iconName = 'icon-volume';
label = 'toolbar.audioRoute';
_volumeComponent: ?Object; _volumeComponent: ?Object;
/** /**
* Initializes a new {@code AudioRouteButton} instance. * Initializes a new {@code AudioRouteButton} instance.
* *
* @param {Object} props - The React {@code Component} props to initialize * @param {Props} props - The React {@code Component} props to initialize
* the new {@code AudioRouteButton} instance with. * the new {@code AudioRouteButton} instance with.
*/ */
constructor(props) { constructor(props: Props) {
super(props); super(props);
/** /**
@ -77,25 +64,21 @@ class AudioRouteButton extends Component<Props> {
* showing the volume control view. * showing the volume control view.
* *
* @private * @private
* @type {ReactComponent} * @type {ReactElement}
*/ */
this._volumeComponent = null; this._volumeComponent = null;
// Bind event handlers so they are only bound once per instance. // Bind event handlers so they are only bound once per instance.
this._onClick = this._onClick.bind(this);
this._setVolumeComponent = this._setVolumeComponent.bind(this); this._setVolumeComponent = this._setVolumeComponent.bind(this);
} }
_onClick: () => void;
/** /**
* Handles clicking/pressing this {@code AudioRouteButton} by showing an * Handles clicking / pressing the button, and opens the appropriate dialog.
* audio route picker.
* *
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onClick() { _handleClick() {
if (MPVolumeView) { if (MPVolumeView) {
NativeModules.MPVolumeViewManager.show( NativeModules.MPVolumeViewManager.show(
findNodeHandle(this._volumeComponent)); findNodeHandle(this._volumeComponent));
@ -104,23 +87,49 @@ class AudioRouteButton extends Component<Props> {
} }
} }
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
_setVolumeComponent: (?Object) => void;
/**
* Sets the internal reference to the React Component wrapping the
* {@code MPVolumeView} component.
*
* @param {ReactElement} component - React Component.
* @private
* @returns {void}
*/
_setVolumeComponent(component) {
this._volumeComponent = component;
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
* @inheritdoc * @inheritdoc
* @returns {ReactElement} * @returns {?ReactElement}
*/ */
render() { render() {
const { iconName, iconStyle, style, underlayColor } = this.props; if (!MPVolumeView && !AudioRoutePickerDialog) {
// $FlowFixMe
return null;
}
const element = super.render();
return ( return (
<View> <View>
<ToolbarButton { element }
iconName = { iconName }
iconStyle = { iconStyle }
onClick = { this._onClick }
style = { style }
underlayColor = { underlayColor } />
{ {
MPVolumeView MPVolumeView
&& <MPVolumeView && <MPVolumeView
@ -130,21 +139,6 @@ class AudioRouteButton extends Component<Props> {
</View> </View>
); );
} }
_setVolumeComponent: (?Object) => void;
/**
* Sets the internal reference to the React Component wrapping the
* {@code MPVolumeView} component.
*
* @param {ReactComponent} component - React Component.
* @private
* @returns {void}
*/
_setVolumeComponent(component) {
this._volumeComponent = component;
}
} }
export default (MPVolumeView || AudioRoutePickerDialog) export default translate(connect()(AudioRouteButton));
&& connect()(AudioRouteButton);

View File

@ -1,29 +1,18 @@
// @flow // @flow
import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { beginShareRoom } from '../../share-room'; import {
import { ToolbarButton } from '../../toolbox'; beginAddPeople,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../../../invite';
import { beginShareRoom } from '../../../../share-room';
import { beginAddPeople } from '../actions'; import AbstractButton from '../AbstractButton';
import { isAddPeopleEnabled, isDialOutEnabled } from '../functions'; import type { Props as AbstractButtonProps } from '../AbstractButton';
/** type Props = AbstractButtonProps & {
* The indicator which determines (at bundle time) whether there should be a
* {@code ToolbarButton} in {@code Toolbox} to expose the functionality of the
* feature share-room in the user interface of the app.
*
* @private
* @type {boolean}
*/
const _SHARE_ROOM_TOOLBAR_BUTTON = true;
/**
* The type of {@link EnterPictureInPictureToobarButton}'s React
* {@code Component} props.
*/
type Props = {
/** /**
* Whether or not the feature to directly invite people into the * Whether or not the feature to directly invite people into the
@ -50,45 +39,71 @@ type Props = {
_onShareRoom: Function _onShareRoom: Function
}; };
/**
* The indicator which determines (at bundle time) whether there should be a
* button in {@code Toolbox} to expose the functionality of the feature
* share-room in the user interface of the app.
*
* @private
* @type {boolean}
*/
const _SHARE_ROOM_TOOLBAR_BUTTON = true;
/** /**
* Implements a {@link ToolbarButton} to enter Picture-in-Picture. * Implements a {@link ToolbarButton} to enter Picture-in-Picture.
*/ */
class InviteButton extends Component<Props> { class InviteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Share room';
iconName = 'icon-link';
label = 'toolbar.shareRoom';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const {
_addPeopleEnabled,
_dialOutEnabled,
_onAddPeople,
_onShareRoom
} = this.props;
if (_addPeopleEnabled || _dialOutEnabled) {
_onAddPeople();
} else if (_SHARE_ROOM_TOOLBAR_BUTTON) {
_onShareRoom();
}
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
* @inheritdoc * @inheritdoc
* @returns {ReactElement} * @returns {React$Node}
*/ */
render() { render() {
const { const { _addPeopleEnabled, _dialOutEnabled } = this.props;
_addPeopleEnabled,
_dialOutEnabled,
_onAddPeople,
_onShareRoom,
...props
} = this.props;
if (_addPeopleEnabled || _dialOutEnabled) { return (
return ( _SHARE_ROOM_TOOLBAR_BUTTON
<ToolbarButton || _addPeopleEnabled
iconName = { 'link' } || _dialOutEnabled
onClick = { _onAddPeople } ? super.render()
{ ...props } /> : null);
);
}
if (_SHARE_ROOM_TOOLBAR_BUTTON) {
return (
<ToolbarButton
iconName = 'link'
onClick = { _onShareRoom }
{ ...props } />
);
}
return null;
} }
} }

View File

@ -0,0 +1,87 @@
// @flow
import { connect } from 'react-redux';
import { getAppProp } from '../../../../app';
import { translate } from '../../../../base/i18n';
import { enterPictureInPicture } from '../../../../mobile/picture-in-picture';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
type Props = AbstractButtonProps & {
/**
* Whether Picture-in-Picture is enabled or not.
*/
_enabled: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* An implementation of a button for entering Picture-in-Picture mode.
*/
class PictureInPictureButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Picture in picture';
iconName = 'icon-menu-down';
label = 'toolbar.pip';
/**
* Handles clicking / pressing the button.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(enterPictureInPicture());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {?ReactElement}
*/
render() {
if (!this.props._enabled) {
// $FlowFixMe
return null;
}
return super.render();
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code PictureInPictureButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _enabled: boolean
* }}
*/
function _mapStateToProps(state): Object {
return {
_enabled: Boolean(getAppProp(state, 'pictureInPictureEnabled'))
};
}
export default translate(connect(_mapStateToProps)(PictureInPictureButton));

View File

@ -0,0 +1,90 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { beginRoomLockRequest } from '../../../../room-lock';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
type Props = AbstractButtonProps & {
/**
* The current conference.
*/
_conference: Object,
/**
* Whether the current conference is locked or not.
*/
_locked: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* An implementation of a button for locking / unlocking a room.
*/
class RoomLockButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Room lock';
iconName = 'security';
label = 'toolbar.lock';
toggledIconName = 'security-locked';
/**
* Handles clicking / pressing the button.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(beginRoomLockRequest());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return !this.props._conference;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @private
* @returns {boolean}
*/
_isToggled() {
return this.props._locked;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code RoomLockButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state): Object {
const { conference, locked } = state['features/base/conference'];
return {
_conference: conference,
_locked: Boolean(conference && locked)
};
}
export default translate(connect(_mapStateToProps)(RoomLockButton));

View File

@ -0,0 +1,50 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { beginShareRoom } from '../../../../share-room';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* An implementation of a button for sharing a room using the native OS sharing
* capabilities.
*/
class ShareRoomButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Share room';
iconName = 'icon-link';
label = 'toolbar.shareRoom';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(beginShareRoom());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return false;
}
}
export default translate(connect()(ShareRoomButton));

View File

@ -0,0 +1,81 @@
// @flow
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n';
import { MEDIA_TYPE, toggleCameraFacingMode } from '../../../../base/media';
import { isLocalTrackMuted } from '../../../../base/tracks';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
type Props = AbstractButtonProps & {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean,
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Share room';
iconName = 'icon-switch-camera';
label = 'toolbar.switchCamera';
/**
* Handles clicking / pressing the button.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleCameraFacingMode());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @private
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state): Object {
const { audioOnly } = state['features/base/conference'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(ToggleCameraButton));

View File

@ -0,0 +1,6 @@
export { default as AudioOnlyButton } from './AudioOnlyButton';
export { default as AudioRouteButton } from './AudioRouteButton';
export { default as InviteButton } from './InviteButton';
export { default as PictureInPictureButton } from './PictureInPictureButton';
export { default as RoomLockButton } from './RoomLockButton';
export { default as ToggleCameraButton } from './ToggleCameraButton';

View File

@ -80,6 +80,15 @@ export default createStyleSheet({
backgroundColor: ColorPalette.red backgroundColor: ColorPalette.red
}, },
/**
* The icon style of toolbar buttons in {@link #primaryToolbar} which
* hangs the current conference up.
*/
hangupButtonIcon: {
...primaryToolbarButtonIcon,
color: ColorPalette.white
},
/** /**
* The style of the toolbar which contains the primary buttons such as * The style of the toolbar which contains the primary buttons such as
* hangup, audio and video mute. * hangup, audio and video mute.

View File

@ -1,102 +0,0 @@
// @flow
import { appNavigate } from '../app';
import { MEDIA_TYPE } from '../base/media';
import { isLocalTrackMuted } from '../base/tracks';
import type { Dispatch } from 'redux';
/**
* Maps redux actions to {@link Toolbox} (React {@code Component}) props.
*
* @param {Function} dispatch - The redux {@code dispatch} function.
* @private
* @returns {{
* _onHangup: Function,
* _onToggleAudio: Function,
* _onToggleVideo: Function
* }}
*/
export function abstractMapDispatchToProps(dispatch: Dispatch<*>): Object {
return {
// Inject {@code dispatch} into the React Component's props in case it
// needs to dispatch an action in the redux store without
// {@code mapDispatchToProps}.
dispatch,
/**
* Dispatches action to leave the current conference.
*
* @private
* @returns {void}
* @type {Function}
*/
_onHangup() {
// XXX We don't know here which value is effectively/internally
// used when there's no valid room name to join. It isn't our
// business to know that anyway. The undefined value is our
// expression of (1) the lack of knowledge & (2) the desire to no
// longer have a valid room name to join.
dispatch(appNavigate(undefined));
}
};
}
/**
* Maps parts of the redux state to {@link Toolbox} (React {@code Component})
* props.
*
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @protected
* @returns {{
* _audioMuted: boolean,
* _videoMuted: boolean,
* _visible: boolean
* }}
*/
export function abstractMapStateToProps(state: Object): Object {
const tracks = state['features/base/tracks'];
const { visible } = state['features/toolbox'];
return {
/**
* Flag showing whether audio is muted.
*
* @protected
* @type {boolean}
*/
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
/**
* Flag showing whether video is muted.
*
* @protected
* @type {boolean}
*/
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO),
/**
* Flag showing whether toolbox is visible.
*
* @protected
* @type {boolean}
*/
_visible: visible
};
}
/**
* Returns the button object corresponding to a specific {@code buttonName}.
*
* @param {string} buttonName - The name of the button.
* @param {Object} state - The current state.
* @returns {Object} - The button object.
*/
export function getButton(buttonName: string, state: Object) {
const { primaryToolbarButtons, secondaryToolbarButtons }
= state['features/toolbox'];
return primaryToolbarButtons.get(buttonName)
|| secondaryToolbarButtons.get(buttonName);
}

View File

@ -2,12 +2,6 @@
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
export {
abstractMapDispatchToProps,
abstractMapStateToProps,
getButton
} from './functions.native';
/** /**
* Helper for getting the height of the toolbox. * Helper for getting the height of the toolbox.
* *