mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 01:41:08 +00:00
refactor: Replace react-hotkeys with custom hook (#35425)
This commit is contained in:
parent
fab95b8dae
commit
4de5cbd6f5
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
|
@ -23,7 +23,6 @@
|
|||
matchManagers: ['npm'],
|
||||
matchPackageNames: [
|
||||
'tesseract.js', // Requires code changes
|
||||
'react-hotkeys', // Requires code changes
|
||||
|
||||
// react-router: Requires manual upgrade
|
||||
'history',
|
||||
|
|
171
app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx
Normal file
171
app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect } from 'storybook/test';
|
||||
|
||||
import type { HandlerMap } from '.';
|
||||
import { Hotkeys } from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Hotkeys',
|
||||
component: Hotkeys,
|
||||
args: {
|
||||
global: undefined,
|
||||
focusable: undefined,
|
||||
handlers: {},
|
||||
},
|
||||
tags: ['test'],
|
||||
} satisfies Meta<typeof Hotkeys>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const hotkeyTest: Story['play'] = async ({ canvas, userEvent }) => {
|
||||
async function confirmHotkey(name: string, shouldFind = true) {
|
||||
// 'status' is the role of the 'output' element
|
||||
const output = await canvas.findByRole('status');
|
||||
if (shouldFind) {
|
||||
await expect(output).toHaveTextContent(name);
|
||||
} else {
|
||||
await expect(output).not.toHaveTextContent(name);
|
||||
}
|
||||
}
|
||||
|
||||
const button = await canvas.findByRole('button');
|
||||
await userEvent.click(button);
|
||||
|
||||
await userEvent.keyboard('n');
|
||||
await confirmHotkey('new');
|
||||
|
||||
await userEvent.keyboard('/');
|
||||
await confirmHotkey('search');
|
||||
|
||||
await userEvent.keyboard('o');
|
||||
await confirmHotkey('open');
|
||||
|
||||
await userEvent.keyboard('{Alt>}N{/Alt}');
|
||||
await confirmHotkey('forceNew');
|
||||
|
||||
await userEvent.keyboard('gh');
|
||||
await confirmHotkey('goToHome');
|
||||
|
||||
await userEvent.keyboard('gn');
|
||||
await confirmHotkey('goToNotifications');
|
||||
|
||||
await userEvent.keyboard('gf');
|
||||
await confirmHotkey('goToFavourites');
|
||||
|
||||
/**
|
||||
* Ensure that hotkeys are not triggered when certain
|
||||
* interactive elements are focused:
|
||||
*/
|
||||
|
||||
await userEvent.keyboard('{enter}');
|
||||
await confirmHotkey('open', false);
|
||||
|
||||
const input = await canvas.findByRole('textbox');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('n');
|
||||
await confirmHotkey('new', false);
|
||||
|
||||
await userEvent.keyboard('{backspace}');
|
||||
await confirmHotkey('None', false);
|
||||
|
||||
/**
|
||||
* Reset playground:
|
||||
*/
|
||||
|
||||
await userEvent.click(button);
|
||||
await userEvent.keyboard('{backspace}');
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
render: function Render() {
|
||||
const [matchedHotkey, setMatchedHotkey] = useState<keyof HandlerMap | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handlers = {
|
||||
back: () => {
|
||||
setMatchedHotkey(null);
|
||||
},
|
||||
new: () => {
|
||||
setMatchedHotkey('new');
|
||||
},
|
||||
forceNew: () => {
|
||||
setMatchedHotkey('forceNew');
|
||||
},
|
||||
search: () => {
|
||||
setMatchedHotkey('search');
|
||||
},
|
||||
open: () => {
|
||||
setMatchedHotkey('open');
|
||||
},
|
||||
goToHome: () => {
|
||||
setMatchedHotkey('goToHome');
|
||||
},
|
||||
goToNotifications: () => {
|
||||
setMatchedHotkey('goToNotifications');
|
||||
},
|
||||
goToFavourites: () => {
|
||||
setMatchedHotkey('goToFavourites');
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
padding: '1em',
|
||||
border: '1px dashed #ccc',
|
||||
fontSize: 14,
|
||||
color: '#222',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 22,
|
||||
marginBottom: '0.3em',
|
||||
}}
|
||||
>
|
||||
Hotkey playground
|
||||
</h1>
|
||||
<p>
|
||||
Last pressed hotkey: <output>{matchedHotkey ?? 'None'}</output>
|
||||
</p>
|
||||
<p>
|
||||
Click within the dashed border and press the "<kbd>n</kbd>
|
||||
" or "<kbd>/</kbd>" key. Press "
|
||||
<kbd>Backspace</kbd>" to clear the displayed hotkey.
|
||||
</p>
|
||||
<p>
|
||||
Try typing a sequence, like "<kbd>g</kbd>" shortly
|
||||
followed by "<kbd>h</kbd>", "<kbd>n</kbd>", or
|
||||
"<kbd>f</kbd>"
|
||||
</p>
|
||||
<p>
|
||||
Note that this playground doesn't support all hotkeys we use in
|
||||
the app.
|
||||
</p>
|
||||
<p>
|
||||
When a <button>Button</button> is focused, "
|
||||
<kbd>Enter</kbd>
|
||||
" should not trigger "open", but "<kbd>o</kbd>
|
||||
" should.
|
||||
</p>
|
||||
<p>
|
||||
When an input element is focused, hotkeys should not interfere with
|
||||
regular typing:
|
||||
</p>
|
||||
<input type='text' />
|
||||
</div>
|
||||
</Hotkeys>
|
||||
);
|
||||
},
|
||||
play: hotkeyTest,
|
||||
};
|
282
app/javascript/mastodon/components/hotkeys/index.tsx
Normal file
282
app/javascript/mastodon/components/hotkeys/index.tsx
Normal file
|
@ -0,0 +1,282 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { normalizeKey, isKeyboardEvent } from './utils';
|
||||
|
||||
/**
|
||||
* In case of multiple hotkeys matching the pressed key(s),
|
||||
* the hotkey with a higher priority is selected. All others
|
||||
* are ignored.
|
||||
*/
|
||||
const hotkeyPriority = {
|
||||
singleKey: 0,
|
||||
combo: 1,
|
||||
sequence: 2,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* This type of function receives a keyboard event and an array of
|
||||
* previously pressed keys (within the last second), and returns
|
||||
* `isMatch` (whether the pressed keys match a hotkey) and `priority`
|
||||
* (a weighting used to resolve conflicts when two hotkeys match the
|
||||
* pressed keys)
|
||||
*/
|
||||
type KeyMatcher = (
|
||||
event: KeyboardEvent,
|
||||
bufferedKeys?: string[],
|
||||
) => {
|
||||
/**
|
||||
* Whether the event.key matches the hotkey
|
||||
*/
|
||||
isMatch: boolean;
|
||||
/**
|
||||
* If there are multiple matching hotkeys, the
|
||||
* first one with the highest priority will be handled
|
||||
*/
|
||||
priority: (typeof hotkeyPriority)[keyof typeof hotkeyPriority];
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches a single key
|
||||
*/
|
||||
function just(keyName: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
isMatch: normalizeKey(event.key) === keyName,
|
||||
priority: hotkeyPriority.singleKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches any single key out of those provided
|
||||
*/
|
||||
function any(...keys: string[]): KeyMatcher {
|
||||
return (event) => ({
|
||||
isMatch: keys.some((keyName) => just(keyName)(event).isMatch),
|
||||
priority: hotkeyPriority.singleKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a single key combined with the option/alt modifier
|
||||
*/
|
||||
function optionPlus(key: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
// Matching against event.code here as alt combos are often
|
||||
// mapped to other characters
|
||||
isMatch: event.altKey && event.code === `Key${key.toUpperCase()}`,
|
||||
priority: hotkeyPriority.combo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when all provided keys are pressed in sequence.
|
||||
*/
|
||||
function sequence(...sequence: string[]): KeyMatcher {
|
||||
return (event, bufferedKeys) => {
|
||||
const lastKeyInSequence = sequence.at(-1);
|
||||
const startOfSequence = sequence.slice(0, -1);
|
||||
const relevantBufferedKeys = bufferedKeys?.slice(-startOfSequence.length);
|
||||
|
||||
const bufferMatchesStartOfSequence =
|
||||
!!relevantBufferedKeys &&
|
||||
startOfSequence.join('') === relevantBufferedKeys.join('');
|
||||
|
||||
return {
|
||||
isMatch:
|
||||
bufferMatchesStartOfSequence &&
|
||||
normalizeKey(event.key) === lastKeyInSequence,
|
||||
priority: hotkeyPriority.sequence,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a map of all global hotkeys we support.
|
||||
* To trigger a hotkey, a handler with a matching name must be
|
||||
* provided to the `useHotkeys` hook or `Hotkeys` component.
|
||||
*/
|
||||
const hotkeyMatcherMap = {
|
||||
help: just('?'),
|
||||
search: any('s', '/'),
|
||||
back: just('backspace'),
|
||||
new: just('n'),
|
||||
forceNew: optionPlus('n'),
|
||||
focusColumn: any('1', '2', '3', '4', '5', '6', '7', '8', '9'),
|
||||
reply: just('r'),
|
||||
favourite: just('f'),
|
||||
boost: just('b'),
|
||||
mention: just('m'),
|
||||
open: any('enter', 'o'),
|
||||
openProfile: just('p'),
|
||||
moveDown: any('down', 'j'),
|
||||
moveUp: any('up', 'k'),
|
||||
toggleHidden: just('x'),
|
||||
toggleSensitive: just('h'),
|
||||
toggleComposeSpoilers: optionPlus('x'),
|
||||
openMedia: just('e'),
|
||||
onTranslate: just('t'),
|
||||
goToHome: sequence('g', 'h'),
|
||||
goToNotifications: sequence('g', 'n'),
|
||||
goToLocal: sequence('g', 'l'),
|
||||
goToFederated: sequence('g', 't'),
|
||||
goToDirect: sequence('g', 'd'),
|
||||
goToStart: sequence('g', 's'),
|
||||
goToFavourites: sequence('g', 'f'),
|
||||
goToPinned: sequence('g', 'p'),
|
||||
goToProfile: sequence('g', 'u'),
|
||||
goToBlocked: sequence('g', 'b'),
|
||||
goToMuted: sequence('g', 'm'),
|
||||
goToRequests: sequence('g', 'r'),
|
||||
cheat: sequence(
|
||||
'up',
|
||||
'up',
|
||||
'down',
|
||||
'down',
|
||||
'left',
|
||||
'right',
|
||||
'left',
|
||||
'right',
|
||||
'b',
|
||||
'a',
|
||||
'enter',
|
||||
),
|
||||
} as const;
|
||||
|
||||
type HotkeyName = keyof typeof hotkeyMatcherMap;
|
||||
|
||||
export type HandlerMap = Partial<
|
||||
Record<HotkeyName, (event: KeyboardEvent) => void>
|
||||
>;
|
||||
|
||||
export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
|
||||
const ref = useRef<T>(null);
|
||||
const bufferedKeys = useRef<string[]>([]);
|
||||
const sequenceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
/**
|
||||
* Store the latest handlers object in a ref so we don't need to
|
||||
* add it as a dependency to the main event listener effect
|
||||
*/
|
||||
const handlersRef = useRef(handlers);
|
||||
useEffect(() => {
|
||||
handlersRef.current = handlers;
|
||||
}, [handlers]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current ?? document;
|
||||
|
||||
function listener(event: Event) {
|
||||
// Ignore key presses from input, textarea, or select elements
|
||||
const tagName = (event.target as HTMLElement).tagName.toLowerCase();
|
||||
const shouldHandleEvent =
|
||||
isKeyboardEvent(event) &&
|
||||
!event.defaultPrevented &&
|
||||
!['input', 'textarea', 'select'].includes(tagName) &&
|
||||
!(
|
||||
['a', 'button'].includes(tagName) &&
|
||||
normalizeKey(event.key) === 'enter'
|
||||
);
|
||||
|
||||
if (shouldHandleEvent) {
|
||||
const matchCandidates: {
|
||||
handler: (event: KeyboardEvent) => void;
|
||||
priority: number;
|
||||
}[] = [];
|
||||
|
||||
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
|
||||
(handlerName) => {
|
||||
const handler = handlersRef.current[handlerName];
|
||||
|
||||
if (handler) {
|
||||
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
|
||||
|
||||
const { isMatch, priority } = hotkeyMatcher(
|
||||
event,
|
||||
bufferedKeys.current,
|
||||
);
|
||||
|
||||
if (isMatch) {
|
||||
matchCandidates.push({ handler, priority });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Sort all matches by priority
|
||||
matchCandidates.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
const bestMatchingHandler = matchCandidates.at(0)?.handler;
|
||||
if (bestMatchingHandler) {
|
||||
bestMatchingHandler(event);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Add last keypress to buffer
|
||||
bufferedKeys.current.push(normalizeKey(event.key));
|
||||
|
||||
// Reset the timeout
|
||||
if (sequenceTimer.current) {
|
||||
clearTimeout(sequenceTimer.current);
|
||||
}
|
||||
sequenceTimer.current = setTimeout(() => {
|
||||
bufferedKeys.current = [];
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
element.addEventListener('keydown', listener);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('keydown', listener);
|
||||
if (sequenceTimer.current) {
|
||||
clearTimeout(sequenceTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hotkeys component allows us to globally register keyboard combinations
|
||||
* under a name and assign actions to them, either globally or scoped to a portion
|
||||
* of the app.
|
||||
*
|
||||
* ### How to use
|
||||
*
|
||||
* To add a new hotkey, add its key combination to the `hotkeyMatcherMap` object
|
||||
* and give it a name.
|
||||
*
|
||||
* Use the `<Hotkeys>` component or the `useHotkeys` hook in the part of of the app
|
||||
* where you want to handle the action, and pass in a handlers object.
|
||||
*
|
||||
* ```tsx
|
||||
* <Hotkeys handlers={{open: openStatus}} />
|
||||
* ```
|
||||
*
|
||||
* Now this function will be called when the 'open' hotkey is pressed by the user.
|
||||
*/
|
||||
export const Hotkeys: React.FC<{
|
||||
/**
|
||||
* An object containing functions to be run when a hotkey is pressed.
|
||||
* The key must be the name of a registered hotkey, e.g. "help" or "search"
|
||||
*/
|
||||
handlers: HandlerMap;
|
||||
/**
|
||||
* When enabled, hotkeys will be matched against the document root
|
||||
* rather than only inside of this component's DOM node.
|
||||
*/
|
||||
global?: boolean;
|
||||
/**
|
||||
* Allow the rendered `div` to be focused
|
||||
*/
|
||||
focusable?: boolean;
|
||||
children: React.ReactNode;
|
||||
}> = ({ handlers, global, focusable = true, children }) => {
|
||||
const ref = useHotkeys<HTMLDivElement>(handlers);
|
||||
|
||||
return (
|
||||
<div ref={global ? undefined : ref} tabIndex={focusable ? -1 : undefined}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
29
app/javascript/mastodon/components/hotkeys/utils.ts
Normal file
29
app/javascript/mastodon/components/hotkeys/utils.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export function isKeyboardEvent(event: Event): event is KeyboardEvent {
|
||||
return 'key' in event;
|
||||
}
|
||||
|
||||
export function normalizeKey(key: string): string {
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
switch (lowerKey) {
|
||||
case ' ':
|
||||
case 'spacebar': // for older browsers
|
||||
return 'space';
|
||||
|
||||
case 'arrowup':
|
||||
return 'up';
|
||||
case 'arrowdown':
|
||||
return 'down';
|
||||
case 'arrowleft':
|
||||
return 'left';
|
||||
case 'arrowright':
|
||||
return 'right';
|
||||
|
||||
case 'esc':
|
||||
case 'escape':
|
||||
return 'escape';
|
||||
|
||||
default:
|
||||
return lowerKey;
|
||||
}
|
||||
}
|
|
@ -8,10 +8,9 @@ import { Link } from 'react-router-dom';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
@ -35,7 +34,6 @@ import StatusActionBar from './status_action_bar';
|
|||
import StatusContent from './status_content';
|
||||
import { StatusThreadLabel } from './status_thread_label';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||
|
@ -325,11 +323,11 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
this.props.onMoveUp?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
|
||||
};
|
||||
|
||||
handleHotkeyMoveDown = e => {
|
||||
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
this.props.onMoveDown?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
|
||||
};
|
||||
|
||||
handleHotkeyToggleHidden = () => {
|
||||
|
@ -437,13 +435,13 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
|
||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
|
||||
{expanded && <span>{status.get('content')}</span>}
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -543,7 +541,7 @@ class Status extends ImmutablePureComponent {
|
|||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
{!skipPrepend && prepend}
|
||||
|
||||
|
@ -604,7 +602,7 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
};
|
||||
|
||||
|
||||
handleMoveDown = (id, featured) => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
|
@ -69,6 +69,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
// TODO: This breaks at the inline-follow-suggestions container
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,6 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
"react-immutable-proptypes": "^2.2.0",
|
||||
"react-immutable-pure-component": "^2.2.2",
|
||||
"react-intl": "^7.1.10",
|
||||
|
|
45
yarn.lock
45
yarn.lock
|
@ -2706,7 +2706,6 @@ __metadata:
|
|||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
react-helmet: "npm:^6.1.0"
|
||||
react-hotkeys: "npm:^1.1.4"
|
||||
react-immutable-proptypes: "npm:^2.2.0"
|
||||
react-immutable-pure-component: "npm:^2.2.2"
|
||||
react-intl: "npm:^7.1.10"
|
||||
|
@ -9172,27 +9171,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isboolean@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "lodash.isboolean@npm:3.0.3"
|
||||
checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isequal@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.isequal@npm:4.5.0"
|
||||
checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isobject@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "lodash.isobject@npm:3.0.2"
|
||||
checksum: 10c0/da4c8480d98b16835b59380b2fbd43c54081acd9466febb788ba77c434384349e0bec162d1c4e89f613f21687b2b6d8384d8a112b80da00c78d28d9915a5cdde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.merge@npm:^4.6.2":
|
||||
version: 4.6.2
|
||||
resolution: "lodash.merge@npm:4.6.2"
|
||||
|
@ -9603,13 +9581,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mousetrap@npm:^1.5.2":
|
||||
version: 1.6.5
|
||||
resolution: "mousetrap@npm:1.6.5"
|
||||
checksum: 10c0/5c361bdbbff3966fd58d70f39b9fe1f8e32c78f3ce65989d83af7aad32a3a95313ce835a8dd8a55cb5de9eeb7c1f0c2b9048631a3073b5606241589e8fc0ba53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mrmime@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "mrmime@npm:2.0.1"
|
||||
|
@ -11115,22 +11086,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hotkeys@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "react-hotkeys@npm:1.1.4"
|
||||
dependencies:
|
||||
lodash.isboolean: "npm:^3.0.3"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
lodash.isobject: "npm:^3.0.2"
|
||||
mousetrap: "npm:^1.5.2"
|
||||
prop-types: "npm:^15.6.0"
|
||||
peerDependencies:
|
||||
react: ">= 0.14.0"
|
||||
react-dom: ">= 0.14.0"
|
||||
checksum: 10c0/6bd566ea97e00058749d43d768ee843e5132f988571536e090b564d5dbaa71093695255514fc5b9fcf9fbd03fcb0603f6e135dcab6dcaaffe43dedbfe742a163
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-immutable-proptypes@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "react-immutable-proptypes@npm:2.2.0"
|
||||
|
|
Loading…
Reference in New Issue
Block a user