my fork of the bluesky client

Ensure react-native-keyboard-controller enabled state doesn't get overwritten (#6727)

* revert to prev state instead of false

* add dep array

* use ref counting approach

* patch keyboard controller to allow changing the enabled prop

* remove state from patch

* change patched prop name

* remove Math.max check, log if < 0

* use noop provider

* rm patch, use `useRef`

* Style nits

* Rm on web

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by samuel.fm

Dan Abramov and committed by
GitHub
b0c36383 6c810900

+169 -84
+3 -3
src/App.native.tsx
··· 4 4 5 5 import React, {useEffect, useState} from 'react' 6 6 import {GestureHandlerRootView} from 'react-native-gesture-handler' 7 - import {KeyboardProvider} from 'react-native-keyboard-controller' 8 7 import {RootSiblingParent} from 'react-native-root-siblings' 9 8 import { 10 9 initialWindowMetrics, ··· 70 69 import {BottomSheetProvider} from '../modules/bottom-sheet' 71 70 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 72 71 import {AppProfiler} from './AppProfiler' 72 + import {KeyboardControllerProvider} from './lib/hooks/useEnableKeyboardController' 73 73 74 74 SplashScreen.preventAutoHideAsync() 75 75 ··· 188 188 <AppProfiler> 189 189 <GeolocationProvider> 190 190 <A11yProvider> 191 - <KeyboardProvider enabled={false} statusBarTranslucent={true}> 191 + <KeyboardControllerProvider> 192 192 <SessionProvider> 193 193 <PrefsStateProvider> 194 194 <I18nProvider> ··· 217 217 </I18nProvider> 218 218 </PrefsStateProvider> 219 219 </SessionProvider> 220 - </KeyboardProvider> 220 + </KeyboardControllerProvider> 221 221 </A11yProvider> 222 222 </GeolocationProvider> 223 223 </AppProfiler>
+46 -49
src/App.web.tsx
··· 3 3 import './style.css' 4 4 5 5 import React, {useEffect, useState} from 'react' 6 - import {KeyboardProvider} from 'react-native-keyboard-controller' 7 6 import {RootSiblingParent} from 'react-native-root-siblings' 8 7 import {SafeAreaProvider} from 'react-native-safe-area-context' 9 8 import {msg} from '@lingui/macro' ··· 102 101 if (!isReady || !hasCheckedReferrer) return null 103 102 104 103 return ( 105 - <KeyboardProvider enabled={false}> 106 - <Alf theme={theme}> 107 - <ThemeProvider theme={theme}> 108 - <RootSiblingParent> 109 - <VideoVolumeProvider> 110 - <ActiveVideoProvider> 111 - <React.Fragment 112 - // Resets the entire tree below when it changes: 113 - key={currentAccount?.did}> 114 - <QueryProvider currentDid={currentAccount?.did}> 115 - <ComposerProvider> 116 - <StatsigProvider> 117 - <MessagesProvider> 118 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 119 - <LabelDefsProvider> 120 - <ModerationOptsProvider> 121 - <LoggedOutViewProvider> 122 - <SelectedFeedProvider> 123 - <HiddenRepliesProvider> 124 - <UnreadNotifsProvider> 125 - <BackgroundNotificationPreferencesProvider> 126 - <MutedThreadsProvider> 127 - <SafeAreaProvider> 128 - <ProgressGuideProvider> 129 - <Shell /> 130 - <NuxDialogs /> 131 - </ProgressGuideProvider> 132 - </SafeAreaProvider> 133 - </MutedThreadsProvider> 134 - </BackgroundNotificationPreferencesProvider> 135 - </UnreadNotifsProvider> 136 - </HiddenRepliesProvider> 137 - </SelectedFeedProvider> 138 - </LoggedOutViewProvider> 139 - </ModerationOptsProvider> 140 - </LabelDefsProvider> 141 - </MessagesProvider> 142 - </StatsigProvider> 143 - </ComposerProvider> 144 - </QueryProvider> 145 - <ToastContainer /> 146 - </React.Fragment> 147 - </ActiveVideoProvider> 148 - </VideoVolumeProvider> 149 - </RootSiblingParent> 150 - </ThemeProvider> 151 - </Alf> 152 - </KeyboardProvider> 104 + <Alf theme={theme}> 105 + <ThemeProvider theme={theme}> 106 + <RootSiblingParent> 107 + <VideoVolumeProvider> 108 + <ActiveVideoProvider> 109 + <React.Fragment 110 + // Resets the entire tree below when it changes: 111 + key={currentAccount?.did}> 112 + <QueryProvider currentDid={currentAccount?.did}> 113 + <ComposerProvider> 114 + <StatsigProvider> 115 + <MessagesProvider> 116 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 117 + <LabelDefsProvider> 118 + <ModerationOptsProvider> 119 + <LoggedOutViewProvider> 120 + <SelectedFeedProvider> 121 + <HiddenRepliesProvider> 122 + <UnreadNotifsProvider> 123 + <BackgroundNotificationPreferencesProvider> 124 + <MutedThreadsProvider> 125 + <SafeAreaProvider> 126 + <ProgressGuideProvider> 127 + <Shell /> 128 + <NuxDialogs /> 129 + </ProgressGuideProvider> 130 + </SafeAreaProvider> 131 + </MutedThreadsProvider> 132 + </BackgroundNotificationPreferencesProvider> 133 + </UnreadNotifsProvider> 134 + </HiddenRepliesProvider> 135 + </SelectedFeedProvider> 136 + </LoggedOutViewProvider> 137 + </ModerationOptsProvider> 138 + </LabelDefsProvider> 139 + </MessagesProvider> 140 + </StatsigProvider> 141 + </ComposerProvider> 142 + </QueryProvider> 143 + <ToastContainer /> 144 + </React.Fragment> 145 + </ActiveVideoProvider> 146 + </VideoVolumeProvider> 147 + </RootSiblingParent> 148 + </ThemeProvider> 149 + </Alf> 153 150 ) 154 151 } 155 152
+3 -13
src/components/Dialog/index.tsx
··· 11 11 } from 'react-native' 12 12 import { 13 13 KeyboardAwareScrollView, 14 - useKeyboardController, 15 14 useKeyboardHandler, 16 15 } from 'react-native-keyboard-controller' 17 16 import {runOnJS} from 'react-native-reanimated' ··· 20 19 import {msg} from '@lingui/macro' 21 20 import {useLingui} from '@lingui/react' 22 21 22 + import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 23 23 import {ScrollProvider} from '#/lib/ScrollContext' 24 24 import {logger} from '#/logger' 25 25 import {isAndroid, isIOS} from '#/platform/detection' ··· 199 199 ) { 200 200 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 201 201 const insets = useSafeAreaInsets() 202 - const {setEnabled} = useKeyboardController() 203 202 204 - const [keyboardHeight, setKeyboardHeight] = React.useState(0) 205 - 206 - React.useEffect(() => { 207 - if (!isIOS) { 208 - return 209 - } 203 + useEnableKeyboardController(isIOS) 210 204 211 - setEnabled(true) 212 - return () => { 213 - setEnabled(false) 214 - } 215 - }) 205 + const [keyboardHeight, setKeyboardHeight] = React.useState(0) 216 206 217 207 useKeyboardHandler( 218 208 {
+110
src/lib/hooks/useEnableKeyboardController.tsx
··· 1 + import { 2 + createContext, 3 + useCallback, 4 + useContext, 5 + useEffect, 6 + useMemo, 7 + useRef, 8 + } from 'react' 9 + import { 10 + KeyboardProvider, 11 + useKeyboardController, 12 + } from 'react-native-keyboard-controller' 13 + import {useFocusEffect} from '@react-navigation/native' 14 + 15 + import {IS_DEV} from '#/env' 16 + 17 + const KeyboardControllerRefCountContext = createContext<{ 18 + incrementRefCount: () => void 19 + decrementRefCount: () => void 20 + }>({ 21 + incrementRefCount: () => {}, 22 + decrementRefCount: () => {}, 23 + }) 24 + 25 + export function KeyboardControllerProvider({ 26 + children, 27 + }: { 28 + children: React.ReactNode 29 + }) { 30 + return ( 31 + <KeyboardProvider 32 + enabled={false} 33 + // I don't think this is necessary, but Chesterton's fence and all that -sfn 34 + statusBarTranslucent={true}> 35 + <KeyboardControllerProviderInner> 36 + {children} 37 + </KeyboardControllerProviderInner> 38 + </KeyboardProvider> 39 + ) 40 + } 41 + 42 + function KeyboardControllerProviderInner({ 43 + children, 44 + }: { 45 + children: React.ReactNode 46 + }) { 47 + const {setEnabled} = useKeyboardController() 48 + const refCount = useRef(0) 49 + 50 + const value = useMemo( 51 + () => ({ 52 + incrementRefCount: () => { 53 + refCount.current++ 54 + setEnabled(refCount.current > 0) 55 + }, 56 + decrementRefCount: () => { 57 + refCount.current-- 58 + setEnabled(refCount.current > 0) 59 + 60 + if (IS_DEV && refCount.current < 0) { 61 + console.error('KeyboardController ref count < 0') 62 + } 63 + }, 64 + }), 65 + [setEnabled], 66 + ) 67 + 68 + return ( 69 + <KeyboardControllerRefCountContext.Provider value={value}> 70 + {children} 71 + </KeyboardControllerRefCountContext.Provider> 72 + ) 73 + } 74 + 75 + export function useEnableKeyboardController(shouldEnable: boolean) { 76 + const {incrementRefCount, decrementRefCount} = useContext( 77 + KeyboardControllerRefCountContext, 78 + ) 79 + 80 + useEffect(() => { 81 + if (!shouldEnable) { 82 + return 83 + } 84 + incrementRefCount() 85 + return () => { 86 + decrementRefCount() 87 + } 88 + }, [shouldEnable, incrementRefCount, decrementRefCount]) 89 + } 90 + 91 + /** 92 + * Like `useEnableKeyboardController`, but using `useFocusEffect` 93 + */ 94 + export function useEnableKeyboardControllerScreen(shouldEnable: boolean) { 95 + const {incrementRefCount, decrementRefCount} = useContext( 96 + KeyboardControllerRefCountContext, 97 + ) 98 + 99 + useFocusEffect( 100 + useCallback(() => { 101 + if (!shouldEnable) { 102 + return 103 + } 104 + incrementRefCount() 105 + return () => { 106 + decrementRefCount() 107 + } 108 + }, [shouldEnable, incrementRefCount, decrementRefCount]), 109 + ) 110 + }
+2 -11
src/screens/Messages/Conversation.tsx
··· 1 1 import React, {useCallback} from 'react' 2 2 import {View} from 'react-native' 3 - import {useKeyboardController} from 'react-native-keyboard-controller' 4 3 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 5 4 import {msg} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' ··· 8 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 8 10 9 import {useEmail} from '#/lib/hooks/useEmail' 10 + import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 11 11 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 12 12 import {isWeb} from '#/platform/detection' 13 13 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 39 39 const convoId = route.params.conversation 40 40 const {setCurrentConvoId} = useCurrentConvoId() 41 41 42 - const {setEnabled} = useKeyboardController() 43 - useFocusEffect( 44 - useCallback(() => { 45 - if (isWeb) return 46 - setEnabled(true) 47 - return () => { 48 - setEnabled(false) 49 - } 50 - }, [setEnabled]), 51 - ) 42 + useEnableKeyboardControllerScreen(true) 52 43 53 44 useFocusEffect( 54 45 useCallback(() => {
+5 -8
src/screens/StarterPack/Wizard/index.tsx
··· 1 1 import React from 'react' 2 2 import {Keyboard, TouchableOpacity, View} from 'react-native' 3 - import { 4 - KeyboardAwareScrollView, 5 - useKeyboardController, 6 - } from 'react-native-keyboard-controller' 3 + import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 7 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 8 5 import {Image} from 'expo-image' 9 6 import { ··· 20 17 import {NativeStackScreenProps} from '@react-navigation/native-stack' 21 18 22 19 import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants' 20 + import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 23 21 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 24 22 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 25 23 import {logEvent} from '#/lib/statsig/statsig' ··· 151 149 const {_} = useLingui() 152 150 const t = useTheme() 153 151 const setMinimalShellMode = useSetMinimalShellMode() 154 - const {setEnabled} = useKeyboardController() 155 152 const [state, dispatch] = useWizardState() 156 153 const {currentAccount} = useSession() 157 154 const {data: currentProfile} = useProfileQuery({ ··· 166 163 }) 167 164 }, [navigation]) 168 165 166 + useEnableKeyboardControllerScreen(true) 167 + 169 168 useFocusEffect( 170 169 React.useCallback(() => { 171 - setEnabled(true) 172 170 setMinimalShellMode(true) 173 171 174 172 return () => { 175 173 setMinimalShellMode(false) 176 - setEnabled(false) 177 174 } 178 - }, [setMinimalShellMode, setEnabled]), 175 + }, [setMinimalShellMode]), 179 176 ) 180 177 181 178 const getDefaultName = () => {