From cfe4564ab3c45bc9cb51b96b8bea9758392163ed Mon Sep 17 00:00:00 2001 From: virtuacoplenny Date: Fri, 29 Sep 2017 13:27:23 -0700 Subject: [PATCH] feat(info): automatically show the info dialog (#2018) * ref(info): be able to open dialog through store * feat(info): automatically show the info dialog Conditions: - Lonely call - Has not opened the info dialog yet * squash: change to show on start, hide later * squash: update naming and comment --- css/modals/invite/_info.scss | 1 + react/features/invite/actionTypes.js | 11 + react/features/invite/actions.js | 18 +- .../invite/components/InfoDialog.web.js | 9 +- .../invite/components/InfoDialogButton.web.js | 201 +++++++++++++++++- react/features/invite/reducer.js | 11 + .../toolbox/components/ToolbarButton.web.js | 13 +- .../components/ToolbarButtonWithDialog.web.js | 16 +- react/features/toolbox/constants.js | 11 + react/features/toolbox/index.js | 1 + 10 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 react/features/toolbox/constants.js diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss index ea6972c43..0c81b4ee6 100644 --- a/css/modals/invite/_info.scss +++ b/css/modals/invite/_info.scss @@ -1,4 +1,5 @@ .info-dialog { + cursor: default; display: flex; .info-dialog-action-link { diff --git a/react/features/invite/actionTypes.js b/react/features/invite/actionTypes.js index c57de29bb..7deb9fd21 100644 --- a/react/features/invite/actionTypes.js +++ b/react/features/invite/actionTypes.js @@ -1,3 +1,14 @@ +/** + * The type of the action which signals a request to display the inline + * conference info dialog. + * + * { + * type: SET_INFO_DIALOG_VISIBILITY, + * visible: boolean + * } + */ +export const SET_INFO_DIALOG_VISIBILITY = Symbol('SET_INFO_DIALOG_VISIBILITY'); + /** * The type of the action which signals an error occurred while requesting dial- * in numbers. diff --git a/react/features/invite/actions.js b/react/features/invite/actions.js index 65427abb4..889a121e0 100644 --- a/react/features/invite/actions.js +++ b/react/features/invite/actions.js @@ -1,13 +1,13 @@ import { openDialog } from '../../features/base/dialog'; import { + SET_INFO_DIALOG_VISIBILITY, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; import { AddPeopleDialog, InviteDialog } from './components'; declare var $: Function; -declare var APP: Object; /** * Opens the Invite Dialog. @@ -27,6 +27,22 @@ export function openAddPeopleDialog() { return openDialog(AddPeopleDialog); } +/** + * Opens the inline conference info dialog. + * + * @param {boolean} visible - Whether or not the dialog should be displayed. + * @returns {{ + * type: SET_INFO_DIALOG_VISIBILITY, + * visible: boolean + * }} + */ +export function setInfoDialogVisibility(visible) { + return { + type: SET_INFO_DIALOG_VISIBILITY, + visible + }; +} + /** * Sends AJAX requests for dial-in numbers and conference ID. * diff --git a/react/features/invite/components/InfoDialog.web.js b/react/features/invite/components/InfoDialog.web.js index a7a44d3a1..908eb6036 100644 --- a/react/features/invite/components/InfoDialog.web.js +++ b/react/features/invite/components/InfoDialog.web.js @@ -45,6 +45,11 @@ class InfoDialog extends Component { */ onClose: PropTypes.func, + /** + * Callback invoked when a mouse-related event has been detected. + */ + onMouseOver: PropTypes.func, + /** * Invoked to obtain translated strings. */ @@ -84,7 +89,9 @@ class InfoDialog extends Component { */ render() { return ( -
+

diff --git a/react/features/invite/components/InfoDialogButton.web.js b/react/features/invite/components/InfoDialogButton.web.js index 1b6c4eadf..dd0f6febb 100644 --- a/react/features/invite/components/InfoDialogButton.web.js +++ b/react/features/invite/components/InfoDialogButton.web.js @@ -1,9 +1,18 @@ +import InlineDialog from '@atlaskit/inline-dialog'; +import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; -import { ToolbarButtonWithDialog } from '../../toolbox'; +import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox'; + +import { setInfoDialogVisibility } from '../actions'; import InfoDialog from './InfoDialog'; +declare var interfaceConfig: Object; + +const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig; + /** * A configuration object to describe how {@code ToolbarButton} should render * the button. @@ -25,6 +34,103 @@ const DEFAULT_BUTTON_CONFIGURATION = { * @extends Component */ class InfoDialogButton extends Component { + /** + * {@code InfoDialogButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not {@code InfoDialog} should be displayed. + */ + _showDialog: PropTypes.bool, + + /** + * Whether or not the toolbox, in which this component exists, are + * visible. + */ + _toolboxVisible: PropTypes.bool, + + /** + * Invoked to toggle display of the info dialog + */ + dispatch: PropTypes.func, + + /** + * From which side tooltips should display. Will be re-used for + * displaying the inline dialog for video quality adjustment. + */ + tooltipPosition: PropTypes.string + }; + + /** + * Initializes new {@code ToolbarButtonWithDialog} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + /** + * The timeout to automatically hide the {@code InfoDialog} if it has + * not been interacted with. + * + * @type {timeoutID} + */ + this._autoHideDialogTimeout = null; + + this.state = { + /** + * Whether or not the dialog has been interacted with somehow, such + * as clicking or toggle display. A value of true will prevent the + * dialog from being automatically hidden. + */ + hasInteractedWithDialog: false + }; + + // Bind event handlers so they are only bound once for every instance. + this._onDialogClose = this._onDialogClose.bind(this); + this._onDialogMouseOver = this._onDialogMouseOver.bind(this); + this._onDialogToggle = this._onDialogToggle.bind(this); + } + + /** + * Set a timeout to automatically hide the {@code InfoDialog}. + * + * @inheritdoc + */ + componentDidMount() { + this._autoHideDialogTimeout = setTimeout(() => { + this._maybeHideDialog(); + }, INITIAL_TOOLBAR_TIMEOUT); + } + + /** + * Update the state when the {@code InfoDialog} visibility has been updated. + * + * @inheritdoc + */ + componentWillReceiveProps(nextProps) { + if (!this.state.hasInteractedWithDialog + && (nextProps._showDialog !== this.props._showDialog)) { + this.setState({ hasInteractedWithDialog: true }); + } + + if (!nextProps._toolboxVisible && this.props._toolboxVisible) { + this._onDialogClose(); + } + } + + /** + * Clear the timeout to automatically show the {@code InfoDialog}. + * + * @inheritdoc + */ + componentWillUnmount() { + clearTimeout(this._autoHideDialogTimeout); + } + /** * Implements React's {@link Component#render()}. * @@ -32,13 +138,96 @@ class InfoDialogButton extends Component { * @returns {ReactElement} */ render() { + const { _showDialog, _toolboxVisible, tooltipPosition } = this.props; + const buttonConfiguration = { + ...DEFAULT_BUTTON_CONFIGURATION, + classNames: [ + ...DEFAULT_BUTTON_CONFIGURATION.classNames, + _showDialog ? 'toggled button-active' : '' + ] + }; + return ( - + } + isOpen = { _toolboxVisible && _showDialog } + onClose = { this._onDialogClose } + onContentClick = { this._onDialogInteract } + position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }> + + ); } + + /** + * Callback invoked after a timeout to trigger hiding of the + * {@code InfoDialog} if there has been no interaction with the dialog + * and the dialog is currently showing. + * + * @private + * @returns {void} + */ + _maybeHideDialog() { + if (!this.state.hasInteractedWithDialog && this.props._showDialog) { + this._onDialogToggle(); + } + } + + /** + * Hides {@code InfoDialog}. + * + * @private + * @returns {void} + */ + _onDialogClose() { + this.props.dispatch(setInfoDialogVisibility(false)); + } + + /** + * Updates the internal state to mark the {@code InfoDialog} as having been + * interacted with. + * + * @private + * @returns {void} + */ + _onDialogMouseOver() { + if (!this.state.hasInteractedWithDialog) { + this.setState({ hasInteractedWithDialog: true }); + } + } + + /** + * Toggles the display of {@code InfoDialog}. + * + * @private + * @returns {void} + */ + _onDialogToggle() { + this.props.dispatch(setInfoDialogVisibility(!this.props._showDialog)); + } } -export default InfoDialogButton; +/** + * Maps (parts of) the Redux state to the associated {@code InfoDialogButton} + * component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _showDialog: boolean, + * _toolboxVisible: boolean + * }} + */ +function _mapStateToProps(state) { + return { + _showDialog: state['features/invite'].infoDialogVisible, + _toolboxVisible: state['features/toolbox'].visible + }; +} + +export default connect(_mapStateToProps)(InfoDialogButton); diff --git a/react/features/invite/reducer.js b/react/features/invite/reducer.js index 5323903c5..215c80bd6 100644 --- a/react/features/invite/reducer.js +++ b/react/features/invite/reducer.js @@ -1,16 +1,27 @@ import { ReducerRegistry } from '../base/redux'; import { + SET_INFO_DIALOG_VISIBILITY, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; const DEFAULT_STATE = { + + // By default show the info dialog when joining the conference. + infoDialogVisible: true, + numbersEnabled: true }; ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => { switch (action.type) { + case SET_INFO_DIALOG_VISIBILITY: + return { + ...state, + infoDialogVisible: action.visible + }; + case UPDATE_DIAL_IN_NUMBERS_FAILED: return { ...state, diff --git a/react/features/toolbox/components/ToolbarButton.web.js b/react/features/toolbox/components/ToolbarButton.web.js index 0b4753e1d..3ce6bf59d 100644 --- a/react/features/toolbox/components/ToolbarButton.web.js +++ b/react/features/toolbox/components/ToolbarButton.web.js @@ -7,23 +7,12 @@ import React, { Component } from 'react'; import { translate } from '../../base/i18n'; +import { TOOLTIP_TO_POPUP_POSITION } from '../constants'; import { isButtonEnabled } from '../functions'; import StatelessToolbarButton from './StatelessToolbarButton'; declare var APP: Object; -/** - * Mapping of tooltip positions to equivalent {@code AKInlineDialog} positions. - * - * @private - */ -const TOOLTIP_TO_POPUP_POSITION = { - bottom: 'bottom center', - left: 'left middle', - top: 'top center', - right: 'right middle' -}; - /** * Represents a button in Toolbar on React. * diff --git a/react/features/toolbox/components/ToolbarButtonWithDialog.web.js b/react/features/toolbox/components/ToolbarButtonWithDialog.web.js index a3e45982e..41234f2a1 100644 --- a/react/features/toolbox/components/ToolbarButtonWithDialog.web.js +++ b/react/features/toolbox/components/ToolbarButtonWithDialog.web.js @@ -3,21 +3,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { TOOLTIP_TO_POPUP_POSITION } from '../constants'; import ToolbarButton from './ToolbarButton'; -/** - * Maps AtlasKit {@code Tooltip} positions to equivalent {@code InlineDialog} - * positions. The {@code InlineDialog} will appear from the the same side of - * the button as the tooltip. - * - */ -const TOOLTIP_TO_DIALOG_POSITION = { - bottom: 'bottom center', - left: 'left middle', - right: 'right middle', - top: 'top center' -}; - /** * React {@code Component} for displaying a button which will open an inline * dialog. @@ -113,7 +101,7 @@ class ToolbarButtonWithDialog extends Component { content = { } isOpen = { _visible && this.state.showDialog } onClose = { this._onDialogClose } - position = { TOOLTIP_TO_DIALOG_POSITION[tooltipPosition] }> + position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }>