From 0913554af97e91f14b5a63ce8c8579755f1405a7 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Fri, 17 May 2024 09:53:08 -0500 Subject: [PATCH] feat(MainToolbar): implement custom order. As part of the PR, it also fixes: - Removes button aliases - Unifies the keys in the object returned by getAllToolboxButtons and the button keys - Makes sure that the number of buttons displayed are always the same as the number of buttons specified in the thresholds and removes the exception for not filling up the main toolbar with buttons from overflow menu when reactions button is disabled. - Introduces a priority for buttons that will be used to fill empty spaces in the main toolbar. --- config.js | 16 +++++ react/features/base/config/configType.ts | 1 + react/features/base/config/configWhitelist.ts | 1 + react/features/toolbox/actionTypes.ts | 10 +++ react/features/toolbox/actions.web.ts | 53 ++++++++++++++++ .../toolbox/components/web/Toolbox.tsx | 61 ++++++++++++------- react/features/toolbox/constants.ts | 48 +++++++++++++-- react/features/toolbox/functions.web.ts | 39 ++++++------ react/features/toolbox/middleware.web.ts | 4 ++ react/features/toolbox/reducer.ts | 16 ++++- react/features/toolbox/types.ts | 6 +- 11 files changed, 205 insertions(+), 50 deletions(-) diff --git a/config.js b/config.js index 1e0921880d..5b6675c02e 100644 --- a/config.js +++ b/config.js @@ -848,6 +848,22 @@ var config = { // autoHideWhileChatIsOpen: false, // }, + // Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed + // buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the + // corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with + // more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a cerain + // configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used. + // The order of the buttons in the array is preserved. + // mainToolbarButtons: [ + // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ], + // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ], + // [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ], + // [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ], + // [ 'microphone', 'camera', 'chat', 'participants-pane' ], + // [ 'microphone', 'camera', 'chat' ], + // [ 'microphone', 'camera' ] + // ], + // Toolbar buttons which have their click/tap event exposed through the API on // `toolbarButtonClicked`. Passing a string for the button key will // prevent execution of the click/tap routine; passing an object with `key` and diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index e70f5652f3..20e6dd5d69 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -441,6 +441,7 @@ export interface IConfig { }; localSubject?: string; locationURL?: URL; + mainToolbarButtons?: Array>; maxFullResolutionParticipants?: number; microsoftApiApplicationClientID?: string; moderatedRoomServiceUrl?: string; diff --git a/react/features/base/config/configWhitelist.ts b/react/features/base/config/configWhitelist.ts index 6db06729ac..e57f467301 100644 --- a/react/features/base/config/configWhitelist.ts +++ b/react/features/base/config/configWhitelist.ts @@ -185,6 +185,7 @@ export default [ 'localRecording', 'localSubject', 'logging', + 'mainToolbarButtons', 'maxFullResolutionParticipants', 'mouseMoveCallbackInterval', 'notifications', diff --git a/react/features/toolbox/actionTypes.ts b/react/features/toolbox/actionTypes.ts index ed7ba15b93..a594298df7 100644 --- a/react/features/toolbox/actionTypes.ts +++ b/react/features/toolbox/actionTypes.ts @@ -59,6 +59,16 @@ export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; */ export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE'; +/** + * The type of the (redux) action which sets the main toolbar thresholds. + * + * { + * type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS, + * mainToolbarButtonsThresholds: IMainToolbarButtonThresholds + * } + */ +export const SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS = 'SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS'; + /** * The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers. */ diff --git a/react/features/toolbox/actions.web.ts b/react/features/toolbox/actions.web.ts index 7d94aca839..ea9c2d073f 100644 --- a/react/features/toolbox/actions.web.ts +++ b/react/features/toolbox/actions.web.ts @@ -8,13 +8,16 @@ import { FULL_SCREEN_CHANGED, SET_FULL_SCREEN, SET_HANGUP_MENU_VISIBLE, + SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS, SET_OVERFLOW_DRAWER, SET_OVERFLOW_MENU_VISIBLE, SET_TOOLBAR_HOVERED, SET_TOOLBOX_TIMEOUT } from './actionTypes'; import { setToolboxVisible } from './actions.web'; +import { THRESHOLDS } from './constants'; import { getToolbarTimeout } from './functions.web'; +import { IMainToolbarButtonThresholds } from './types'; export * from './actions.any'; @@ -121,6 +124,56 @@ export function setFullScreen(fullScreen: boolean) { }; } +/** + * Sets the mainToolbarButtonsThresholds. + * + * @returns {Function} + */ +export function setMainToolbarThresholds() { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const { mainToolbarButtons } = getState()['features/base/config']; + + if (!mainToolbarButtons || !Array.isArray(mainToolbarButtons) || mainToolbarButtons.length === 0) { + return; + } + + const mainToolbarButtonsThresholds: IMainToolbarButtonThresholds = []; + + const mainToolbarButtonsLenghtMap = new Map(); + let orderIsChanged = false; + + mainToolbarButtons.forEach(buttons => { + if (!Array.isArray(buttons) || buttons.length === 0) { + return; + } + + mainToolbarButtonsLenghtMap.set(buttons.length, buttons); + }); + + THRESHOLDS.forEach(({ width, order }) => { + let finalOrder = mainToolbarButtonsLenghtMap.get(order.length); + + if (finalOrder) { + orderIsChanged = true; + } else { + finalOrder = order; + } + + mainToolbarButtonsThresholds.push({ + order: finalOrder, + width + }); + }); + + if (orderIsChanged) { + dispatch({ + type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS, + mainToolbarButtonsThresholds + }); + } + }; +} + /** * Shows the toolbox for specified timeout. * diff --git a/react/features/toolbox/components/web/Toolbox.tsx b/react/features/toolbox/components/web/Toolbox.tsx index 82343a8a7b..07a7c39e77 100644 --- a/react/features/toolbox/components/web/Toolbox.tsx +++ b/react/features/toolbox/components/web/Toolbox.tsx @@ -15,7 +15,7 @@ import { setToolbarHovered, showToolbox } from '../../actions.web'; -import { NOT_APPLICABLE, THRESHOLDS } from '../../constants'; +import { MAIN_TOOLBAR_BUTTONS_PRIORITY } from '../../constants'; import { getAllToolboxButtons, getJwtDisabledButtons, @@ -23,7 +23,7 @@ import { isToolboxVisible } from '../../functions.web'; import { useKeyboardShortcuts } from '../../hooks.web'; -import { IToolboxButton, NOTIFY_CLICK_MODE } from '../../types'; +import { IToolboxButton, NOTIFY_CLICK_MODE, ToolbarButton } from '../../types'; import HangupButton from '../HangupButton'; import { EndConferenceButton } from './EndConferenceButton'; @@ -92,6 +92,14 @@ interface IProps extends WithTranslation { */ _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. */ @@ -174,6 +182,7 @@ const Toolbox = ({ _isMobile, _isNarrowLayout, _jwtDisabledButtons, + _mainToolbarButtonsThresholds, _overflowDrawer, _overflowMenuVisible, _reactionsButtonEnabled, @@ -280,35 +289,42 @@ const Toolbox = ({ 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 isHangupVisible = isButtonEnabled('hangup', _toolbarButtons); - const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width) - || THRESHOLDS[THRESHOLDS.length - 1]; + const { order } = _mainToolbarButtonsThresholds.find(({ width }) => _clientWidth > width) + || _mainToolbarButtonsThresholds[_mainToolbarButtonsThresholds.length - 1]; - const keys = Object.keys(buttons); + 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 filtered = [ - ...order.map(key => buttons[key as keyof typeof buttons]), - ...Object.values(buttons).filter((button, index) => !order.includes(keys[index])) - ].filter(({ key, alias = NOT_APPLICABLE }) => - !_jwtDisabledButtons.includes(key) - && (isButtonEnabled(key, _toolbarButtons) || isButtonEnabled(alias, _toolbarButtons)) - ); + const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length); + const overflowMenuButtons = filteredButtons.reduce((acc, key) => { + if (!mainButtonsKeys.includes(key)) { + acc.push(buttons[key]); + } - let sliceIndex = _overflowDrawer || _reactionsButtonEnabled ? order.length + 2 : order.length + 1; + return acc; + }, [] as IToolboxButton[]); - if (isHangupVisible) { - sliceIndex -= 1; - } + // 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; - // This implies that the overflow button will be displayed, so save some space for it. - if (sliceIndex < filtered.length) { - sliceIndex -= 1; + button && mainButtonsKeys.push(button); } return { - mainMenuButtons: filtered.slice(0, sliceIndex), - overflowMenuButtons: filtered.slice(sliceIndex) + mainMenuButtons: mainButtonsKeys.map(key => buttons[key]), + overflowMenuButtons }; } @@ -505,6 +521,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { _jwtDisabledButtons: getJwtDisabledButtons(state), _hangupMenuVisible: hangupMenuVisible, _isNarrowLayout: isNarrowLayout, + _mainToolbarButtonsThresholds: state['features/toolbox'].mainToolbarButtonsThresholds, _overflowMenuVisible: overflowMenuVisible, _overflowDrawer: overflowDrawer, _reactionsButtonEnabled: isReactionsButtonEnabled(state), diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts index d50bd1d2e3..dbaada03f5 100644 --- a/react/features/toolbox/constants.ts +++ b/react/features/toolbox/constants.ts @@ -6,23 +6,23 @@ import { ToolbarButton } from './types'; export const THRESHOLDS = [ { width: 565, - order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants', 'tileview' ] + order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ] }, { width: 520, - order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants', 'tileview' ] + order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ] }, { width: 470, - order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants' ] + order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ] }, { width: 420, - order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants' ] + order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ] }, { width: 370, - order: [ 'microphone', 'camera', 'chat', 'participants' ] + order: [ 'microphone', 'camera', 'chat', 'participants-pane' ] }, { width: 225, @@ -34,7 +34,43 @@ export const THRESHOLDS = [ } ]; -export const NOT_APPLICABLE = 'N/A'; +/** + * Main toolbar buttons priority used to determine which button should be picked to fill empty spaces for disabled + * buttons. + */ +export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [ + 'microphone', + 'camera', + 'desktop', + 'chat', + 'raisehand', + 'reactions', + 'participants-pane', + 'tileview', + 'invite', + 'toggle-camera', + 'videoquality', + 'fullscreen', + 'security', + 'closedcaptions', + 'recording', + 'livestreaming', + 'linktosalesforce', + 'sharedvideo', + 'shareaudio', + 'noisesuppression', + 'whiteboard', + 'etherpad', + 'select-background', + 'stats', + 'settings', + 'shortcuts', + 'profile', + 'embedmeeting', + 'feedback', + 'download', + 'help' +]; export const TOOLBAR_TIMEOUT = 4000; diff --git a/react/features/toolbox/functions.web.ts b/react/features/toolbox/functions.web.ts index acbcfbd212..601e14a190 100644 --- a/react/features/toolbox/functions.web.ts +++ b/react/features/toolbox/functions.web.ts @@ -271,7 +271,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: { group: 2 }; - const videoQuality = { + const videoquality = { key: 'videoquality', Content: VideoQualityButton, group: 2 @@ -285,12 +285,11 @@ export function getAllToolboxButtons(_customToolbarButtons?: { const security = { key: 'security', - alias: 'info', Content: SecurityDialogButton, group: 2 }; - const cc = { + const closedcaptions = { key: 'closedcaptions', Content: ClosedCaptionButton, group: 2 @@ -308,25 +307,25 @@ export function getAllToolboxButtons(_customToolbarButtons?: { group: 2 }; - const linkToSalesforce = { + const linktosalesforce = { key: 'linktosalesforce', Content: LinkToSalesforceButton, group: 2 }; - const shareVideo = { + const sharedvideo = { key: 'sharedvideo', Content: SharedVideoButton, group: 3 }; - const shareAudio = { + const shareaudio = { key: 'shareaudio', Content: ShareAudioButton, group: 3 }; - const noiseSuppression = { + const noisesuppression = { key: 'noisesuppression', Content: NoiseSuppressionButton, group: 3 @@ -351,7 +350,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: { group: 3 }; - const speakerStats = { + const stats = { key: 'stats', Content: SpeakerStatsButton, group: 3 @@ -369,7 +368,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: { group: 4 }; - const embed = { + const embedmeeting = { key: 'embedmeeting', Content: EmbedMeetingButton, group: 4 @@ -415,27 +414,27 @@ export function getAllToolboxButtons(_customToolbarButtons?: { chat, raisehand, reactions, - participants, + 'participants-pane': participants, invite, tileview, - toggleCamera, - videoQuality, + 'toggle-camera': toggleCamera, + videoquality, fullscreen, security, - cc, + closedcaptions, recording, livestreaming, - linkToSalesforce, - shareVideo, - shareAudio, - noiseSuppression, + linktosalesforce, + sharedvideo, + shareaudio, + noisesuppression, whiteboard, etherpad, - virtualBackground, - speakerStats, + 'select-background': virtualBackground, + stats, settings, shortcuts, - embed, + embedmeeting, feedback, download, help, diff --git a/react/features/toolbox/middleware.web.ts b/react/features/toolbox/middleware.web.ts index 1f2d94bfab..afe90c7aef 100644 --- a/react/features/toolbox/middleware.web.ts +++ b/react/features/toolbox/middleware.web.ts @@ -16,6 +16,7 @@ import { SET_TOOLBAR_BUTTONS, SET_TOOLBOX_TIMEOUT } from './actionTypes'; +import { setMainToolbarThresholds } from './actions.web'; import { TOOLBAR_BUTTONS, VISITORS_MODE_BUTTONS } from './constants'; import { NOTIFY_CLICK_MODE } from './types'; @@ -53,6 +54,9 @@ MiddlewareRegistry.register(store => next => action => { } = state['features/base/config']; batch(() => { + if (action.type !== I_AM_VISITOR_MODE) { + dispatch(setMainToolbarThresholds()); + } dispatch({ type: SET_BUTTONS_WITH_NOTIFY_CLICK, buttonsWithNotifyClick: _buildButtonsArray(buttonsWithNotifyClick, customToolbarButtons) diff --git a/react/features/toolbox/reducer.ts b/react/features/toolbox/reducer.ts index eac258d283..23dd00206b 100644 --- a/react/features/toolbox/reducer.ts +++ b/react/features/toolbox/reducer.ts @@ -6,6 +6,7 @@ import { FULL_SCREEN_CHANGED, SET_BUTTONS_WITH_NOTIFY_CLICK, SET_HANGUP_MENU_VISIBLE, + SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS, SET_OVERFLOW_DRAWER, SET_OVERFLOW_MENU_VISIBLE, SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK, @@ -17,7 +18,8 @@ import { SET_TOOLBOX_VISIBLE, TOGGLE_TOOLBOX_VISIBLE } from './actionTypes'; -import { NOTIFY_CLICK_MODE } from './types'; +import { THRESHOLDS } from './constants'; +import { IMainToolbarButtonThresholds, NOTIFY_CLICK_MODE } from './types'; /** * Initial state of toolbox's part of Redux store. @@ -47,6 +49,11 @@ const INITIAL_STATE = { */ hovered: false, + /** + * The thresholds for screen size and visible main toolbar buttons. + */ + mainToolbarButtonsThresholds: THRESHOLDS, + participantMenuButtonsWithNotifyClick: new Map(), /** @@ -98,6 +105,7 @@ export interface IToolboxState { fullScreen?: boolean; hangupMenuVisible: boolean; hovered: boolean; + mainToolbarButtonsThresholds: IMainToolbarButtonThresholds; overflowDrawer: boolean; overflowMenuVisible: boolean; participantMenuButtonsWithNotifyClick: Map; @@ -151,6 +159,12 @@ ReducerRegistry.register( ...state, buttonsWithNotifyClick: action.buttonsWithNotifyClick }; + + case SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS: + return { + ...state, + mainToolbarButtonsThresholds: action.mainToolbarButtonsThresholds + }; case SET_TOOLBAR_HOVERED: return { ...state, diff --git a/react/features/toolbox/types.ts b/react/features/toolbox/types.ts index 9be8155ba3..6fe42702a7 100644 --- a/react/features/toolbox/types.ts +++ b/react/features/toolbox/types.ts @@ -2,7 +2,6 @@ import { ComponentType } from 'react'; export interface IToolboxButton { Content: ComponentType; - alias?: string; group: number; key: string; } @@ -49,3 +48,8 @@ export enum NOTIFY_CLICK_MODE { ONLY_NOTIFY = 'ONLY_NOTIFY', PREVENT_AND_NOTIFY = 'PREVENT_AND_NOTIFY' } + +export type IMainToolbarButtonThresholds = Array<{ + order: Array; + width: number; +}>;