mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-08-07 20:47:46 +00:00
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.
536 lines
18 KiB
TypeScript
536 lines
18 KiB
TypeScript
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<string, NOTIFY_CLICK_MODE>;
|
|
|
|
/**
|
|
* 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<ToolbarButton | string>;
|
|
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<string>;
|
|
|
|
/**
|
|
* 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<string>;
|
|
}
|
|
|
|
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<HTMLDivElement>(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 (
|
|
<div className = { containerClassName }>
|
|
<div
|
|
className = 'toolbox-content-wrapper'
|
|
onFocus = { onTabIn }
|
|
{ ...(_isMobile ? {} : {
|
|
onMouseOut,
|
|
onMouseOver
|
|
}) }>
|
|
|
|
<div
|
|
className = 'toolbox-content-items'
|
|
ref = { _toolboxRef }>
|
|
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && (
|
|
<Content
|
|
{ ...rest }
|
|
buttonKey = { key }
|
|
key = { key } />))}
|
|
|
|
{Boolean(overflowMenuButtons.length) && (
|
|
<OverflowMenuButton
|
|
ariaControls = 'overflow-menu'
|
|
buttons = { overflowMenuButtons.reduce<Array<IToolboxButton[]>>((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
|
|
? <HangupMenuButton
|
|
ariaControls = 'hangup-menu'
|
|
isOpen = { _hangupMenuVisible }
|
|
key = 'hangup-menu'
|
|
notifyMode = { _buttonsWithNotifyClick?.get('hangup-menu') }
|
|
onVisibilityChange = { onSetHangupVisible }>
|
|
<ContextMenu
|
|
accessibilityLabel = { t(toolbarAccLabel) }
|
|
className = { classes.hangupMenu }
|
|
hidden = { false }
|
|
inDrawer = { _overflowDrawer }
|
|
onKeyDown = { onEscKey }>
|
|
<EndConferenceButton
|
|
buttonKey = 'end-meeting'
|
|
notifyMode = { _buttonsWithNotifyClick?.get('end-meeting') } />
|
|
<LeaveConferenceButton
|
|
buttonKey = 'hangup'
|
|
notifyMode = { _buttonsWithNotifyClick?.get('hangup') } />
|
|
</ContextMenu>
|
|
</HangupMenuButton>
|
|
: <HangupButton
|
|
buttonKey = 'hangup'
|
|
customClass = 'hangup-button'
|
|
key = 'hangup-button'
|
|
notifyMode = { _buttonsWithNotifyClick.get('hangup') }
|
|
visible = { isButtonEnabled('hangup', _toolbarButtons) } />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (_disabled) {
|
|
return null;
|
|
}
|
|
|
|
const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${
|
|
_toolbarButtons.length ? '' : 'no-buttons'} ${_chatOpen ? 'shift-right' : ''}`;
|
|
|
|
return (
|
|
<div
|
|
className = { cx(rootClassNames, _shiftUp && 'shift-up') }
|
|
id = 'new-toolbox'>
|
|
{renderToolboxContent()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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));
|