import React, { useCallback, useEffect, useRef } from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { IReduxState, IStore } from '../../../app/types'; import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n/functions'; import { isLocalParticipantModerator } from '../../../base/participants/functions'; import ContextMenu from '../../../base/ui/components/web/ContextMenu'; import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web'; import { setHangupMenuVisible, setOverflowMenuVisible, setToolbarHovered, showToolbox } from '../../actions.web'; import { MAIN_TOOLBAR_BUTTONS_PRIORITY } from '../../constants'; import { getAllToolboxButtons, getJwtDisabledButtons, isButtonEnabled, isToolboxVisible } from '../../functions.web'; import { useKeyboardShortcuts } from '../../hooks.web'; import { IToolboxButton, NOTIFY_CLICK_MODE, ToolbarButton } from '../../types'; import HangupButton from '../HangupButton'; import { EndConferenceButton } from './EndConferenceButton'; import HangupMenuButton from './HangupMenuButton'; import { LeaveConferenceButton } from './LeaveConferenceButton'; import OverflowMenuButton from './OverflowMenuButton'; import Separator from './Separator'; /** * The type of the React {@code Component} props of {@link Toolbox}. */ interface IProps extends WithTranslation { /** * Toolbar buttons which have their click exposed through the API. */ _buttonsWithNotifyClick: Map; /** * Whether or not the chat feature is currently displayed. */ _chatOpen: boolean; /** * The width of the client. */ _clientWidth: number; /** * Custom Toolbar buttons. */ _customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>; /** * Whether or not a dialog is displayed. */ _dialog: boolean; /** * Whether or not the toolbox is disabled. It is for recorders. */ _disabled: boolean; /** * Whether the end conference feature is supported. */ _endConferenceSupported: boolean; /** * Whether the hangup menu is visible. */ _hangupMenuVisible: boolean; /** * Whether or not the app is running in mobile browser. */ _isMobile: boolean; /** * Whether we are in narrow layout mode. */ _isNarrowLayout: boolean; /** * The array of toolbar buttons disabled through jwt features. */ _jwtDisabledButtons: string[]; /** * The main toolbar buttons thresholds used to determine the visible buttons depending on the current screen size. */ _mainToolbarButtonsThresholds: Array<{ order: Array; width: number; }>; /** * Whether or not the overflow menu is displayed in a drawer drawer. */ _overflowDrawer: boolean; /** * Whether or not the overflow menu is visible. */ _overflowMenuVisible: boolean; /** * Whether or not to display reactions in separate button. */ _reactionsButtonEnabled: boolean; /** * Whether the toolbox should be shifted up or not. */ _shiftUp: boolean; /** * Whether any reactions buttons should be displayed or not. */ _shouldDisplayReactionsButtons: boolean; /** * The enabled buttons. */ _toolbarButtons: Array; /** * Flag showing whether toolbar is visible. */ _visible: boolean; /** * Invoked to active other features of the app. */ dispatch: IStore['dispatch']; /** * Explicitly passed array with the buttons which this Toolbox should display. */ toolbarButtons: Array; } const useStyles = makeStyles()(() => { return { contextMenu: { position: 'relative', right: 'auto', margin: 0, marginBottom: '8px', maxHeight: 'calc(100dvh - 100px)', minWidth: '240px' }, hangupMenu: { position: 'relative', right: 'auto', display: 'flex', flexDirection: 'column', rowGap: '8px', margin: 0, padding: '16px', marginBottom: '4px' } }; }); const Toolbox = ({ _buttonsWithNotifyClick, _chatOpen, _clientWidth, _customToolbarButtons, _dialog, _disabled, _endConferenceSupported, _hangupMenuVisible, _isMobile, _isNarrowLayout, _jwtDisabledButtons, _mainToolbarButtonsThresholds, _overflowDrawer, _overflowMenuVisible, _reactionsButtonEnabled, _shiftUp, _shouldDisplayReactionsButtons, _toolbarButtons, _visible, dispatch, t, toolbarButtons }: IProps) => { const { classes, cx } = useStyles(); const _toolboxRef = useRef(null); useKeyboardShortcuts(toolbarButtons); useEffect(() => { if (!_visible) { if (document.activeElement instanceof HTMLElement && _toolboxRef.current?.contains(document.activeElement)) { document.activeElement.blur(); } } }, [ _visible ]); /** * Sets the visibility of the hangup menu. * * @param {boolean} visible - Whether or not the hangup menu should be * displayed. * @private * @returns {void} */ const onSetHangupVisible = useCallback((visible: boolean) => { dispatch(setHangupMenuVisible(visible)); dispatch(setToolbarHovered(visible)); }, []); /** * Sets the visibility of the overflow menu. * * @param {boolean} visible - Whether or not the overflow menu should be * displayed. * @private * @returns {void} */ const onSetOverflowVisible = useCallback((visible: boolean) => { dispatch(setOverflowMenuVisible(visible)); dispatch(setToolbarHovered(visible)); }, []); useEffect(() => { if (_hangupMenuVisible && !_visible) { onSetHangupVisible(false); dispatch(setToolbarHovered(false)); } }, [ _hangupMenuVisible, _visible ]); useEffect(() => { if (_overflowMenuVisible && _dialog) { onSetOverflowVisible(false); dispatch(setToolbarHovered(false)); } }, [ _overflowMenuVisible, _dialog ]); /** * Key handler for overflow/hangup menus. * * @param {KeyboardEvent} e - Esc key click to close the popup. * @returns {void} */ const onEscKey = useCallback((e?: React.KeyboardEvent) => { if (e?.key === 'Escape') { e?.stopPropagation(); _hangupMenuVisible && dispatch(setHangupMenuVisible(false)); _overflowMenuVisible && dispatch(setOverflowMenuVisible(false)); } }, [ _hangupMenuVisible, _overflowMenuVisible ]); /** * Sets the notify click mode for the buttons. * * @param {Object} buttons - The list of toolbar buttons. * @returns {void} */ function setButtonsNotifyClickMode(buttons: Object) { if (typeof APP === 'undefined' || (_buttonsWithNotifyClick?.size ?? 0) <= 0) { return; } Object.values(buttons).forEach((button: any) => { if (typeof button === 'object') { button.notifyMode = _buttonsWithNotifyClick.get(button.key); } }); } /** * Returns all buttons that need to be rendered. * * @param {Object} state - The redux state. * @returns {Object} The visible buttons arrays . */ function getVisibleButtons() { const buttons = getAllToolboxButtons(_customToolbarButtons); const filteredButtons = Object.keys(buttons).filter(key => typeof key !== 'undefined' // filter invalid buttons that may be comming from config.mainToolbarButtons // override && !_jwtDisabledButtons.includes(key) && isButtonEnabled(key, _toolbarButtons)); setButtonsNotifyClickMode(buttons); const { order } = _mainToolbarButtonsThresholds.find(({ width }) => _clientWidth > width) || _mainToolbarButtonsThresholds[_mainToolbarButtonsThresholds.length - 1]; const mainToolbarButtonKeysOrder = [ ...order.filter(key => filteredButtons.includes(key)), ...MAIN_TOOLBAR_BUTTONS_PRIORITY.filter(key => !order.includes(key) && filteredButtons.includes(key)), ...filteredButtons.filter(key => !order.includes(key) && !MAIN_TOOLBAR_BUTTONS_PRIORITY.includes(key)) ]; const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length); const overflowMenuButtons = filteredButtons.reduce((acc, key) => { if (!mainButtonsKeys.includes(key)) { acc.push(buttons[key]); } return acc; }, [] as IToolboxButton[]); // if we have 1 button in the overflow menu it is better to directly display it in the main toolbar by replacing // the "More" menu button with it. if (overflowMenuButtons.length === 1) { const button = overflowMenuButtons.shift()?.key; button && mainButtonsKeys.push(button); } return { mainMenuButtons: mainButtonsKeys.map(key => buttons[key]), overflowMenuButtons }; } /** * Dispatches an action signaling the toolbar is not being hovered. * * @private * @returns {void} */ function onMouseOut() { !_overflowMenuVisible && dispatch(setToolbarHovered(false)); } /** * Dispatches an action signaling the toolbar is being hovered. * * @private * @returns {void} */ function onMouseOver() { dispatch(setToolbarHovered(true)); } /** * Toggle the toolbar visibility when tabbing into it. * * @returns {void} */ const onTabIn = useCallback(() => { if (!_visible) { dispatch(showToolbox()); } }, [ _visible ]); /** * Renders the toolbox content. * * @returns {ReactElement} */ function renderToolboxContent() { const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`; const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons(); const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand'); const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons && ( (!_reactionsButtonEnabled && (raiseHandInOverflowMenu || _isNarrowLayout || _isMobile)) || overflowMenuButtons.some(({ key }) => key === 'reactions') ); const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu; return (
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && ( ))} {Boolean(overflowMenuButtons.length) && ( >((acc, val) => { if (val.key === 'reactions' && showReactionsInOverflowMenu) { return acc; } if (val.key === 'raisehand' && showRaiseHandInReactionsMenu) { return acc; } if (acc.length) { const prev = acc[acc.length - 1]; const group = prev[prev.length - 1].group; if (group === val.group) { prev.push(val); } else { acc.push([ val ]); } } else { acc.push([ val ]); } return acc; }, []) } isOpen = { _overflowMenuVisible } key = 'overflow-menu' onToolboxEscKey = { onEscKey } onVisibilityChange = { onSetOverflowVisible } showRaiseHandInReactionsMenu = { showRaiseHandInReactionsMenu } showReactionsMenu = { showReactionsInOverflowMenu } /> )} {isButtonEnabled('hangup', _toolbarButtons) && ( _endConferenceSupported ? : )}
); } if (_disabled) { return null; } const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${ _toolbarButtons.length ? '' : 'no-buttons'} ${_chatOpen ? 'shift-right' : ''}`; return (
{renderToolboxContent()}
); }; /** * Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component} * props. * * @param {Object} state - The redux store/state. * @param {Object} ownProps - The props explicitly passed. * @private * @returns {{}} */ function _mapStateToProps(state: IReduxState, ownProps: any) { const { conference } = state['features/base/conference']; const { isNarrowLayout } = state['features/base/responsive-ui']; const endConferenceSupported = conference?.isEndConferenceSupported() && isLocalParticipantModerator(state); const { customToolbarButtons, iAmRecorder, iAmSipGateway } = state['features/base/config']; const { hangupMenuVisible, overflowMenuVisible, overflowDrawer } = state['features/toolbox']; const { clientWidth } = state['features/base/responsive-ui']; const toolbarButtons = ownProps.toolbarButtons || state['features/toolbox'].toolbarButtons; return { _buttonsWithNotifyClick: state['features/toolbox'].buttonsWithNotifyClick, _chatOpen: state['features/chat'].isOpen, _clientWidth: clientWidth, _customToolbarButtons: customToolbarButtons, _dialog: Boolean(state['features/base/dialog'].component), _disabled: Boolean(iAmRecorder || iAmSipGateway), _endConferenceSupported: Boolean(endConferenceSupported), _isMobile: isMobileBrowser(), _jwtDisabledButtons: getJwtDisabledButtons(state), _hangupMenuVisible: hangupMenuVisible, _isNarrowLayout: isNarrowLayout, _mainToolbarButtonsThresholds: state['features/toolbox'].mainToolbarButtonsThresholds, _overflowMenuVisible: overflowMenuVisible, _overflowDrawer: overflowDrawer, _reactionsButtonEnabled: isReactionsButtonEnabled(state), _shiftUp: state['features/toolbox'].shiftUp, _shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state), _toolbarButtons: toolbarButtons, _visible: isToolboxVisible(state) }; } export default translate(connect(_mapStateToProps)(Toolbox));