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

View File

@ -1,4 +1,3 @@
export { default as AddPeopleDialog } from './AddPeopleDialog';
export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';
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 './actionTypes';
export * from './components';

View File

@ -4,91 +4,75 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import { toggleAudioOnly } from '../../base/conference';
import {
MEDIA_TYPE,
toggleCameraFacingMode
} from '../../base/media';
import { Container } from '../../base/react';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../base/responsive-ui';
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 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.
*/
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.
*/
_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.
*/
_visible: boolean,
dispatch: Function
_visible: boolean
};
/**
@ -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,
* audio and video mute.
@ -158,28 +112,20 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement}
*/
_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 (
<View
key = 'primaryToolbar'
pointerEvents = 'box-none'
style = { styles.primaryToolbar }>
<AudioMuteButton styles = { audioButtonStyles } />
<AudioMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
<HangupButton styles = { hangupButtonStyles } />
<VideoMuteButton styles = { videoButtonStyles } />
<VideoMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
</View>
);
/* eslint-enable react/jsx-handler-names */
}
/**
@ -190,62 +136,20 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement}
*/
_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 (
<View
key = 'secondaryToolbar'
pointerEvents = 'box-none'
style = { styles.secondaryToolbar }>
{
AudioRouteButton
&& <AudioRouteButton
iconName = { 'volume' }
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
}
<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 } />
<AudioRouteButton styles = { secondaryToolbarButtonStyles } />
<ToggleCameraButton styles = { secondaryToolbarButtonStyles } />
<AudioOnlyButton styles = { secondaryToolbarButtonStyles } />
<RoomLockButton styles = { secondaryToolbarButtonStyles } />
<InviteButton styles = { secondaryToolbarButtonStyles } />
<PictureInPictureButton
styles = { secondaryToolbarButtonStyles } />
</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.
*
* @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}
* Maps parts of the redux state to {@link Toolbox} (React {@code Component})
* props.
*
* @param {Object} state - The redux store/state.
* @private
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @protected
* @returns {{
* _audioOnly: boolean,
* _enabled: boolean,
* _locked: boolean
* _visible: boolean
* }}
*/
function _mapStateToProps(state) {
const conference = state['features/base/conference'];
const { enabled } = state['features/toolbox'];
function _mapStateToProps(state: Object): Object {
const { enabled, visible } = state['features/toolbox'];
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.
*
@ -350,15 +191,13 @@ function _mapStateToProps(state) {
_enabled: enabled,
/**
* The indicator which determines whether the conference is
* locked/password-protected.
* Flag showing whether toolbox is visible.
*
* @protected
* @type {boolean}
*/
_locked: Boolean(conference.locked)
_visible: visible
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(
makeAspectRatioAware(Toolbox));
export default connect(_mapStateToProps)(makeAspectRatioAware(Toolbox));

View File

@ -37,7 +37,7 @@ export type Props = {
/**
* 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 = {
showLabel: false,
styles: undefined,
@ -173,9 +173,9 @@ export default class AbstractButton<P: Props, S : *> extends Component<P, S> {
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
* @returns {React$Node}
*/
render() {
render(): React$Node {
const props = {
...this.props,
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
import React, { Component } from 'react';
import React from 'react';
import {
findNodeHandle,
NativeModules,
@ -9,10 +9,13 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { openDialog } from '../../base/dialog';
import { AudioRoutePickerDialog } from '../../mobile/audio-mode';
import { openDialog } from '../../../../base/dialog';
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
@ -28,48 +31,32 @@ const MPVolumeView
*/
const HIDE_VIEW_STYLE = { display: 'none' };
type Props = {
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function used to open/show the
* {@code AudioRoutePickerDialog}.
*/
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
dispatch: Function
};
/**
* 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;
/**
* 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.
*/
constructor(props) {
constructor(props: Props) {
super(props);
/**
@ -77,25 +64,21 @@ class AudioRouteButton extends Component<Props> {
* showing the volume control view.
*
* @private
* @type {ReactComponent}
* @type {ReactElement}
*/
this._volumeComponent = null;
// Bind event handlers so they are only bound once per instance.
this._onClick = this._onClick.bind(this);
this._setVolumeComponent = this._setVolumeComponent.bind(this);
}
_onClick: () => void;
/**
* Handles clicking/pressing this {@code AudioRouteButton} by showing an
* audio route picker.
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_onClick() {
_handleClick() {
if (MPVolumeView) {
NativeModules.MPVolumeViewManager.show(
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()}.
*
* @inheritdoc
* @returns {ReactElement}
* @returns {?ReactElement}
*/
render() {
const { iconName, iconStyle, style, underlayColor } = this.props;
if (!MPVolumeView && !AudioRoutePickerDialog) {
// $FlowFixMe
return null;
}
const element = super.render();
return (
<View>
<ToolbarButton
iconName = { iconName }
iconStyle = { iconStyle }
onClick = { this._onClick }
style = { style }
underlayColor = { underlayColor } />
{ element }
{
MPVolumeView
&& <MPVolumeView
@ -130,21 +139,6 @@ class AudioRouteButton extends Component<Props> {
</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)
&& connect()(AudioRouteButton);
export default translate(connect()(AudioRouteButton));

View File

@ -1,29 +1,18 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { beginShareRoom } from '../../share-room';
import { ToolbarButton } from '../../toolbox';
import {
beginAddPeople,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../../../invite';
import { beginShareRoom } from '../../../../share-room';
import { beginAddPeople } from '../actions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../functions';
import AbstractButton from '../AbstractButton';
import type { Props as AbstractButtonProps } from '../AbstractButton';
/**
* 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 = {
type Props = AbstractButtonProps & {
/**
* Whether or not the feature to directly invite people into the
@ -50,45 +39,71 @@ type Props = {
_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.
*/
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()}.
*
* @inheritdoc
* @returns {ReactElement}
* @returns {React$Node}
*/
render() {
const {
_addPeopleEnabled,
_dialOutEnabled,
_onAddPeople,
_onShareRoom,
...props
} = this.props;
const { _addPeopleEnabled, _dialOutEnabled } = this.props;
if (_addPeopleEnabled || _dialOutEnabled) {
return (
<ToolbarButton
iconName = { 'link' }
onClick = { _onAddPeople }
{ ...props } />
);
}
if (_SHARE_ROOM_TOOLBAR_BUTTON) {
return (
<ToolbarButton
iconName = 'link'
onClick = { _onShareRoom }
{ ...props } />
);
}
return null;
return (
_SHARE_ROOM_TOOLBAR_BUTTON
|| _addPeopleEnabled
|| _dialOutEnabled
? super.render()
: 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
},
/**
* 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
* 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;
export {
abstractMapDispatchToProps,
abstractMapStateToProps,
getButton
} from './functions.native';
/**
* Helper for getting the height of the toolbox.
*