diff --git a/flow-typed/npm/redux_v3.x.x.js b/flow-typed/npm/redux_v3.x.x.js index f4f5e2005..b37dd7056 100644 --- a/flow-typed/npm/redux_v3.x.x.js +++ b/flow-typed/npm/redux_v3.x.x.js @@ -55,4 +55,8 @@ declare module 'redux' { declare function compose(...fns: Array>): Function; + // Utility function in Redux that can be used for function composition + // e.g. bar(foo(baz)) is equivalent to compose(bar, foo)(baz). + declare function compose(...fns: Array): Function; + } diff --git a/react/features/toolbox/actions.native.js b/react/features/toolbox/actions.native.js index f550712d6..c93ad18c0 100644 --- a/react/features/toolbox/actions.native.js +++ b/react/features/toolbox/actions.native.js @@ -15,6 +15,13 @@ import { SET_TOOLBOX_VISIBLE } from './actionTypes'; +/** + * FIXME: We should make sure all common functions for native and web are + * separated in a global functions file, as well as all actions! Currently this + * file contains actions that are imported in actions.web. + */ +import { getButton } from './functions.web'; + /** * Event handler for local raise hand changed event. * @@ -23,10 +30,8 @@ import { */ export function changeLocalRaiseHand(handRaised: boolean): Function { return (dispatch: Dispatch<*>, getState: Function) => { - const state = getState(); - const { secondaryToolbarButtons } = state['features/toolbox']; const buttonName = 'raisehand'; - const button = secondaryToolbarButtons.get(buttonName); + const button = getButton(buttonName, getState()); button.toggled = handRaised; @@ -264,10 +269,8 @@ export function showEtherpadButton(): Function { */ export function toggleFullScreen(isFullScreen: boolean): Function { return (dispatch: Dispatch<*>, getState: Function) => { - const state = getState(); - const { primaryToolbarButtons } = state['features/toolbox']; const buttonName = 'fullscreen'; - const button = primaryToolbarButtons.get(buttonName); + const button = getButton(buttonName, getState()); button.toggled = isFullScreen; @@ -283,14 +286,7 @@ export function toggleFullScreen(isFullScreen: boolean): Function { */ export function toggleToolbarButton(buttonName: string): Function { return (dispatch: Dispatch, getState: Function) => { - const state = getState(); - const { - primaryToolbarButtons, - secondaryToolbarButtons - } = state['features/toolbox']; - const button - = primaryToolbarButtons.get(buttonName) - || secondaryToolbarButtons.get(buttonName); + const button = getButton(buttonName, getState()); dispatch(setToolbarButton(buttonName, { toggled: !button.toggled diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index b7de47faa..26599a92d 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -1,5 +1,7 @@ /* @flow */ +import { compose } from 'redux'; + import Recording from '../../../modules/UI/recording/Recording'; import SideContainerToggler from '../../../modules/UI/side_pannels/SideContainerToggler'; @@ -7,14 +9,17 @@ import UIEvents from '../../../service/UI/UIEvents'; import UIUtil from '../../../modules/UI/util/UIUtil'; import { + changeLocalRaiseHand, clearToolboxTimeout, setSubjectSlideIn, setToolbarButton, setToolboxTimeout, setToolboxTimeoutMS, setToolboxVisible, + toggleFullScreen, toggleToolbarButton } from './actions.native'; + import { SET_DEFAULT_TOOLBOX_BUTTONS } from './actionTypes'; import { getDefaultToolboxButtons } from './functions'; @@ -74,6 +79,87 @@ export function dockToolbox(dock: boolean): Function { }; } +/** + * Returns button on mount/unmount handlers with dispatch function stored in + * closure. + * + * @param {Function} dispatch - Redux action dispatcher. + * @param {Function} getState - The function fetching the Redux state. + * @returns {Object} Button on mount/unmount handlers. + * @private + */ +function _getButtonHandlers(dispatch, getState) { + const { isGuest } = getState()['features/jwt']; + + const localRaiseHandHandler = compose(dispatch, changeLocalRaiseHand); + const toggleFullScreenHandler = compose(dispatch, toggleFullScreen); + + return { + /** + * Mount handler for desktop button. + * + * @type {Object} + */ + desktop: { + onMount: () => dispatch(showDesktopSharingButton()) + }, + + /** + * Mount/Unmount handler for toggling fullscreen button. + * + * @type {Object} + */ + fullscreen: { + onMount: () => + APP.UI.addListener( + UIEvents.FULLSCREEN_TOGGLED, + toggleFullScreenHandler), + onUnmount: () => + APP.UI.removeListener( + UIEvents.FULLSCREEN_TOGGLED, + toggleFullScreenHandler) + }, + + /** + * Mount handler for profile button. + * + * @type {Object} + */ + profile: { + onMount: () => + isGuest + || dispatch(setProfileButtonUnclickable(true)) + }, + + /** + * Mount/Unmount handlers for raisehand button. + * + * @type {button} + */ + raisehand: { + onMount: () => + APP.UI.addListener( + UIEvents.LOCAL_RAISE_HAND_CHANGED, + localRaiseHandHandler), + onUnmount: () => + APP.UI.removeListener( + UIEvents.LOCAL_RAISE_HAND_CHANGED, + localRaiseHandHandler) + }, + + /** + * Mount handler for recording button. + * + * @type {Object} + */ + recording: { + onMount: () => + config.enableRecording + && dispatch(showRecordingButton()) + } + }; +} + /** * Hides the toolbox. * @@ -114,16 +200,18 @@ export function hideToolbox(force: boolean = false): Function { /** * Sets the default toolbar buttons of the Toolbox. * - * @returns {{ - * type: SET_DEFAULT_TOOLBOX_BUTTONS, - * primaryToolbarButtons: Map, - * secondaryToolbarButtons: Map - * }} + * @returns {Function} */ -export function setDefaultToolboxButtons(): Object { - return { - type: SET_DEFAULT_TOOLBOX_BUTTONS, - ...getDefaultToolboxButtons() +export function setDefaultToolboxButtons(): Function { + return (dispatch: Dispatch, getState: Function) => { + // Save dispatch function in closure. + const buttonHandlers = _getButtonHandlers(dispatch, getState); + const toolboxButtons = getDefaultToolboxButtons(buttonHandlers); + + dispatch({ + type: SET_DEFAULT_TOOLBOX_BUTTONS, + ...toolboxButtons + }); }; } diff --git a/react/features/toolbox/components/PrimaryToolbar.web.js b/react/features/toolbox/components/PrimaryToolbar.web.js index 62996d072..161734804 100644 --- a/react/features/toolbox/components/PrimaryToolbar.web.js +++ b/react/features/toolbox/components/PrimaryToolbar.web.js @@ -3,13 +3,9 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import UIEvents from '../../../../service/UI/UIEvents'; - -import { showDesktopSharingButton, toggleFullScreen } from '../actions'; import { getToolbarClassNames } from '../functions'; import Toolbar from './Toolbar'; -declare var APP: Object; declare var interfaceConfig: Object; /** @@ -20,15 +16,6 @@ declare var interfaceConfig: Object; */ class PrimaryToolbar extends Component { static propTypes = { - /** - * Handler for toggling fullscreen mode. - */ - _onFullScreenToggled: React.PropTypes.func, - - /** - * Handler for showing desktop sharing button. - */ - _onShowDesktopSharingButton: React.PropTypes.func, /** * Contains toolbar buttons for primary toolbar. @@ -51,43 +38,10 @@ class PrimaryToolbar extends Component { constructor(props) { super(props); - const buttonHandlers = { - /** - * Mount handler for desktop button. - * - * @type {Object} - */ - desktop: { - onMount: () => this.props._onShowDesktopSharingButton() - }, - - /** - * Mount/Unmount handler for toggling fullscreen button. - * - * @type {Object} - */ - fullscreen: { - onMount: () => - APP.UI.addListener( - UIEvents.FULLSCREEN_TOGGLED, - this.props._onFullScreenToggled), - onUnmount: () => - APP.UI.removeListener( - UIEvents.FULLSCREEN_TOGGLED, - this.props._onFullScreenToggled) - } - }; const splitterIndex = interfaceConfig.MAIN_TOOLBAR_SPLITTER_INDEX; this.state = { - /** - * Object containing on mount/unmount handlers for toolbar buttons. - * - * @type {Object} - */ - buttonHandlers, - /** * If deployment supports toolbar splitter this value contains its * index. @@ -113,14 +67,13 @@ class PrimaryToolbar extends Component { return null; } - const { buttonHandlers, splitterIndex } = this.state; + const { splitterIndex } = this.state; const { primaryToolbarClassName } = getToolbarClassNames(this.props); const tooltipPosition = interfaceConfig.filmStripOnly ? 'left' : 'bottom'; return ( { - const { - _isGuest, - _onSetProfileButtonUnclickable - } = this.props; - - _isGuest || _onSetProfileButtonUnclickable(true); - } - }, - - /** - * Mount/Unmount handlers for raisehand button. - * - * @type {button} - */ - raisehand: { - onMount: () => - APP.UI.addListener( - UIEvents.LOCAL_RAISE_HAND_CHANGED, - this.props._onLocalRaiseHandChanged), - onUnmount: () => - APP.UI.removeListener( - UIEvents.LOCAL_RAISE_HAND_CHANGED, - this.props._onLocalRaiseHandChanged) - }, - - /** - * Mount handler for recording button. - * - * @type {Object} - */ - recording: { - onMount: () => - config.enableRecording - && this.props._onShowRecordingButton() - } - }; - - this.state = { - /** - * Object containing on mount/unmount handlers for toolbar buttons. - * - * @type {Object} - */ - buttonHandlers - }; - } - /** * Register legacy UI listener. * @@ -170,12 +88,10 @@ class SecondaryToolbar extends Component { return null; } - const { buttonHandlers } = this.state; const { secondaryToolbarClassName } = getToolbarClassNames(this.props); return ( @@ -190,45 +106,12 @@ class SecondaryToolbar extends Component { * * @param {Function} dispatch - Redux action dispatcher. * @returns {{ - * _onLocalRaiseHandChanged: Function, - * _onSetProfileButtonUnclickable: Function, - * _onShowRecordingButton: Function, * _onSideToolbarContainerToggled * }} * @private */ function _mapDispatchToProps(dispatch: Function): Object { return { - /** - * Dispatches an action that 'hand' is raised. - * - * @param {boolean} isRaisedHand - Show whether hand is raised. - * @returns {Object} Dispatched action. - */ - _onLocalRaiseHandChanged(isRaisedHand: boolean) { - return dispatch(changeLocalRaiseHand(isRaisedHand)); - }, - - /** - * Dispatches an action signalling to set profile button unclickable. - * - * @param {boolean} unclickable - Flag showing whether unclickable - * property is true. - * @returns {Object} Dispatched action. - */ - _onSetProfileButtonUnclickable(unclickable: boolean) { - return dispatch(setProfileButtonUnclickable(unclickable)); - }, - - /** - * Dispatches an action signalling that recording button should be - * shown. - * - * @returns {Object} Dispatched action. - */ - _onShowRecordingButton() { - return dispatch(showRecordingButton()); - }, /** * Dispatches an action signalling that side toolbar container is diff --git a/react/features/toolbox/components/Toolbar.web.js b/react/features/toolbox/components/Toolbar.web.js index 72e7df9bc..4d836b4e3 100644 --- a/react/features/toolbox/components/Toolbar.web.js +++ b/react/features/toolbox/components/Toolbar.web.js @@ -36,11 +36,6 @@ class Toolbar extends Component { */ _onMouseOver: React.PropTypes.func, - /** - * Contains button handlers. - */ - buttonHandlers: React.PropTypes.object, - /** * Children of current React component. */ @@ -77,8 +72,6 @@ class Toolbar extends Component { constructor(props) { super(props); - this._setButtonHandlers(); - // Bind callbacks to preverse this. this._renderToolbarButton = this._renderToolbarButton.bind(this); } @@ -154,35 +147,6 @@ class Toolbar extends Component { return acc; } - - /** - * Sets handlers for some of the buttons. - * - * @private - * @returns {void} - */ - _setButtonHandlers(): void { - const { - buttonHandlers, - toolbarButtons - } = this.props; - - // Only a few buttons have buttonHandlers defined, so it may be - // undefined or empty depending on the buttons rendered. - // TODO Merge the buttonHandlers and onClick properties and come up with - // a consistent event handling property. - buttonHandlers && Object.keys(buttonHandlers).forEach(key => { - let button = toolbarButtons.get(key); - - if (button) { - button = { - ...button, - ...buttonHandlers[key] - }; - toolbarButtons.set(key, button); - } - }); - } } /** diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 6a207b0ea..660792ff3 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -13,6 +13,21 @@ export { abstractMapStateToProps } from './functions.native'; /* eslint-disable flowtype/space-before-type-colon */ +/** + * Returns the button object corresponding to the given 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); +} + /** * Takes toolbar button props and maps them to HTML attributes to set. * @@ -52,9 +67,11 @@ export function getButtonAttributesByProps(props: Object = {}) * Returns an object which contains the default buttons for the primary and * secondary toolbars. * + * @param {Object} buttonHandlers - Contains additional toolbox button + * handlers. * @returns {Object} */ -export function getDefaultToolboxButtons(): Object { +export function getDefaultToolboxButtons(buttonHandlers: Object): Object { let toolbarButtons = { primaryToolbarButtons: new Map(), secondaryToolbarButtons: new Map() @@ -67,13 +84,21 @@ export function getDefaultToolboxButtons(): Object { toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS.reduce( (acc, buttonName) => { - const button = defaultToolbarButtons[buttonName]; + let button = defaultToolbarButtons[buttonName]; + const currentButtonHandlers = buttonHandlers[buttonName]; if (button) { const place = _getToolbarButtonPlace(buttonName); button.buttonName = buttonName; + if (currentButtonHandlers) { + button = { + ...button, + ...currentButtonHandlers + }; + } + // In filmstrip-only mode we only add a button if it's // filmstrip-only enabled. if (!filmStripOnly || button.filmstripOnlyEnabled) { @@ -89,21 +114,6 @@ export function getDefaultToolboxButtons(): Object { return toolbarButtons; } -/** - * Get place for toolbar button. Now it can be in the primary Toolbar or in the - * secondary Toolbar. - * - * @param {string} btn - Button name. - * @private - * @returns {string} - */ -function _getToolbarButtonPlace(btn) { - return ( - interfaceConfig.MAIN_TOOLBAR_BUTTONS.includes(btn) - ? 'primaryToolbarButtons' - : 'secondaryToolbarButtons'); -} - /** * Returns toolbar class names to add while rendering. * @@ -171,3 +181,18 @@ export function showCustomToolbarPopup( AJS.$(popupSelectorID).tooltip('hide'); } } + +/** + * Get place for toolbar button. Now it can be in the primary Toolbar or in the + * secondary Toolbar. + * + * @param {string} btn - Button name. + * @private + * @returns {string} + */ +function _getToolbarButtonPlace(btn) { + return ( + interfaceConfig.MAIN_TOOLBAR_BUTTONS.includes(btn) + ? 'primaryToolbarButtons' + : 'secondaryToolbarButtons'); +}