diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 5ae590ea3..999fc6a0f 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -19,9 +19,9 @@ vertical-align: middle; } -/** -* Toolbar button styles. -*/ + /** + * Toolbar button styles. + */ .button { color: #FFFFFF; cursor: pointer; @@ -97,8 +97,8 @@ } /** -* Common toolbar styles. -*/ + * Common toolbar styles. + */ .toolbar { background-color: $toolbarBackground; height: 100%; @@ -119,8 +119,8 @@ } /** - * Primary toolbar styles. - */ + * Primary toolbar styles. + */ &_primary { position: absolute; left: 50%; @@ -148,8 +148,8 @@ } /** - * Secondary toolbar styles. - */ + * Secondary toolbar styles. + */ &_secondary { position: absolute; align-items: center; @@ -186,6 +186,28 @@ } } + /** + * Styles the toolbar in filmstrip-only mode. + */ + &_filmstrip-only { + border-radius: 3px; + bottom: 0; + display: inline-block; + height: auto; + position: absolute; + right: 0; + width: $defaultToolbarSize; + + .button:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; + } + .button:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + } + } + /** * Toolbar specific round badge. */ diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index f065074da..84a375b87 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -233,21 +233,22 @@ export function showSIPCallButton(show: boolean): Function { */ export function showToolbox(timeout: number = 0): Object { return (dispatch: Dispatch<*>, getState: Function) => { - if (interfaceConfig.filmStripOnly) { - return; - } - const state = getState(); - const { timeoutMS, visible } = state['features/toolbox']; + const { alwaysVisible, timeoutMS, visible } = state['features/toolbox']; if (!visible) { dispatch(setToolboxVisible(true)); dispatch(setSubjectSlideIn(true)); - dispatch( - setToolboxTimeout( - () => dispatch(hideToolbox()), - timeout || timeoutMS)); - dispatch(setToolboxTimeoutMS(interfaceConfig.TOOLBAR_TIMEOUT)); + + // If the Toolbox is always visible, there's no need for a timeout + // to toggle its visibility. + if (!alwaysVisible) { + dispatch( + setToolboxTimeout( + () => dispatch(hideToolbox()), + timeout || timeoutMS)); + dispatch(setToolboxTimeoutMS(interfaceConfig.TOOLBAR_TIMEOUT)); + } } }; } diff --git a/react/features/toolbox/components/PrimaryToolbar.web.js b/react/features/toolbox/components/PrimaryToolbar.web.js index 51dbbded1..5345e1764 100644 --- a/react/features/toolbox/components/PrimaryToolbar.web.js +++ b/react/features/toolbox/components/PrimaryToolbar.web.js @@ -101,9 +101,17 @@ class PrimaryToolbar extends Component { * * @returns {ReactElement} */ - render() { - const { buttonHandlers, splitterIndex } = this.state; + render(): ReactElement<*> | null { const { _primaryToolbarButtons } = this.props; + + // The number of buttons to show in the toolbar isn't fixed, it depends + // on availability of features and configuration parameters, so if we + // don't have anything to render we exit here. + if (_primaryToolbarButtons.size === 0) { + return null; + } + + const { buttonHandlers, splitterIndex } = this.state; const { primaryToolbarClassName } = getToolbarClassNames(this.props); return ( diff --git a/react/features/toolbox/components/SecondaryToolbar.web.js b/react/features/toolbox/components/SecondaryToolbar.web.js index a64add237..134d425a4 100644 --- a/react/features/toolbox/components/SecondaryToolbar.web.js +++ b/react/features/toolbox/components/SecondaryToolbar.web.js @@ -150,9 +150,17 @@ class SecondaryToolbar extends Component { * * @returns {ReactElement} */ - render(): ReactElement<*> { - const { buttonHandlers } = this.state; + render(): ReactElement<*> | null { const { _secondaryToolbarButtons } = this.props; + + // The number of buttons to show in the toolbar isn't fixed, it depends + // on availability of features and configuration parameters, so if we + // don't have anything to render we exit here. + if (_secondaryToolbarButtons.size === 0) { + return null; + } + + const { buttonHandlers } = this.state; const { secondaryToolbarClassName } = getToolbarClassNames(this.props); return ( diff --git a/react/features/toolbox/components/Toolbar.web.js b/react/features/toolbox/components/Toolbar.web.js index 7f7bf5b38..cea9da7fd 100644 --- a/react/features/toolbox/components/Toolbar.web.js +++ b/react/features/toolbox/components/Toolbar.web.js @@ -8,10 +8,6 @@ import { } from '../actions'; import ToolbarButton from './ToolbarButton'; -declare var APP: Object; -declare var config: Object; -declare var interfaceConfig: Object; - /** * Implements a toolbar in React/Web. It is a strip that contains a set of * toolbar items such as buttons. Toolbar is commonly placed inside of a @@ -153,6 +149,15 @@ class Toolbar extends Component { toolbarButtons } = this.props; + // Only a few buttons have custom button handlers defined, so this + // list may be undefined or empty depending on the buttons we're + // rendering. + // TODO: merge the buttonHandlers and onClick properties and come up + // with a consistent event handling property. + if (!buttonHandlers) { + return; + } + Object.keys(buttonHandlers).forEach(key => { let button = toolbarButtons.get(key); diff --git a/react/features/toolbox/components/Toolbox.web.js b/react/features/toolbox/components/Toolbox.web.js index 38073ac1d..49cd770d5 100644 --- a/react/features/toolbox/components/Toolbox.web.js +++ b/react/features/toolbox/components/Toolbox.web.js @@ -31,6 +31,11 @@ class Toolbox extends Component { * @static */ static propTypes = { + /** + * Indicates if the toolbox should always be visible. + */ + _alwaysVisible: React.PropTypes.bool, + /** * Handler dispatching setting default buttons action. */ @@ -159,8 +164,9 @@ class Toolbox extends Component { * @private */ _renderToolbars(): ReactElement<*> | null { - // The toolbars should not be shown until timeoutID is initialized. - if (this.props._timeoutID === null) { + // In case we're not in alwaysVisible mode the toolbox should not be + // shown until timeoutID is initialized. + if (!this.props._alwaysVisible && this.props._timeoutID === null) { return null; } @@ -202,8 +208,9 @@ function _mapDispatchToProps(dispatch: Function): Object { * @returns {Object} Dispatched action. */ _setToolboxAlwaysVisible() { - dispatch( - setToolboxAlwaysVisible(config.alwaysVisibleToolbar === true)); + dispatch(setToolboxAlwaysVisible( + config.alwaysVisibleToolbar === true + || interfaceConfig.filmStripOnly)); } }; } @@ -214,6 +221,7 @@ function _mapDispatchToProps(dispatch: Function): Object { * @param {Object} state - Redux state. * @private * @returns {{ + * _alwaysVisible: boolean, * _audioMuted: boolean, * _locked: boolean, * _subjectSlideIn: boolean, @@ -222,6 +230,7 @@ function _mapDispatchToProps(dispatch: Function): Object { */ function _mapStateToProps(state: Object): Object { const { + alwaysVisible, subject, subjectSlideIn, timeoutID @@ -230,10 +239,18 @@ function _mapStateToProps(state: Object): Object { return { ...abstractMapStateToProps(state), + /** + * Indicates if the toolbox should always be visible. + * + * @private + * @type {boolean} + */ + _alwaysVisible: alwaysVisible, + /** * Property containing conference subject. * - * @protected + * @private * @type {string} */ _subject: subject, @@ -241,7 +258,7 @@ function _mapStateToProps(state: Object): Object { /** * Flag showing whether to set subject slide in animation. * - * @protected + * @private * @type {boolean} */ _subjectSlideIn: subjectSlideIn, @@ -249,7 +266,7 @@ function _mapStateToProps(state: Object): Object { /** * Property containing toolbox timeout id. * - * @protected + * @private * @type {number} */ _timeoutID: timeoutID diff --git a/react/features/toolbox/defaultToolbarButtons.js b/react/features/toolbox/defaultToolbarButtons.js index 35665a0a2..7082c4ee5 100644 --- a/react/features/toolbox/defaultToolbarButtons.js +++ b/react/features/toolbox/defaultToolbarButtons.js @@ -46,6 +46,7 @@ export default { camera: { classNames: [ 'button', 'icon-camera' ], enabled: true, + filmstripOnlyEnabled: true, id: 'toolbar_button_camera', onClick() { if (APP.conference.videoMuted) { @@ -203,6 +204,7 @@ export default { hangup: { classNames: [ 'button', 'icon-hangup', 'button_hangup' ], enabled: true, + filmstripOnlyEnabled: true, id: 'toolbar_button_hangup', onClick() { JitsiMeetJS.analytics.sendEvent('toolbar.hangup'); @@ -231,6 +233,7 @@ export default { microphone: { classNames: [ 'button', 'icon-microphone' ], enabled: true, + filmstripOnlyEnabled: true, id: 'toolbar_button_mute', onClick() { const sharedVideoManager = APP.UI.getSharedVideoManager(); diff --git a/react/features/toolbox/functions.js b/react/features/toolbox/functions.js index 119ff5c04..f2d1edbf6 100644 --- a/react/features/toolbox/functions.js +++ b/react/features/toolbox/functions.js @@ -167,6 +167,8 @@ export function getDefaultToolboxButtons(): Object { if (typeof interfaceConfig !== 'undefined' && interfaceConfig.TOOLBAR_BUTTONS) { + const { filmStripOnly } = interfaceConfig; + toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS.reduce( (acc, buttonName) => { @@ -176,7 +178,12 @@ export function getDefaultToolboxButtons(): Object { const place = _getToolbarButtonPlace(buttonName); button.buttonName = buttonName; - acc[place].set(buttonName, button); + + // In filmstrip-only mode we only add a button if it's + // filmstrip-only enabled. + if (!filmStripOnly || button.filmstripOnlyEnabled) { + acc[place].set(buttonName, button); + } } return acc; @@ -210,7 +217,11 @@ function _getToolbarButtonPlace(btn) { * @private */ export function getToolbarClassNames(props: Object) { - const primaryToolbarClassNames = [ 'toolbar_primary' ]; + const primaryToolbarClassNames = [ + interfaceConfig.filmStripOnly + ? 'toolbar_filmstrip-only' + : 'toolbar_primary' + ]; const secondaryToolbarClassNames = [ 'toolbar_secondary' ]; if (props._visible) { diff --git a/react/features/toolbox/reducer.js b/react/features/toolbox/reducer.js index c53dd57df..2f3b756bd 100644 --- a/react/features/toolbox/reducer.js +++ b/react/features/toolbox/reducer.js @@ -207,6 +207,18 @@ function _setButton(state, { button, buttonName }): Object { ...button }; + // In filmstrip-only mode we only show buttons if they're filmstrip-only + // enabled, so we don't need to update if this isn't the case. + // FIXME A reducer should be a pure function of the current state and the + // specified action so it should not use the global variable + // interfaceConfig. Anyway, we'll move interfaceConfig into the (redux) + // store so we'll surely revisit the source code bellow. + if (interfaceConfig.filmStripOnly && !selectedButton.filmstripOnlyEnabled) { + return { + ...state + }; + } + const updatedToolbar = state[place].set(buttonName, selectedButton); return {