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; +}>;