Files
jitsi-meet/react/features/toolbox/components/web/Toolbox.tsx

534 lines
18 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useEffect, useRef } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
2017-02-16 17:02:40 -06:00
import { IReduxState, IStore } from '../../../app/types';
import { NotifyClickButton } from '../../../base/config/configType';
import { VISITORS_MODE_BUTTONS } from '../../../base/config/constants';
import {
getButtonNotifyMode,
getButtonsWithNotifyClick,
getToolbarButtons,
isToolbarButtonEnabled
} from '../../../base/config/functions.web';
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';
2024-02-01 12:54:40 -06:00
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web';
2023-02-23 12:43:16 -06:00
import { iAmVisitor } from '../../../visitors/functions';
import {
setHangupMenuVisible,
setOverflowMenuVisible,
setToolbarHovered,
showToolbox
2023-04-07 13:22:34 +03:00
} from '../../actions.web';
import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
2023-04-07 13:22:34 +03:00
import {
getAllToolboxButtons,
2023-04-07 13:22:34 +03:00
getJwtDisabledButtons,
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts } from '../../hooks.web';
import { IToolboxButton } from '../../types';
import HangupButton from '../HangupButton';
2020-05-20 12:57:03 +02:00
import { EndConferenceButton } from './EndConferenceButton';
import HangupMenuButton from './HangupMenuButton';
import { LeaveConferenceButton } from './LeaveConferenceButton';
import OverflowMenuButton from './OverflowMenuButton';
import Separator from './Separator';
2018-05-10 21:10:26 -05:00
/**
* 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?: NotifyClickButton[];
/**
* 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[];
/**
* Whether or not the overflow menu is displayed in a drawer drawer.
*/
_overflowDrawer: boolean;
/**
* Whether or not the overflow menu is visible.
*/
_overflowMenuVisible: boolean;
2023-04-27 19:19:53 -05:00
/**
* Whether or not to display reactions in separate button.
*/
_reactionsButtonEnabled: boolean;
/**
2024-02-01 12:54:40 -06:00
* Whether the toolbox should be shifted up or not.
*/
2024-02-01 12:54:40 -06:00
_shiftUp: boolean;
/**
2024-02-01 12:54:40 -06:00
* Whether any reactions buttons should be displayed or not.
*/
2024-02-01 12:54:40 -06:00
_shouldDisplayReactionsButtons: boolean;
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 07:36:19 -05:00
/**
* The enabled buttons.
*/
_toolbarButtons: Array<string>;
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 07:36:19 -05:00
/**
* 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,
2023-01-25 23:39:55 +05:30
marginBottom: '8px',
maxHeight: 'calc(100dvh - 100px)',
minWidth: '240px'
},
2022-09-13 10:36:00 +03:00
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,
_overflowDrawer,
_overflowMenuVisible,
_reactionsButtonEnabled,
_shiftUp,
2024-02-01 12:54:40 -06:00
_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));
}, []);
2017-02-16 17:02:40 -06:00
/**
* Sets the visibility of the overflow menu.
2017-02-16 17:02:40 -06:00
*
* @param {boolean} visible - Whether or not the overflow menu should be
* displayed.
* @private
* @returns {void}
2017-02-16 17:02:40 -06:00
*/
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 ]);
2017-02-16 17:02:40 -06:00
/**
* 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?.length) {
return;
}
Object.values(buttons).forEach((button: any) => {
if (typeof button === 'object') {
button.notifyMode = getButtonNotifyMode(button.key, _buttonsWithNotifyClick);
}
});
}
/**
* 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);
setButtonsNotifyClickMode(buttons);
feat: Participants optimisations (#9515) * fix(participants): Change from array to Map * fix(unload): optimise * feat: Introduces new states for e2ee feature. Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list. squash: Uses participants map and go over the elements only once. * feat: Optimizes isEveryoneModerator to do less frequent checks in all participants. * fix: Drops deep equal from participants pane and uses the map. * fix(SharedVideo): isVideoPlaying * fix(participants): Optimise isEveryoneModerator * fix(e2e): Optimise everyoneEnabledE2EE * fix: JS errors. * ref(participants): remove getParticipants * fix(participants): Prepare for PR. * fix: Changes participants pane to be component. The functional component was always rendered: `prev props: {} !== {} :next props`. * feat: Optimization to skip participants list on pane closed. * fix: The participants list shows and the local participant. * fix: Fix wrong action name for av-moderation. * fix: Minimizes the number of render calls of av moderation notification. * fix: Fix iterating over remote participants. * fix: Fixes lint error. * fix: Reflects participant updates for av-moderation. * fix(ParticipantPane): to work with IDs. * fix(av-moderation): on PARTCIPANT_UPDATE * fix(ParticipantPane): close delay. * fix: address code review comments * fix(API): mute-everyone * fix: bugs * fix(Thumbnail): on mobile. * fix(ParticipantPane): Close context menu on click. * fix: Handles few error when local participant is undefined. * feat: Hides AV moderation if not supported. * fix: Show mute all video. * fix: Fixes updating participant for av moderation. Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 07:36:19 -05:00
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
const keys = Object.keys(buttons);
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)
&& (isToolbarButtonEnabled(key, _toolbarButtons) || isToolbarButtonEnabled(alias, _toolbarButtons))
);
let sliceIndex = _overflowDrawer || _reactionsButtonEnabled ? order.length + 2 : order.length + 1;
if (isHangupVisible) {
sliceIndex -= 1;
}
// This implies that the overflow button will be displayed, so save some space for it.
if (sliceIndex < filtered.length) {
sliceIndex -= 1;
}
return {
mainMenuButtons: filtered.slice(0, sliceIndex),
overflowMenuButtons: filtered.slice(sliceIndex)
};
}
/**
* Dispatches an action signaling the toolbar is not being hovered.
*
* @private
* @returns {void}
*/
function onMouseOut() {
!_overflowMenuVisible && dispatch(setToolbarHovered(false));
2017-02-16 17:02:40 -06:00
}
/**
* Dispatches an action signaling the toolbar is being hovered.
2017-02-16 17:02:40 -06:00
*
* @private
* @returns {void}
2017-02-16 17:02:40 -06:00
*/
function onMouseOver() {
dispatch(setToolbarHovered(true));
}
/**
* Toggle the toolbar visibility when tabbing into it.
*
* @returns {void}
*/
const onTabIn = useCallback(() => {
if (!_visible) {
dispatch(showToolbox());
}
}, [ _visible ]);
2019-02-20 23:35:19 +00:00
/**
* Renders the toolbox content.
*
2021-02-23 13:09:22 +02:00
* @returns {ReactElement}
2019-02-20 23:35:19 +00:00
*/
function renderToolboxContent() {
2021-02-23 13:09:22 +02:00
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons();
2023-04-27 19:19:53 -05:00
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
2024-02-01 12:54:40 -06:00
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!_reactionsButtonEnabled && (raiseHandInOverflowMenu || _isNarrowLayout || _isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions')
);
2023-04-27 19:19:53 -05:00
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
2019-02-20 23:35:19 +00:00
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) => {
2023-04-27 19:19:53 -05:00
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 ]);
}
2023-04-27 19:19:53 -05:00
} else {
acc.push([ val ]);
}
2023-04-27 19:19:53 -05:00
return acc;
}, []) }
isOpen = { _overflowMenuVisible }
key = 'overflow-menu'
onToolboxEscKey = { onEscKey }
onVisibilityChange = { onSetOverflowVisible }
2023-04-27 19:19:53 -05:00
showRaiseHandInReactionsMenu = { showRaiseHandInReactionsMenu }
showReactionsMenu = { showReactionsInOverflowMenu } />
)}
{isToolbarButtonEnabled('hangup', _toolbarButtons) && (
_endConferenceSupported
? <HangupMenuButton
ariaControls = 'hangup-menu'
isOpen = { _hangupMenuVisible }
key = 'hangup-menu'
notifyMode = { getButtonNotifyMode('hangup-menu', _buttonsWithNotifyClick) }
onVisibilityChange = { onSetHangupVisible }>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.hangupMenu }
hidden = { false }
inDrawer = { _overflowDrawer }
onKeyDown = { onEscKey }>
<EndConferenceButton
buttonKey = 'end-meeting'
notifyMode = { getButtonNotifyMode(
'end-meeting',
_buttonsWithNotifyClick
) } />
<LeaveConferenceButton
buttonKey = 'hangup'
notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) } />
</ContextMenu>
</HangupMenuButton>
: <HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) }
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
)}
</div>
2019-02-20 23:35:19 +00:00
</div>
2021-02-23 13:09:22 +02:00
</div>
);
2019-02-20 23:35:19 +00:00
}
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>
);
};
2017-02-16 17:02:40 -06:00
/**
* Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component}
* props.
2017-02-16 17:02:40 -06:00
*
* @param {Object} state - The redux store/state.
* @param {Object} ownProps - The props explicitly passed.
2017-02-16 17:02:40 -06:00
* @private
* @returns {{}}
2017-02-16 17:02:40 -06:00
*/
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'];
2017-02-16 17:02:40 -06:00
const {
hangupMenuVisible,
overflowMenuVisible,
overflowDrawer
2017-04-01 00:52:40 -05:00
} = state['features/toolbox'];
const { clientWidth } = state['features/base/responsive-ui'];
let toolbarButtons = ownProps.toolbarButtons || getToolbarButtons(state);
2023-02-23 12:43:16 -06:00
if (iAmVisitor(state)) {
toolbarButtons = VISITORS_MODE_BUTTONS.filter(e => toolbarButtons.indexOf(e) > -1);
}
2017-02-16 17:02:40 -06:00
return {
_buttonsWithNotifyClick: getButtonsWithNotifyClick(state),
_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,
_overflowMenuVisible: overflowMenuVisible,
_overflowDrawer: overflowDrawer,
2023-04-27 19:19:53 -05:00
_reactionsButtonEnabled: isReactionsButtonEnabled(state),
_shiftUp: state['features/toolbox'].shiftUp,
2024-02-01 12:54:40 -06:00
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
_toolbarButtons: toolbarButtons,
_visible: isToolboxVisible(state)
2017-02-16 17:02:40 -06:00
};
}
export default translate(connect(_mapStateToProps)(Toolbox));