feat(shortcuts) Update dialog (#12993)

Create Shortcuts tab in Settings Dialog
Move keyboard shortcut option from More to this tab
Move shortcuts info from KeyboardShortcuts dialog to this tab
Remove KeyboardShortcuts dialog
This commit is contained in:
Robert Pintilii 2023-03-03 13:48:17 +02:00 committed by GitHub
parent 036286a1d6
commit 7b8b911fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 264 additions and 206 deletions

View File

@ -1006,6 +1006,7 @@
"selectCamera": "Camera", "selectCamera": "Camera",
"selectMic": "Microphone", "selectMic": "Microphone",
"selfView": "Self view", "selfView": "Self view",
"shortcuts": "Shortcuts",
"speakers": "Speakers", "speakers": "Speakers",
"startAudioMuted": "Everyone starts muted", "startAudioMuted": "Everyone starts muted",
"startReactionsMuted": "Mute reaction sounds for everyone", "startReactionsMuted": "Mute reaction sounds for everyone",

View File

@ -8,10 +8,9 @@ import {
createShortcutEvent, createShortcutEvent,
sendAnalytics sendAnalytics
} from '../../react/features/analytics'; } from '../../react/features/analytics';
import { toggleDialog } from '../../react/features/base/dialog';
import { clickOnVideo } from '../../react/features/filmstrip/actions'; import { clickOnVideo } from '../../react/features/filmstrip/actions';
import { KeyboardShortcutsDialog } import { openSettingsDialog } from '../../react/features/settings/actions';
from '../../react/features/keyboard-shortcuts'; import { SETTINGS_TABS } from '../../react/features/settings/constants';
const logger = Logger.getLogger(__filename); const logger = Logger.getLogger(__filename);
@ -120,15 +119,17 @@ const KeyboardShortcut = {
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true; return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
}, },
getShortcutsDescriptions() {
return _shortcutsHelp;
},
/** /**
* Opens the {@KeyboardShortcutsDialog} dialog. * Opens the {@SettingsDialog} dialog on the Shortcuts page.
* *
* @returns {void} * @returns {void}
*/ */
openDialog() { openDialog() {
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
shortcutDescriptions: _shortcutsHelp
}));
}, },
/** /**

View File

@ -5,7 +5,6 @@ import '../base/media/middleware';
import '../dynamic-branding/middleware'; import '../dynamic-branding/middleware';
import '../e2ee/middleware'; import '../e2ee/middleware';
import '../external-api/middleware'; import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';
import '../no-audio-signal/middleware'; import '../no-audio-signal/middleware';
import '../notifications/middleware'; import '../notifications/middleware';
import '../noise-detection/middleware'; import '../noise-detection/middleware';

View File

@ -131,6 +131,7 @@ const useStyles = makeStyles()(theme => {
footer: { footer: {
justifyContent: 'flex-end', justifyContent: 'flex-end',
paddingTop: theme.spacing(4),
'& button:last-child': { '& button:last-child': {
marginLeft: '16px' marginLeft: '16px'

View File

@ -1,10 +0,0 @@
/**
* The type of the action which signals the keyboard shortcuts dialog should
* be displayed.
*
* {
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
* }
*/
export const OPEN_KEYBOARD_SHORTCUTS_DIALOG
= 'OPEN_KEYBOARD_SHORTCUTS_DIALOG';

View File

@ -1,14 +0,0 @@
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
/**
* Opens the dialog showing available keyboard shortcuts.
*
* @returns {{
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
* }}
*/
export function openKeyboardShortcutsDialog() {
return {
type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
};
}

View File

@ -5,7 +5,8 @@ import { translate } from '../../../base/i18n';
import { IconShortcuts } from '../../../base/icons'; import { IconShortcuts } from '../../../base/icons';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openKeyboardShortcutsDialog } from '../../actions'; import { openSettingsDialog } from '../../../settings/actions';
import { SETTINGS_TABS } from '../../../settings/constants';
/** /**
* The type of the React {@code Component} props of {@link KeyboardShortcutsButton}. * The type of the React {@code Component} props of {@link KeyboardShortcutsButton}.
@ -37,7 +38,7 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
const { dispatch } = this.props; const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('shortcuts')); sendAnalytics(createToolbarEvent('shortcuts'));
dispatch(openKeyboardShortcutsDialog()); dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS));
} }
} }

View File

@ -1,102 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Dialog from '../../../base/ui/components/web/Dialog';
/**
* The type of the React {@code Component} props of
* {@link KeyboardShortcutsDialog}.
*/
interface IProps {
/**
* A Map with keyboard keys as keys and translation keys as values.
*/
shortcutDescriptions: Map<string, string>;
}
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const useStyles = makeStyles()(theme => {
return {
list: {
listStyleType: 'none',
padding: 0,
'& .shortcuts-list__item': {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing(1)} 0`,
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01
},
'& .item-action': {
backgroundColor: theme.palette.ui04,
...withPixelLineHeight(theme.typography.labelBold),
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
}
}
};
});
const KeyboardShortcutsDialog = ({ shortcutDescriptions }: IProps) => {
const { classes, cx } = useStyles();
const { t } = useTranslation();
// eslint-disable-next-line react/no-multi-comp
const _renderShortcutsListItem = (keyboardKey: string, translationKey: string) => {
let modifierKey = 'Alt';
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return (
<li
className = 'shortcuts-list__item'
key = { keyboardKey }>
<span
aria-label = { t(translationKey) }
className = 'shortcuts-list__description'>
{t(translationKey)}
</span>
<span className = 'item-action'>
{keyboardKey.startsWith(':')
? `${modifierKey} + ${keyboardKey.slice(1)}`
: keyboardKey}
</span>
</li>
);
};
return (
<Dialog
cancel = {{ hidden: true }}
ok = {{ hidden: true }}
titleKey = 'keyboardShortcuts.keyboardShortcuts'>
<div
id = 'keyboard-shortcuts'>
<ul
className = { cx('shortcuts-list', classes.list) }
id = 'keyboard-shortcuts-list'>
{Array.from(shortcutDescriptions)
.map(description => _renderShortcutsListItem(...description))}
</ul>
</div>
</Dialog>
);
};
export default KeyboardShortcutsDialog;

View File

@ -1,2 +1 @@
export { default as KeyboardShortcutsButton } from './KeyboardShortcutsButton'; export { default as KeyboardShortcutsButton } from './KeyboardShortcutsButton';
export { default as KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';

View File

@ -1,2 +1 @@
export * from './actions';
export * from './components'; export * from './components';

View File

@ -1,21 +0,0 @@
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
/**
* Implements the middleware of the feature keyboard-shortcuts.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(_store => next => action => {
switch (action.type) {
case OPEN_KEYBOARD_SHORTCUTS_DIALOG:
if (typeof APP === 'object') {
APP.keyboardshortcut.openDialog();
}
break;
}
return next(action);
});

View File

@ -1,5 +1,7 @@
import { batch } from 'react-redux'; import { batch } from 'react-redux';
// @ts-expect-error
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
import { IStore } from '../app/types'; import { IStore } from '../app/types';
import { import {
setFollowMe, setFollowMe,
@ -22,7 +24,8 @@ import {
getModeratorTabProps, getModeratorTabProps,
getMoreTabProps, getMoreTabProps,
getNotificationsTabProps, getNotificationsTabProps,
getProfileTabProps getProfileTabProps,
getShortcutsTabProps
} from './functions'; } from './functions';
/** /**
@ -237,3 +240,19 @@ export function toggleVideoSettings() {
dispatch(setVideoSettingsVisibility(!value)); dispatch(setVideoSettingsVisibility(!value));
}; };
} }
/**
* Submits the settings from the "Shortcuts" tab of the settings dialog.
*
* @param {Object} newState - The new settings.
* @returns {Function}
*/
export function submitShortcutsTab(newState: any) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const currentState = getShortcutsTabProps(getState());
if (newState.keyboardShortcutsEnabled !== currentState.keyboardShortcutsEnabled) {
keyboardShortcut.enable(newState.keyboardShortcutsEnabled);
}
};
}

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import { WithTranslation } from 'react-i18next'; import { WithTranslation } from 'react-i18next';
// @ts-expect-error
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
import AbstractDialogTab, { import AbstractDialogTab, {
IProps as AbstractDialogTabProps IProps as AbstractDialogTabProps
} from '../../../base/dialog/components/web/AbstractDialogTab'; } from '../../../base/dialog/components/web/AbstractDialogTab';
@ -81,7 +79,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this); this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this); this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this); this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
} }
@ -132,19 +129,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
super._onChange({ showPrejoinPage: checked }); super._onChange({ showPrejoinPage: checked });
} }
/**
* Callback invoked to select if global keyboard shortcuts
* should be enabled.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyboardShortcutEnableChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
keyboardShortcut.enable(checked);
super._onChange({ keyboardShortcutEnable: checked });
}
/** /**
* Callback invoked to select a max number of stage participants from the select dropdown. * Callback invoked to select a max number of stage participants from the select dropdown.
* *
@ -190,31 +174,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
); );
} }
/**
* Returns the React Element for keyboardShortcut settings.
*
* @private
* @returns {ReactElement}
*/
_renderKeyboardShortcutCheckbox() {
const { t } = this.props;
return (
<div
className = 'settings-sub-pane-element'
key = 'keyboard-shortcut'>
<span className = 'checkbox-label'>
{ t('keyboardShortcuts.keyboardShortcuts') }
</span>
<Checkbox
checked = { keyboardShortcut.getEnabled() }
label = { t('prejoin.keyboardShortcuts') }
name = 'enable-keyboard-shortcuts'
onChange = { this._onKeyboardShortcutEnableChanged } />
</div>
);
}
/** /**
* Returns the React Element for modifying prejoin screen settings. * Returns the React Element for modifying prejoin screen settings.
* *
@ -304,7 +263,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
className = 'settings-sub-pane left' className = 'settings-sub-pane left'
key = 'settings-sub-pane-left'> key = 'settings-sub-pane-left'>
{ showPrejoinSettings && this._renderPrejoinScreenSettings() } { showPrejoinSettings && this._renderPrejoinScreenSettings() }
{ this._renderKeyboardShortcutCheckbox() }
</div> </div>
); );
} }

View File

@ -4,7 +4,15 @@ import { withStyles } from '@mui/styles';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { IReduxState } from '../../../app/types'; import { IReduxState } from '../../../app/types';
import { IconBell, IconCalendar, IconGear, IconHost, IconUser, IconVolumeUp } from '../../../base/icons/svg'; import {
IconBell,
IconCalendar,
IconGear,
IconHost,
IconShortcuts,
IconUser,
IconVolumeUp
} from '../../../base/icons/svg';
import { connect } from '../../../base/redux/functions'; import { connect } from '../../../base/redux/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web'; import { withPixelLineHeight } from '../../../base/styles/functions.web';
import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs'; import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
@ -19,7 +27,8 @@ import {
submitModeratorTab, submitModeratorTab,
submitMoreTab, submitMoreTab,
submitNotificationsTab, submitNotificationsTab,
submitProfileTab submitProfileTab,
submitShortcutsTab
} from '../../actions'; } from '../../actions';
import { SETTINGS_TABS } from '../../constants'; import { SETTINGS_TABS } from '../../constants';
import { import {
@ -27,7 +36,8 @@ import {
getMoreTabProps, getMoreTabProps,
getNotificationsMap, getNotificationsMap,
getNotificationsTabProps, getNotificationsTabProps,
getProfileTabProps getProfileTabProps,
getShortcutsTabProps
} from '../../functions'; } from '../../functions';
// @ts-ignore // @ts-ignore
@ -36,6 +46,7 @@ import ModeratorTab from './ModeratorTab';
import MoreTab from './MoreTab'; import MoreTab from './MoreTab';
import NotificationsTab from './NotificationsTab'; import NotificationsTab from './NotificationsTab';
import ProfileTab from './ProfileTab'; import ProfileTab from './ProfileTab';
import ShortcutsTab from './ShortcutsTab';
/* eslint-enable lines-around-comment */ /* eslint-enable lines-around-comment */
/** /**
@ -342,6 +353,24 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
}); });
} }
tabs.push({
name: SETTINGS_TABS.SHORTCUTS,
component: ShortcutsTab,
labelKey: 'settings.shortcuts',
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
propsUpdateFunction: (tabState: any, newProps: any) => {
// Updates tab props, keeping users selection
return {
...newProps,
keyboardShortcutsEnabled: tabState?.keyboardShortcutsEnabled
};
},
className: `settings-pane ${classes.settingsDialog}`,
submit: submitShortcutsTab,
icon: IconShortcuts
});
if (showMoreTab) { if (showMoreTab) {
tabs.push({ tabs.push({
name: SETTINGS_TABS.MORE, name: SETTINGS_TABS.MORE,

View File

@ -0,0 +1,174 @@
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react';
import { WithTranslation } from 'react-i18next';
// @ts-expect-error
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
import AbstractDialogTab, {
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../../base/i18n/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Checkbox from '../../../base/ui/components/web/Checkbox';
/**
* The type of the React {@code Component} props of {@link ShortcutsTab}.
*/
export interface IProps extends AbstractDialogTabProps, WithTranslation {
/**
* CSS classes object.
*/
classes: any;
/**
* Whether to display the shortcuts or not.
*/
displayShortcuts: boolean;
/**
* Wether the keyboard shortcuts are enabled or not.
*/
keyboardShortcutsEnabled: boolean;
}
const styles = (theme: Theme) => {
return {
container: {
display: 'flex',
flexDirection: 'column' as const,
width: '100%',
paddingBottom: theme.spacing(3)
},
checkbox: {
marginBottom: theme.spacing(3)
},
listContainer: {
listStyleType: 'none',
padding: 0,
margin: 0
},
listItem: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: `${theme.spacing(1)} 0`,
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01
},
listItemKey: {
backgroundColor: theme.palette.ui04,
...withPixelLineHeight(theme.typography.labelBold),
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
}
};
};
/**
* React {@code Component} for modifying the local user's profile.
*
* @augments Component
*/
class ShortcutsTab extends AbstractDialogTab<IProps, any> {
/**
* Initializes a new {@code MoreTab} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
this._renderShortcutsListItem = this._renderShortcutsListItem.bind(this);
}
/**
* Callback invoked to select if global keyboard shortcuts
* should be enabled.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyboardShortcutEnableChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
super._onChange({ keyboardShortcutsEnabled: checked });
}
/**
* Render a keyboard shortcut with key and description.
*
* @param {string} keyboardKey - The keyboard key for the shortcut.
* @param {string} translationKey - The translation key for the shortcut description.
* @returns {JSX}
*/
_renderShortcutsListItem(keyboardKey: string, translationKey: string) {
const { classes, t } = this.props;
let modifierKey = 'Alt';
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return (
<li
className = { classes.listItem }
key = { keyboardKey }>
<span
aria-label = { t(translationKey) }>
{t(translationKey)}
</span>
<span className = { classes.listItemKey }>
{keyboardKey.startsWith(':')
? `${modifierKey} + ${keyboardKey.slice(1)}`
: keyboardKey}
</span>
</li>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
classes,
displayShortcuts,
keyboardShortcutsEnabled,
t
} = this.props;
const shortcutDescriptions: Map<string, string> = displayShortcuts
? keyboardShortcut.getShortcutsDescriptions()
: new Map();
return (
<div className = { classes.container }>
<Checkbox
checked = { keyboardShortcutsEnabled }
className = { classes.checkbox }
label = { t('prejoin.keyboardShortcuts') }
name = 'enable-keyboard-shortcuts'
onChange = { this._onKeyboardShortcutEnableChanged } />
{displayShortcuts && (
<ul className = { classes.listContainer }>
{Array.from(shortcutDescriptions)
.map(description => this._renderShortcutsListItem(...description))}
</ul>
)}
</div>
);
}
}
export default withStyles(styles)(translate(ShortcutsTab));

View File

@ -4,7 +4,8 @@ export const SETTINGS_TABS = {
MORE: 'more_tab', MORE: 'more_tab',
MODERATOR: 'moderator-tab', MODERATOR: 'moderator-tab',
NOTIFICATIONS: 'notifications_tab', NOTIFICATIONS: 'notifications_tab',
PROFILE: 'profile_tab' PROFILE: 'profile_tab',
SHORTCUTS: 'shortcuts_tab'
}; };
/** /**

View File

@ -1,3 +1,5 @@
// @ts-expect-error
import keyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut';
import { IReduxState } from '../app/types'; import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types'; import { IStateful } from '../base/app/types';
import { isNameReadOnly } from '../base/config/functions'; import { isNameReadOnly } from '../base/config/functions';
@ -14,6 +16,7 @@ import { parseStandardURIString } from '../base/util/uri';
import { isStageFilmstripEnabled } from '../filmstrip/functions'; import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me/functions'; import { isFollowMeActive } from '../follow-me/functions';
import { getParticipantsPaneConfig } from '../participants-pane/functions'; import { getParticipantsPaneConfig } from '../participants-pane/functions';
import { isPrejoinPageVisible } from '../prejoin/functions';
import { isReactionsEnabled } from '../reactions/functions.any'; import { isReactionsEnabled } from '../reactions/functions.any';
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants'; import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
@ -275,3 +278,23 @@ export function getAudioSettingsVisibility(state: IReduxState) {
export function getVideoSettingsVisibility(state: IReduxState) { export function getVideoSettingsVisibility(state: IReduxState) {
return state['features/settings'].videoSettingsVisible; return state['features/settings'].videoSettingsVisible;
} }
/**
* Returns the properties for the "Shortcuts" tab from settings dialog from Redux
* state.
*
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the shortcuts dialog is displayed on the
* welcome page or not.
* @returns {Object} - The properties for the "Shortcuts" tab from settings
* dialog.
*/
export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePage?: boolean) {
const state = toState(stateful);
return {
displayShortcuts: !isDisplayedOnWelcomePage && !isPrejoinPageVisible(state),
keyboardShortcutsEnabled: keyboardShortcut.getEnabled()
};
}