Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Create global keyboard shortcut handler (#10145)

authored by

DS Boyce and committed by
GitHub
cca3326b e0ea778e

+257 -182
+1
package.json
··· 199 199 "react": "19.1.0", 200 200 "react-compiler-runtime": "^19.1.0-rc.1", 201 201 "react-dom": "19.1.0", 202 + "react-hotkeys-hook": "5.2.4", 202 203 "react-image-crop": "^11.0.7", 203 204 "react-is": "19", 204 205 "react-keyed-flatten-children": "^5.0.0",
+13 -14
src/App.native.tsx
··· 11 11 import * as ScreenOrientation from 'expo-screen-orientation' 12 12 import * as SplashScreen from 'expo-splash-screen' 13 13 import * as SystemUI from 'expo-system-ui' 14 - import {msg} from '@lingui/core/macro' 15 - import {useLingui} from '@lingui/react' 14 + import {useLingui} from '@lingui/react/macro' 16 15 import * as Sentry from '@sentry/react-native' 17 16 18 17 import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' ··· 89 88 import {BottomSheetProvider} from '../modules/bottom-sheet' 90 89 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 91 90 92 - SplashScreen.preventAutoHideAsync() 91 + void SplashScreen.preventAutoHideAsync() 93 92 if (IS_IOS) { 94 - SystemUI.setBackgroundColorAsync('black') 93 + void SystemUI.setBackgroundColorAsync('black') 95 94 } 96 95 if (IS_ANDROID) { 97 96 // iOS is handled by the config plugin -sfn ··· 105 104 /** 106 105 * Begin geolocation ASAP 107 106 */ 108 - Geo.resolve() 109 - prefetchAgeAssuranceConfig() 110 - prefetchLiveEvents() 111 - prefetchAppConfig() 107 + void Geo.resolve() 108 + void prefetchAgeAssuranceConfig() 109 + void prefetchLiveEvents() 110 + void prefetchAppConfig() 112 111 113 112 function InnerApp() { 114 113 const [isReady, setIsReady] = useState(false) 115 114 const {currentAccount} = useSession() 116 115 const {resumeSession} = useSessionApi() 117 116 const theme = useColorModeTheme() 118 - const {_} = useLingui() 117 + const {t: l} = useLingui() 119 118 const hasCheckedReferrer = useStarterPackEntry() 120 119 121 120 // init ··· 134 133 } 135 134 } 136 135 const account = readLastActiveAccount() 137 - onLaunch(account) 136 + void onLaunch(account) 138 137 }, [resumeSession]) 139 138 140 139 useEffect(() => { 141 140 return listenSessionDropped(() => { 142 - Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), { 141 + Toast.show(l`Sorry! Your session expired. Please sign in again.`, { 143 142 type: 'info', 144 143 }) 145 144 }) 146 - }, [_]) 145 + }, [l]) 147 146 148 147 return ( 149 148 <Alf theme={theme}> ··· 220 219 const [isReady, setReady] = useState(false) 221 220 222 221 useEffect(() => { 223 - Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => 224 - setReady(true), 222 + void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then( 223 + () => setReady(true), 225 224 ) 226 225 }, []) 227 226
+17 -15
src/App.web.tsx
··· 5 5 import {Fragment, useEffect, useState} from 'react' 6 6 import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' 7 7 import {SafeAreaProvider} from 'react-native-safe-area-context' 8 - import {msg} from '@lingui/core/macro' 9 - import {useLingui} from '@lingui/react' 8 + import {useLingui} from '@lingui/react/macro' 10 9 import * as Sentry from '@sentry/react-native' 11 10 11 + import {Provider as HotkeysProvider} from '#/lib/hotkeys' 12 12 import {QueryProvider} from '#/lib/react-query' 13 13 import {ThemeProvider} from '#/lib/ThemeContext' 14 14 import {Provider as TranslateOnDeviceProvider} from '#/lib/translation' ··· 82 82 /** 83 83 * Begin geolocation ASAP 84 84 */ 85 - Geo.resolve() 86 - prefetchAgeAssuranceConfig() 87 - prefetchLiveEvents() 88 - prefetchAppConfig() 85 + void Geo.resolve() 86 + void prefetchAgeAssuranceConfig() 87 + void prefetchLiveEvents() 88 + void prefetchAppConfig() 89 89 90 90 function InnerApp() { 91 91 const [isReady, setIsReady] = useState(false) 92 92 const {currentAccount} = useSession() 93 93 const {resumeSession} = useSessionApi() 94 94 const theme = useColorModeTheme() 95 - const {_} = useLingui() 95 + const {t: l} = useLingui() 96 96 const hasCheckedReferrer = useStarterPackEntry() 97 97 98 98 // init ··· 105 105 await features.init 106 106 } 107 107 } catch (e) { 108 - logger.error(`session: resumeSession failed`, {message: e}) 108 + logger.error('session: resumeSession failed', {message: e}) 109 109 } finally { 110 110 setIsReady(true) 111 111 } 112 112 } 113 113 const account = readLastActiveAccount() 114 - onLaunch(account) 114 + void onLaunch(account) 115 115 }, [resumeSession]) 116 116 117 117 useEffect(() => { 118 118 return listenSessionDropped(() => { 119 - Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), { 119 + Toast.show(l`Sorry! Your session expired. Please sign in again.`, { 120 120 type: 'info', 121 121 }) 122 122 }) 123 - }, [_]) 123 + }, [l]) 124 124 125 125 return ( 126 126 <Alf theme={theme}> ··· 156 156 <HideBottomBarBorderProvider> 157 157 <IntentDialogProvider> 158 158 <TranslateOnDeviceProvider> 159 - <Shell /> 160 - <ToastOutlet /> 159 + <HotkeysProvider> 160 + <Shell /> 161 + <ToastOutlet /> 162 + </HotkeysProvider> 161 163 </TranslateOnDeviceProvider> 162 164 </IntentDialogProvider> 163 165 </HideBottomBarBorderProvider> ··· 195 197 const [isReady, setReady] = useState(false) 196 198 197 199 useEffect(() => { 198 - Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => 199 - setReady(true), 200 + void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then( 201 + () => setReady(true), 200 202 ) 201 203 }, []) 202 204
+79 -62
src/components/forms/SearchInput.tsx
··· 1 - import {forwardRef} from 'react' 1 + import {useEffect, useRef} from 'react' 2 2 import {type TextInput, View} from 'react-native' 3 3 import {useLingui} from '@lingui/react/macro' 4 4 5 5 import {HITSLOP_10} from '#/lib/constants' 6 + import {listenFocusSearch} from '#/state/events' 6 7 import {atoms as a, useTheme} from '#/alf' 7 8 import {Button, ButtonIcon} from '#/components/Button' 8 9 import * as TextField from '#/components/forms/TextField' ··· 10 11 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 11 12 import {IS_NATIVE} from '#/env' 12 13 13 - type SearchInputProps = Omit<TextField.InputProps, 'label'> & { 14 + type Props = Omit<TextField.InputProps, 'label'> & { 14 15 label?: TextField.InputProps['label'] 15 16 /** 16 17 * Called when the user presses the (X) button 17 18 */ 18 19 onClearText?: () => void 20 + hotkey?: boolean 21 + ref?: React.RefObject<TextInput | null> 19 22 } 20 23 21 - export const SearchInput = forwardRef<TextInput, SearchInputProps>( 22 - function SearchInput({value, label, onClearText, ...rest}, ref) { 23 - const t = useTheme() 24 - const {t: l} = useLingui() 25 - const showClear = value && value.length > 0 24 + export function SearchInput({ 25 + value, 26 + label, 27 + onClearText, 28 + hotkey, 29 + ref, 30 + ...rest 31 + }: Props) { 32 + const t = useTheme() 33 + const {t: l} = useLingui() 34 + const showClear = value && value.length > 0 35 + const internalRef = useRef<TextInput>(null) 36 + const inputRef = ref ?? internalRef 26 37 27 - return ( 28 - <View style={[a.w_full, a.relative]}> 29 - <TextField.Root> 30 - <TextField.Icon icon={MagnifyingGlassIcon} /> 31 - <TextField.Input 32 - inputRef={ref} 33 - label={label || l`Search`} 34 - value={value} 35 - placeholder={l`Search`} 36 - returnKeyType="search" 37 - keyboardAppearance={t.scheme} 38 - selectTextOnFocus={IS_NATIVE} 39 - autoFocus={false} 40 - accessibilityRole="search" 41 - autoCorrect={false} 42 - autoComplete="off" 43 - autoCapitalize="none" 44 - style={[ 45 - showClear 46 - ? { 47 - paddingRight: 24, 48 - } 49 - : {}, 50 - ]} 51 - {...rest} 52 - /> 53 - </TextField.Root> 38 + useEffect(() => { 39 + if (!hotkey) return 40 + return listenFocusSearch(() => { 41 + inputRef.current?.focus() 42 + }) 43 + }, [hotkey, inputRef]) 44 + 45 + return ( 46 + <View style={[a.w_full, a.relative]}> 47 + <TextField.Root> 48 + <TextField.Icon icon={MagnifyingGlassIcon} /> 49 + <TextField.Input 50 + inputRef={inputRef} 51 + label={label || l`Search`} 52 + value={value} 53 + placeholder={l`Search`} 54 + returnKeyType="search" 55 + keyboardAppearance={t.scheme} 56 + selectTextOnFocus={IS_NATIVE} 57 + autoFocus={false} 58 + accessibilityRole="search" 59 + autoCorrect={false} 60 + autoComplete="off" 61 + autoCapitalize="none" 62 + style={[ 63 + showClear 64 + ? { 65 + paddingRight: 24, 66 + } 67 + : {}, 68 + ]} 69 + {...rest} 70 + /> 71 + </TextField.Root> 54 72 55 - {showClear && ( 56 - <View 57 - style={[ 58 - a.absolute, 59 - a.z_20, 60 - a.my_auto, 61 - a.inset_0, 62 - a.justify_center, 63 - a.pr_sm, 64 - {left: 'auto'}, 65 - ]}> 66 - <Button 67 - testID="searchTextInputClearBtn" 68 - onPress={onClearText} 69 - label={l`Clear search query`} 70 - hitSlop={HITSLOP_10} 71 - size="tiny" 72 - shape="round" 73 - variant="ghost" 74 - color="secondary"> 75 - <ButtonIcon icon={X} size="xs" /> 76 - </Button> 77 - </View> 78 - )} 79 - </View> 80 - ) 81 - }, 82 - ) 73 + {showClear && ( 74 + <View 75 + style={[ 76 + a.absolute, 77 + a.z_20, 78 + a.my_auto, 79 + a.inset_0, 80 + a.justify_center, 81 + a.pr_sm, 82 + {left: 'auto'}, 83 + ]}> 84 + <Button 85 + testID="searchTextInputClearBtn" 86 + onPress={onClearText} 87 + label={l`Clear search query`} 88 + hitSlop={HITSLOP_10} 89 + size="tiny" 90 + shape="round" 91 + variant="ghost" 92 + color="secondary"> 93 + <ButtonIcon icon={X} size="xs" /> 94 + </Button> 95 + </View> 96 + )} 97 + </View> 98 + ) 99 + }
+76
src/lib/hotkeys/index.tsx
··· 1 + import React from 'react' 2 + import {useLingui} from '@lingui/react/macro' 3 + import { 4 + HotkeysProvider, 5 + useHotkeys, 6 + useHotkeysContext, 7 + } from 'react-hotkeys-hook' 8 + 9 + import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 10 + import {emitFocusSearch} from '#/state/events' 11 + import {useSession} from '#/state/session' 12 + 13 + enum Hotkeys { 14 + OPEN_COMPOSER = 'n', 15 + FOCUS_SEARCH = 'slash', 16 + } 17 + 18 + export function Provider({children}: React.PropsWithChildren<unknown>) { 19 + return ( 20 + <HotkeysProvider initiallyActiveScopes={['global']}> 21 + <KeyboardShortcuts>{children}</KeyboardShortcuts> 22 + </HotkeysProvider> 23 + ) 24 + } 25 + 26 + export {useHotkeysContext} 27 + 28 + function KeyboardShortcuts({children}: React.PropsWithChildren<unknown>) { 29 + useKeyboardShortcuts() 30 + return children 31 + } 32 + 33 + function useKeyboardShortcuts() { 34 + const {openComposer} = useOpenComposer() 35 + const {hasSession} = useSession() 36 + const {t: l} = useLingui() 37 + 38 + const shouldIgnore = (requiresSession: boolean = false) => { 39 + if (requiresSession && !hasSession) { 40 + return true 41 + } 42 + 43 + return false 44 + } 45 + 46 + const handleKey = ( 47 + callback: () => void, 48 + options?: {requiresSession?: boolean}, 49 + ) => { 50 + if (shouldIgnore(options?.requiresSession)) { 51 + return 52 + } 53 + callback() 54 + } 55 + 56 + useHotkeys( 57 + Hotkeys.OPEN_COMPOSER, 58 + () => 59 + handleKey( 60 + () => { 61 + openComposer({logContext: 'Other'}) 62 + }, 63 + { 64 + requiresSession: true, 65 + }, 66 + ), 67 + {scopes: ['global'], description: l`Compose new post`}, 68 + [openComposer], 69 + ) 70 + 71 + useHotkeys(Hotkeys.FOCUS_SEARCH, () => handleKey(emitFocusSearch), { 72 + scopes: ['global'], 73 + preventDefault: true, 74 + description: l`Focus the search field`, 75 + }) 76 + }
+1
src/screens/Search/Shell.tsx
··· 380 380 inputPlaceholder ?? l`Search for posts, users, or feeds` 381 381 } 382 382 hitSlop={{...HITSLOP_20, top: 0}} 383 + hotkey={true} 383 384 /> 384 385 </View> 385 386 {showAutocomplete && (
+18 -8
src/state/dialogs/index.tsx
··· 7 7 useState, 8 8 } from 'react' 9 9 10 + import {useHotkeysContext} from '#/lib/hotkeys' 10 11 import {type DialogControlRefProps} from '#/components/Dialog' 11 12 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' 12 13 import {IS_WEB} from '#/env' ··· 62 63 63 64 export function Provider({children}: React.PropsWithChildren<{}>) { 64 65 const [fullyExpandedCount, setFullyExpandedCount] = useState(0) 66 + const {disableScope, enableScope} = useHotkeysContext() 65 67 66 68 const activeDialogs = useRef< 67 69 Map<string, React.MutableRefObject<DialogControlRefProps>> ··· 77 79 78 80 return openDialogs.current.size > 0 79 81 } else { 80 - BottomSheetNativeComponent.dismissAll() 82 + void BottomSheetNativeComponent.dismissAll() 81 83 return false 82 84 } 83 85 }, []) 84 86 85 - const setDialogIsOpen = useCallback((id: string, isOpen: boolean) => { 86 - if (isOpen) { 87 - openDialogs.current.add(id) 88 - } else { 89 - openDialogs.current.delete(id) 90 - } 91 - }, []) 87 + const setDialogIsOpen = useCallback( 88 + (id: string, isOpen: boolean) => { 89 + if (isOpen) { 90 + openDialogs.current.add(id) 91 + } else { 92 + openDialogs.current.delete(id) 93 + } 94 + if (openDialogs.current.size > 0) { 95 + disableScope('global') 96 + } else { 97 + enableScope('global') 98 + } 99 + }, 100 + [disableScope, enableScope], 101 + ) 92 102 93 103 const context = useMemo<IDialogContext>( 94 104 () => ({
+8
src/state/events.ts
··· 45 45 emitter.on('post-created', fn) 46 46 return () => emitter.off('post-created', fn) 47 47 } 48 + 49 + export function emitFocusSearch() { 50 + emitter.emit('focus-search') 51 + } 52 + export function listenFocusSearch(fn: () => void): UnlistenFn { 53 + emitter.on('focus-search', fn) 54 + return () => emitter.off('focus-search', fn) 55 + }
+11 -1
src/state/lightbox.tsx
··· 1 - import {createContext, useContext, useMemo, useState} from 'react' 1 + import {createContext, useContext, useEffect, useMemo, useState} from 'react' 2 2 import {nanoid} from 'nanoid/non-secure' 3 3 4 4 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 5 + import {useHotkeysContext} from '#/lib/hotkeys' 5 6 import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types' 6 7 7 8 export type Lightbox = { ··· 28 29 29 30 export function Provider({children}: React.PropsWithChildren<{}>) { 30 31 const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null) 32 + const {disableScope, enableScope} = useHotkeysContext() 33 + 34 + useEffect(() => { 35 + if (activeLightbox) { 36 + disableScope('global') 37 + } else { 38 + enableScope('global') 39 + } 40 + }, [activeLightbox, disableScope, enableScope]) 31 41 32 42 const openLightbox = useNonReactiveCallback( 33 43 (lightbox: Omit<Lightbox, 'id'>) => {
+11 -1
src/state/modals/index.tsx
··· 1 - import {createContext, useContext, useMemo, useState} from 'react' 1 + import {createContext, useContext, useEffect, useMemo, useState} from 'react' 2 2 3 3 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 4 + import {useHotkeysContext} from '#/lib/hotkeys' 4 5 5 6 export interface UserAddRemoveListsModal { 6 7 name: 'user-add-remove-lists' ··· 47 48 48 49 export function Provider({children}: React.PropsWithChildren<{}>) { 49 50 const [activeModals, setActiveModals] = useState<Modal[]>([]) 51 + const {disableScope, enableScope} = useHotkeysContext() 52 + 53 + useEffect(() => { 54 + if (activeModals.length > 0) { 55 + disableScope('global') 56 + } else { 57 + enableScope('global') 58 + } 59 + }, [activeModals.length, disableScope, enableScope]) 50 60 51 61 const openModal = useNonReactiveCallback((modal: Modal) => { 52 62 setActiveModals(modals => [...modals, modal])
-77
src/state/shell/composer/useComposerKeyboardShortcut.tsx
··· 1 - import {useEffect} from 'react' 2 - 3 - import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 4 - import {useDialogStateContext} from '#/state/dialogs' 5 - import {useLightbox} from '#/state/lightbox' 6 - import {useModals} from '#/state/modals' 7 - import {useSession} from '#/state/session' 8 - import {useIsDrawerOpen} from '#/state/shell/drawer-open' 9 - 10 - /** 11 - * Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2} 12 - */ 13 - function shouldIgnore(event: KeyboardEvent) { 14 - const target: any = event.target || event.srcElement 15 - if (!target) return false 16 - const {tagName} = target 17 - if (!tagName) return false 18 - const isInput = 19 - tagName === 'INPUT' && 20 - ![ 21 - 'checkbox', 22 - 'radio', 23 - 'range', 24 - 'button', 25 - 'file', 26 - 'reset', 27 - 'submit', 28 - 'color', 29 - ].includes(target.type) 30 - // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select> 31 - if ( 32 - target.isContentEditable || 33 - ((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') && 34 - !target.readOnly) 35 - ) { 36 - return true 37 - } 38 - return false 39 - } 40 - 41 - export function useComposerKeyboardShortcut() { 42 - const {openComposer} = useOpenComposer() 43 - const {openDialogs} = useDialogStateContext() 44 - const {isModalActive} = useModals() 45 - const {activeLightbox} = useLightbox() 46 - const isDrawerOpen = useIsDrawerOpen() 47 - const {hasSession} = useSession() 48 - 49 - useEffect(() => { 50 - if (!hasSession) { 51 - return 52 - } 53 - 54 - function handler(event: KeyboardEvent) { 55 - if (shouldIgnore(event)) return 56 - if ( 57 - openDialogs?.current.size > 0 || 58 - isModalActive || 59 - activeLightbox || 60 - isDrawerOpen 61 - ) 62 - return 63 - if (event.key === 'n' || event.key === 'N') { 64 - openComposer({logContext: 'Other'}) 65 - } 66 - } 67 - document.addEventListener('keydown', handler) 68 - return () => document.removeEventListener('keydown', handler) 69 - }, [ 70 - openComposer, 71 - isModalActive, 72 - openDialogs, 73 - activeLightbox, 74 - isDrawerOpen, 75 - hasSession, 76 - ]) 77 - }
+15 -1
src/state/shell/drawer-open.tsx
··· 1 1 import {createContext, useContext, useState} from 'react' 2 2 3 + import {useHotkeysContext} from '#/lib/hotkeys' 4 + 3 5 type StateContext = boolean 4 6 type SetContext = (v: boolean) => void 5 7 ··· 10 12 11 13 export function Provider({children}: React.PropsWithChildren<{}>) { 12 14 const [state, setState] = useState(false) 15 + const {disableScope, enableScope} = useHotkeysContext() 16 + 17 + const setDrawerOpen = (open: boolean) => { 18 + if (open) { 19 + disableScope('global') 20 + } else { 21 + enableScope('global') 22 + } 23 + setState(open) 24 + } 13 25 14 26 return ( 15 27 <stateContext.Provider value={state}> 16 - <setContext.Provider value={setState}>{children}</setContext.Provider> 28 + <setContext.Provider value={setDrawerOpen}> 29 + {children} 30 + </setContext.Provider> 17 31 </stateContext.Provider> 18 32 ) 19 33 }
+1 -1
src/view/shell/desktop/LeftNav.tsx
··· 580 580 style={[a.rounded_full]}> 581 581 <ButtonIcon icon={EditBig} position="left" /> 582 582 <ButtonText> 583 - <Trans context="action">New Post</Trans> 583 + <Trans context="action">New post</Trans> 584 584 </ButtonText> 585 585 </Button> 586 586 </View>
+1
src/view/shell/desktop/Search.tsx
··· 105 105 onChangeText={onChangeText} 106 106 onClearText={onPressCancelSearch} 107 107 onSubmitEditing={onSubmit} 108 + hotkey={true} 108 109 /> 109 110 {tQuery !== '' && isActive && moderationOpts && ( 110 111 <View
-2
src/view/shell/index.web.tsx
··· 9 9 import {type NavigationProp} from '#/lib/routes/types' 10 10 import {useSession} from '#/state/session' 11 11 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 12 - import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' 13 12 import {useCloseAllActiveElements} from '#/state/util' 14 13 import {Lightbox} from '#/view/com/lightbox/Lightbox' 15 14 import {ModalsContainer} from '#/view/com/modals/Modal' ··· 45 44 const {state: policyUpdateState} = usePolicyUpdateContext() 46 45 const welcomeModalControl = useWelcomeModal() 47 46 48 - useComposerKeyboardShortcut() 49 47 useIntentHandler() 50 48 51 49 useEffect(() => {
+5
yarn.lock
··· 13846 13846 resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d" 13847 13847 integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== 13848 13848 13849 + react-hotkeys-hook@5.2.4: 13850 + version "5.2.4" 13851 + resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-5.2.4.tgz#45ad54d78823b2a929963d482aff98efad0530f2" 13852 + integrity sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A== 13853 + 13849 13854 react-image-crop@^11.0.7: 13850 13855 version "11.0.7" 13851 13856 resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e"