From a2834a2495fe59288285524099bda06d0428fcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 18 Apr 2018 16:34:40 +0200 Subject: [PATCH] [RN] Refactor Toolbox Create standalone components for each feature and move all state to them. Toolbars are now dummy containers. --- lang/main.json | 3 + .../invite/components/InviteButton.web.js | 0 react/features/invite/components/index.js | 1 - .../EnterPictureInPictureToolbarButton.js | 111 ------- .../picture-in-picture/components/index.js | 2 - .../mobile/picture-in-picture/index.js | 1 - .../toolbox/components/Toolbox.native.js | 299 ++++-------------- .../components/buttons/AbstractButton.js | 6 +- .../buttons/native/AudioOnlyButton.js | 84 +++++ .../{ => buttons/native}/AudioRouteButton.js | 114 ++++--- .../buttons/native/InviteButton.js} | 111 ++++--- .../buttons/native/PictureInPictureButton.js | 87 +++++ .../buttons/native/RoomLockButton.js | 90 ++++++ .../buttons/native/ShareRoomButton.js | 50 +++ .../buttons/native/ToggleCameraButton.js | 81 +++++ .../components/buttons/native/index.js | 6 + react/features/toolbox/components/styles.js | 9 + react/features/toolbox/functions.native.js | 102 ------ react/features/toolbox/functions.web.js | 6 - 19 files changed, 599 insertions(+), 564 deletions(-) delete mode 100644 react/features/invite/components/InviteButton.web.js delete mode 100644 react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js delete mode 100644 react/features/mobile/picture-in-picture/components/index.js create mode 100644 react/features/toolbox/components/buttons/native/AudioOnlyButton.js rename react/features/toolbox/components/{ => buttons/native}/AudioRouteButton.js (61%) rename react/features/{invite/components/InviteButton.native.js => toolbox/components/buttons/native/InviteButton.js} (66%) create mode 100644 react/features/toolbox/components/buttons/native/PictureInPictureButton.js create mode 100644 react/features/toolbox/components/buttons/native/RoomLockButton.js create mode 100644 react/features/toolbox/components/buttons/native/ShareRoomButton.js create mode 100644 react/features/toolbox/components/buttons/native/ToggleCameraButton.js diff --git a/lang/main.json b/lang/main.json index a99eaacef..1aea9ca69 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/react/features/invite/components/InviteButton.web.js b/react/features/invite/components/InviteButton.web.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/invite/components/index.js b/react/features/invite/components/index.js index e4c747f0b..e32c31d96 100644 --- a/react/features/invite/components/index.js +++ b/react/features/invite/components/index.js @@ -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'; diff --git a/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js b/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js deleted file mode 100644 index b86dbae07..000000000 --- a/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js +++ /dev/null @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { - _onEnterPictureInPicture, - _pictureInPictureEnabled, - ...props - } = this.props; - - if (!_pictureInPictureEnabled) { - return null; - } - - return ( - - ); - } -} - -/** - * 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); diff --git a/react/features/mobile/picture-in-picture/components/index.js b/react/features/mobile/picture-in-picture/components/index.js deleted file mode 100644 index 963025b05..000000000 --- a/react/features/mobile/picture-in-picture/components/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as EnterPictureInPictureToolbarButton } - from './EnterPictureInPictureToolbarButton'; diff --git a/react/features/mobile/picture-in-picture/index.js b/react/features/mobile/picture-in-picture/index.js index 803dacd06..e40f04336 100644 --- a/react/features/mobile/picture-in-picture/index.js +++ b/react/features/mobile/picture-in-picture/index.js @@ -1,3 +1,2 @@ export * from './actions'; export * from './actionTypes'; -export * from './components'; diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index 4acc02101..eaa49910d 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -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 { ); } - /** - * 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 { * @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 ( - + - + ); - - /* eslint-enable react/jsx-handler-names */ } /** @@ -190,62 +136,20 @@ class Toolbox extends Component { * @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 ( - { - AudioRouteButton - && - } - - - - - + + + + + + ); - - /* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */ } /** @@ -263,84 +167,21 @@ class Toolbox extends Component { } /** - * 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)); diff --git a/react/features/toolbox/components/buttons/AbstractButton.js b/react/features/toolbox/components/buttons/AbstractButton.js index c068b463d..f549d6c52 100644 --- a/react/features/toolbox/components/buttons/AbstractButton.js +++ b/react/features/toolbox/components/buttons/AbstractButton.js @@ -37,7 +37,7 @@ export type Props = { /** * An abstract implementation of a button. */ -export default class AbstractButton extends Component { +export default class AbstractButton extends Component { static defaultProps = { showLabel: false, styles: undefined, @@ -173,9 +173,9 @@ export default class AbstractButton extends Component { * Implements React's {@link Component#render()}. * * @inheritdoc - * @returns {ReactElement} + * @returns {React$Node} */ - render() { + render(): React$Node { const props = { ...this.props, accessibilityLabel: this.accessibilityLabel, diff --git a/react/features/toolbox/components/buttons/native/AudioOnlyButton.js b/react/features/toolbox/components/buttons/native/AudioOnlyButton.js new file mode 100644 index 000000000..9107c41f3 --- /dev/null +++ b/react/features/toolbox/components/buttons/native/AudioOnlyButton.js @@ -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 { + 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)); diff --git a/react/features/toolbox/components/AudioRouteButton.js b/react/features/toolbox/components/buttons/native/AudioRouteButton.js similarity index 61% rename from react/features/toolbox/components/AudioRouteButton.js rename to react/features/toolbox/components/buttons/native/AudioRouteButton.js index 5598e488c..919b49011 100644 --- a/react/features/toolbox/components/AudioRouteButton.js +++ b/react/features/toolbox/components/buttons/native/AudioRouteButton.js @@ -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 { +class AudioRouteButton extends AbstractButton { + 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 { * 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 { } } + /** + * 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 ( - + { element } { MPVolumeView && { ); } - - _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)); diff --git a/react/features/invite/components/InviteButton.native.js b/react/features/toolbox/components/buttons/native/InviteButton.js similarity index 66% rename from react/features/invite/components/InviteButton.native.js rename to react/features/toolbox/components/buttons/native/InviteButton.js index 6bf903084..b06729285 100644 --- a/react/features/invite/components/InviteButton.native.js +++ b/react/features/toolbox/components/buttons/native/InviteButton.js @@ -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 { +class InviteButton extends AbstractButton { + 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 ( - - ); - } - - if (_SHARE_ROOM_TOOLBAR_BUTTON) { - return ( - - ); - } - - return null; + return ( + _SHARE_ROOM_TOOLBAR_BUTTON + || _addPeopleEnabled + || _dialOutEnabled + ? super.render() + : null); } } diff --git a/react/features/toolbox/components/buttons/native/PictureInPictureButton.js b/react/features/toolbox/components/buttons/native/PictureInPictureButton.js new file mode 100644 index 000000000..2d14157bc --- /dev/null +++ b/react/features/toolbox/components/buttons/native/PictureInPictureButton.js @@ -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 { + 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)); diff --git a/react/features/toolbox/components/buttons/native/RoomLockButton.js b/react/features/toolbox/components/buttons/native/RoomLockButton.js new file mode 100644 index 000000000..b1baf79ec --- /dev/null +++ b/react/features/toolbox/components/buttons/native/RoomLockButton.js @@ -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 { + 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)); diff --git a/react/features/toolbox/components/buttons/native/ShareRoomButton.js b/react/features/toolbox/components/buttons/native/ShareRoomButton.js new file mode 100644 index 000000000..8e00fdb42 --- /dev/null +++ b/react/features/toolbox/components/buttons/native/ShareRoomButton.js @@ -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 { + 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)); diff --git a/react/features/toolbox/components/buttons/native/ToggleCameraButton.js b/react/features/toolbox/components/buttons/native/ToggleCameraButton.js new file mode 100644 index 000000000..fa696c09a --- /dev/null +++ b/react/features/toolbox/components/buttons/native/ToggleCameraButton.js @@ -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 { + 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)); diff --git a/react/features/toolbox/components/buttons/native/index.js b/react/features/toolbox/components/buttons/native/index.js index e69de29bb..d25ed050b 100644 --- a/react/features/toolbox/components/buttons/native/index.js +++ b/react/features/toolbox/components/buttons/native/index.js @@ -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'; diff --git a/react/features/toolbox/components/styles.js b/react/features/toolbox/components/styles.js index 0b42f4c61..c7e7a9d23 100644 --- a/react/features/toolbox/components/styles.js +++ b/react/features/toolbox/components/styles.js @@ -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. diff --git a/react/features/toolbox/functions.native.js b/react/features/toolbox/functions.native.js index da0a6ad3c..e69de29bb 100644 --- a/react/features/toolbox/functions.native.js +++ b/react/features/toolbox/functions.native.js @@ -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); -} diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 0c5c31a67..fe15a2baa 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -2,12 +2,6 @@ declare var interfaceConfig: Object; -export { - abstractMapDispatchToProps, - abstractMapStateToProps, - getButton -} from './functions.native'; - /** * Helper for getting the height of the toolbox. *