refactor: Replace react-hotkeys with custom hook (#35425)

This commit is contained in:
diondiondion 2025-07-21 16:43:38 +02:00 committed by GitHub
commit 4de5cbd6f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 540 additions and 146 deletions

View file

@ -93,9 +93,12 @@ class ComposeForm extends ImmutablePureComponent {
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
if (['esc', 'escape'].includes(e.key.toLowerCase())) {
this.textareaRef.current?.blur();
}
};
getFulltextForCharacterCounting = () => {

View file

@ -10,15 +10,13 @@ import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { useDispatch, useSelector } from 'react-redux';
import { HotKeys } from 'react-hotkeys';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
import { openModal } from 'mastodon/actions/modal';
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { Hotkeys } from 'mastodon/components/hotkeys';
import AttachmentList from 'mastodon/components/attachment_list';
import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button';
@ -169,7 +167,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
};
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
<AvatarComposite accounts={accounts} size={48} />
@ -219,7 +217,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
</div>
</div>
</div>
</HotKeys>
</Hotkeys>
);
};

View file

@ -8,7 +8,6 @@ import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
@ -20,6 +19,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -137,7 +137,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user-plus' icon={PersonAddIcon} />
@ -149,7 +149,7 @@ class Notification extends ImmutablePureComponent {
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@ -157,7 +157,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user' icon={PersonIcon} />
@ -169,7 +169,7 @@ class Notification extends ImmutablePureComponent {
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@ -195,7 +195,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='star' icon={StarIcon} className='star-icon' />
@ -217,7 +217,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -225,7 +225,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='retweet' icon={RepeatIcon} />
@ -247,7 +247,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -259,7 +259,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-status focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='home' icon={HomeIcon} />
@ -282,7 +282,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -294,7 +294,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-update focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='pencil' icon={EditIcon} />
@ -317,7 +317,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -331,7 +331,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='tasks' icon={InsertChartIcon} />
@ -358,7 +358,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -371,7 +371,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.relationshipsSevered, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
<RelationshipsSeveranceEvent
type={event.get('type')}
@ -381,7 +381,7 @@ class Notification extends ImmutablePureComponent {
hidden={hidden}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -394,7 +394,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
<ModerationWarning
action={warning.get('action')}
@ -402,7 +402,7 @@ class Notification extends ImmutablePureComponent {
hidden={hidden}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@ -410,7 +410,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminSignUp, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user-plus' icon={PersonAddIcon} />
@ -422,7 +422,7 @@ class Notification extends ImmutablePureComponent {
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@ -438,7 +438,7 @@ class Notification extends ImmutablePureComponent {
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} data-hover-card-account={targetAccount.get('id')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='flag' icon={FlagIcon} />
@ -450,7 +450,7 @@ class Notification extends ImmutablePureComponent {
<Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}

View file

@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { HotKeys } from 'react-hotkeys';
import { navigateToProfile } from 'mastodon/actions/accounts';
import { mentionComposeById } from 'mastodon/actions/compose';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@ -156,5 +155,5 @@ export const NotificationGroup: React.FC<{
return null;
}
return <HotKeys handlers={handlers}>{content}</HotKeys>;
return <Hotkeys handlers={handlers}>{content}</Hotkeys>;
};

View file

@ -3,12 +3,11 @@ import type { JSX } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses';
import { Avatar } from 'mastodon/components/avatar';
import { AvatarGroup } from 'mastodon/components/avatar_group';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
@ -91,7 +90,7 @@ export const NotificationGroupWithStatus: React.FC<{
);
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div
role='button'
className={classNames(
@ -149,6 +148,6 @@ export const NotificationGroupWithStatus: React.FC<{
)}
</div>
</div>
</HotKeys>
</Hotkeys>
);
};

View file

@ -2,14 +2,13 @@ import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import {
navigateToStatus,
toggleStatusSpoilers,
} from 'mastodon/actions/statuses';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
@ -83,7 +82,7 @@ export const NotificationWithStatus: React.FC<{
if (!statusId || isFiltered) return null;
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div
role='button'
className={classNames(
@ -111,6 +110,6 @@ export const NotificationWithStatus: React.FC<{
unfocusable
/>
</div>
</HotKeys>
</Hotkeys>
);
};

View file

@ -10,10 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { HotKeys } from 'react-hotkeys';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { TimelineHint } from 'mastodon/components/timeline_hint';
@ -616,7 +615,7 @@ class Status extends ImmutablePureComponent {
<div className={classNames('scrollable', { fullscreen })} ref={this.setContainerRef}>
{ancestors}
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false)} ref={this.setStatusRef}>
<DetailedStatus
key={`details-${status.get('id')}`}
@ -654,7 +653,7 @@ class Status extends ImmutablePureComponent {
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
</Hotkeys>
{descendants}
{remoteHint}

View file

@ -9,13 +9,13 @@ import { Redirect, Route, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { fetchNotifications } from 'mastodon/actions/notification_groups';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import { AlertsController } from 'mastodon/components/alerts_controller';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { HoverCardController } from 'mastodon/components/hover_card_controller';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
@ -98,40 +98,6 @@ const mapStateToProps = state => ({
username: state.getIn(['accounts', me, 'username']),
});
const keyMap = {
help: '?',
new: 'n',
search: ['s', '/'],
forceNew: 'option+n',
toggleComposeSpoilers: 'option+x',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
reply: 'r',
favourite: 'f',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
openProfile: 'p',
moveDown: ['down', 'j'],
moveUp: ['up', 'k'],
back: 'backspace',
goToHome: 'g h',
goToNotifications: 'g n',
goToLocal: 'g l',
goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s',
goToFavourites: 'g f',
goToPinned: 'g p',
goToProfile: 'g u',
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'e',
onTranslate: 't',
};
class SwitchingColumnsArea extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
@ -400,6 +366,10 @@ class UI extends PureComponent {
}
};
handleDonate = () => {
location.href = 'https://joinmastodon.org/sponsors#donate'
}
componentDidMount () {
const { signedIn } = this.props.identity;
@ -426,10 +396,6 @@ class UI extends PureComponent {
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
}
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
};
}
componentWillUnmount () {
@ -509,10 +475,6 @@ class UI extends PureComponent {
}
};
setHotkeysRef = c => {
this.hotkeys = c;
};
handleHotkeyToggleHelp = () => {
if (this.props.location.pathname === '/keyboard-shortcuts') {
this.props.history.goBack();
@ -593,10 +555,11 @@ class UI extends PureComponent {
goToBlocked: this.handleHotkeyGoToBlocked,
goToMuted: this.handleHotkeyGoToMuted,
goToRequests: this.handleHotkeyGoToRequests,
cheat: this.handleDonate,
};
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<Hotkeys global handlers={handlers}>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
{children}
@ -611,7 +574,7 @@ class UI extends PureComponent {
<ModalContainer />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
</div>
</HotKeys>
</Hotkeys>
);
}