From 46ea1f577cc8617bea0eea52cba4413f4b2a9cfe Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Wed, 22 May 2024 13:01:17 -0500 Subject: [PATCH] ref(Toolbox): replace mapStateToProps with hooks. --- .../toolbox/components/web/Toolbox.tsx | 418 +++++------------- react/features/toolbox/functions.web.ts | 103 ++++- 2 files changed, 197 insertions(+), 324 deletions(-) diff --git a/react/features/toolbox/components/web/Toolbox.tsx b/react/features/toolbox/components/web/Toolbox.tsx index 07a7c39e77..af12962aed 100644 --- a/react/features/toolbox/components/web/Toolbox.tsx +++ b/react/features/toolbox/components/web/Toolbox.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import { WithTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; -import { IReduxState, IStore } from '../../../app/types'; +import { IReduxState } 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'; @@ -15,15 +14,14 @@ import { setToolbarHovered, showToolbox } from '../../actions.web'; -import { MAIN_TOOLBAR_BUTTONS_PRIORITY } from '../../constants'; import { - getAllToolboxButtons, getJwtDisabledButtons, + getVisibleButtons, isButtonEnabled, isToolboxVisible } from '../../functions.web'; import { useKeyboardShortcuts } from '../../hooks.web'; -import { IToolboxButton, NOTIFY_CLICK_MODE, ToolbarButton } from '../../types'; +import { IToolboxButton } from '../../types'; import HangupButton from '../HangupButton'; import { EndConferenceButton } from './EndConferenceButton'; @@ -35,115 +33,12 @@ 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']; +interface IProps { /** * Explicitly passed array with the buttons which this Toolbox should display. */ - toolbarButtons: Array; + toolbarButtons?: Array; } const useStyles = makeStyles()(() => { @@ -170,43 +65,55 @@ const useStyles = makeStyles()(() => { }; }); -const Toolbox = ({ - _buttonsWithNotifyClick, - _chatOpen, - _clientWidth, - _customToolbarButtons, - _dialog, - _disabled, - _endConferenceSupported, - _hangupMenuVisible, - _isMobile, - _isNarrowLayout, - _jwtDisabledButtons, - _mainToolbarButtonsThresholds, - _overflowDrawer, - _overflowMenuVisible, - _reactionsButtonEnabled, - _shiftUp, - _shouldDisplayReactionsButtons, - _toolbarButtons, - _visible, - dispatch, - t, +/** + * A component that renders the main toolbar. + * + * @param {IProps} props - The props of the component. + * @returns {ReactElement} + */ +export default function Toolbox({ toolbarButtons -}: IProps) => { +}: IProps) { const { classes, cx } = useStyles(); + const { t } = useTranslation(); + const dispatch = useDispatch(); const _toolboxRef = useRef(null); - useKeyboardShortcuts(toolbarButtons); + const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference); + const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout); + const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth); + const isModerator = useSelector(isLocalParticipantModerator); + const customToolbarButtons = useSelector( + (state: IReduxState) => state['features/base/config'].customToolbarButtons); + const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder); + const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway); + const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer); + const shiftUp = useSelector((state: IReduxState) => state['features/toolbox'].shiftUp); + const overflowMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].overflowMenuVisible); + const hangupMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].hangupMenuVisible); + const buttonsWithNotifyClick + = useSelector((state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick); + const reduxToolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons); + const toolbarButtonsToUse = toolbarButtons || reduxToolbarButtons; + const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen); + const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component)); + const jwtDisabledButtons = useSelector(getJwtDisabledButtons); + const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled); + const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons); + const toolbarVisible = useSelector(isToolboxVisible); + const mainToolbarButtonsThresholds + = useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds); + + useKeyboardShortcuts(toolbarButtonsToUse); useEffect(() => { - if (!_visible) { + if (!toolbarVisible) { if (document.activeElement instanceof HTMLElement && _toolboxRef.current?.contains(document.activeElement)) { document.activeElement.blur(); } } - }, [ _visible ]); + }, [ toolbarVisible ]); /** * Sets the visibility of the hangup menu. @@ -219,7 +126,7 @@ const Toolbox = ({ const onSetHangupVisible = useCallback((visible: boolean) => { dispatch(setHangupMenuVisible(visible)); dispatch(setToolbarHovered(visible)); - }, []); + }, [ dispatch ]); /** * Sets the visibility of the overflow menu. @@ -232,21 +139,21 @@ const Toolbox = ({ const onSetOverflowVisible = useCallback((visible: boolean) => { dispatch(setOverflowMenuVisible(visible)); dispatch(setToolbarHovered(visible)); - }, []); + }, [ dispatch ]); useEffect(() => { - if (_hangupMenuVisible && !_visible) { + if (hangupMenuVisible && !toolbarVisible) { onSetHangupVisible(false); dispatch(setToolbarHovered(false)); } - }, [ _hangupMenuVisible, _visible ]); + }, [ dispatch, hangupMenuVisible, toolbarVisible, onSetHangupVisible ]); useEffect(() => { - if (_overflowMenuVisible && _dialog) { + if (overflowMenuVisible && isDialogVisible) { onSetOverflowVisible(false); dispatch(setToolbarHovered(false)); } - }, [ _overflowMenuVisible, _dialog ]); + }, [ dispatch, overflowMenuVisible, isDialogVisible, onSetOverflowVisible ]); /** * Key handler for overflow/hangup menus. @@ -257,76 +164,10 @@ const Toolbox = ({ const onEscKey = useCallback((e?: React.KeyboardEvent) => { if (e?.key === 'Escape') { e?.stopPropagation(); - _hangupMenuVisible && dispatch(setHangupMenuVisible(false)); - _overflowMenuVisible && dispatch(setOverflowMenuVisible(false)); + 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 - }; - } + }, [ dispatch, hangupMenuVisible, overflowMenuVisible ]); /** * Dispatches an action signaling the toolbar is not being hovered. @@ -334,9 +175,9 @@ const Toolbox = ({ * @private * @returns {void} */ - function onMouseOut() { - !_overflowMenuVisible && dispatch(setToolbarHovered(false)); - } + const onMouseOut = useCallback(() => { + !overflowMenuVisible && dispatch(setToolbarHovered(false)); + }, [ dispatch, overflowMenuVisible ]); /** * Dispatches an action signaling the toolbar is being hovered. @@ -344,9 +185,9 @@ const Toolbox = ({ * @private * @returns {void} */ - function onMouseOver() { + const onMouseOver = useCallback(() => { dispatch(setToolbarHovered(true)); - } + }, [ dispatch ]); /** * Toggle the toolbar visibility when tabbing into it. @@ -354,35 +195,49 @@ const Toolbox = ({ * @returns {void} */ const onTabIn = useCallback(() => { - if (!_visible) { + if (!toolbarVisible) { dispatch(showToolbox()); } - }, [ _visible ]); + }, [ toolbarVisible, dispatch ]); - /** - * Renders the toolbox content. - * - * @returns {ReactElement} - */ - function renderToolboxContent() { - const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; - const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`; + if (iAmRecorder || iAmSipGateway) { + return null; + } - 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; + const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator); + const isMobile = isMobileBrowser(); - return ( + const rootClassNames = `new-toolbox ${toolbarVisible ? 'visible' : ''} ${ + toolbarButtonsToUse.length ? '' : 'no-buttons'} ${chatOpen ? 'shift-right' : ''}`; + + const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; + const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`; + + const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({ + customToolbarButtons, + buttonsWithNotifyClick, + toolbarButtons: toolbarButtonsToUse, + clientWidth, + jwtDisabledButtons, + mainToolbarButtonsThresholds + }); + const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand'); + const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons + && ( + (!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile)) + || overflowMenuButtons.some(({ key }) => key === 'reactions') + ); + const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu; + + return ( +
@@ -423,7 +278,7 @@ const Toolbox = ({ return acc; }, []) } - isOpen = { _overflowMenuVisible } + isOpen = { overflowMenuVisible } key = 'overflow-menu' onToolboxEscKey = { onEscKey } onVisibilityChange = { onSetOverflowVisible } @@ -431,105 +286,38 @@ const Toolbox = ({ showReactionsMenu = { showReactionsInOverflowMenu } /> )} - {isButtonEnabled('hangup', _toolbarButtons) && ( - _endConferenceSupported + {isButtonEnabled('hangup', toolbarButtonsToUse) && ( + endConferenceSupported ? : + notifyMode = { buttonsWithNotifyClick.get('hangup') } + visible = { isButtonEnabled('hangup', toolbarButtonsToUse) } /> )}
- ); - } - - 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)); diff --git a/react/features/toolbox/functions.web.ts b/react/features/toolbox/functions.web.ts index 601e14a190..58df096fcd 100644 --- a/react/features/toolbox/functions.web.ts +++ b/react/features/toolbox/functions.web.ts @@ -38,8 +38,8 @@ import ProfileButton from './components/web/ProfileButton'; import ShareDesktopButton from './components/web/ShareDesktopButton'; import ToggleCameraButton from './components/web/ToggleCameraButton'; import VideoSettingsButton from './components/web/VideoSettingsButton'; -import { TOOLBAR_TIMEOUT } from './constants'; -import { IToolboxButton, NOTIFY_CLICK_MODE } from './types'; +import { MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants'; +import { IMainToolbarButtonThresholds, IToolboxButton, NOTIFY_CLICK_MODE } from './types'; export * from './functions.any'; @@ -189,19 +189,21 @@ export function getToolbarTimeout(state: IReduxState) { return toolbarConfig?.timeout || TOOLBAR_TIMEOUT; } +interface ICustomToolbarButton { + backgroundColor?: string; + icon: string; + id: string; + text: string; +} + /** * Returns all buttons that could be rendered. * * @param {Object} _customToolbarButtons - An array containing custom buttons objects. * @returns {Object} The button maps mainMenuButtons and overflowMenuButtons. */ -export function getAllToolboxButtons(_customToolbarButtons?: { - backgroundColor?: string; - icon: string; - id: string; - text: string; - }[]): { [key: string]: IToolboxButton; } { - +export function getAllToolboxButtons( + _customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } { const microphone = { key: 'microphone', Content: AudioSettingsButton, @@ -442,6 +444,89 @@ export function getAllToolboxButtons(_customToolbarButtons?: { }; } +/** + * Sets the notify click mode for the buttons. + * + * @param {Object} buttons - The list of toolbar buttons. + * @param {Map} buttonsWithNotifyClick - The buttons notify click configuration. + * @returns {void} + */ +function setButtonsNotifyClickMode(buttons: Object, buttonsWithNotifyClick: Map) { + 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); + } + }); +} + +interface IGetVisibleButtonsParams { + buttonsWithNotifyClick: Map; + clientWidth: number; + customToolbarButtons?: ICustomToolbarButton[]; + jwtDisabledButtons: string[]; + mainToolbarButtonsThresholds: IMainToolbarButtonThresholds; + toolbarButtons: string[]; +} + +/** + * Returns all buttons that need to be rendered. + * + * @param {IGetVisibleButtonsParams} params - The parameters needed to extract the visible buttons. + * @returns {Object} - The visible buttons arrays . + */ +export function getVisibleButtons({ + customToolbarButtons, + buttonsWithNotifyClick, + toolbarButtons, + clientWidth, + jwtDisabledButtons, + mainToolbarButtonsThresholds +}: IGetVisibleButtonsParams) { + 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, buttonsWithNotifyClick); + 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 + }; +} + /** * Returns the list of participant menu buttons that have that notify the api when clicked. *