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>
This commit is contained in:
Samuel Newman
2024-11-25 20:30:15 +00:00
committed by GitHub
parent 6c81090021
commit b0c36383a9
6 changed files with 169 additions and 84 deletions
+3 -3
View File
@@ -4,7 +4,6 @@ import '#/view/icons'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler' import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {RootSiblingParent} from 'react-native-root-siblings' import {RootSiblingParent} from 'react-native-root-siblings'
import { import {
initialWindowMetrics, initialWindowMetrics,
@@ -70,6 +69,7 @@ import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet' import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {AppProfiler} from './AppProfiler' import {AppProfiler} from './AppProfiler'
import {KeyboardControllerProvider} from './lib/hooks/useEnableKeyboardController'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@@ -188,7 +188,7 @@ function App() {
<AppProfiler> <AppProfiler>
<GeolocationProvider> <GeolocationProvider>
<A11yProvider> <A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}> <KeyboardControllerProvider>
<SessionProvider> <SessionProvider>
<PrefsStateProvider> <PrefsStateProvider>
<I18nProvider> <I18nProvider>
@@ -217,7 +217,7 @@ function App() {
</I18nProvider> </I18nProvider>
</PrefsStateProvider> </PrefsStateProvider>
</SessionProvider> </SessionProvider>
</KeyboardProvider> </KeyboardControllerProvider>
</A11yProvider> </A11yProvider>
</GeolocationProvider> </GeolocationProvider>
</AppProfiler> </AppProfiler>
+46 -49
View File
@@ -3,7 +3,6 @@ import '#/view/icons'
import './style.css' import './style.css'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {RootSiblingParent} from 'react-native-root-siblings' import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context' import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@@ -102,54 +101,52 @@ function InnerApp() {
if (!isReady || !hasCheckedReferrer) return null if (!isReady || !hasCheckedReferrer) return null
return ( return (
<KeyboardProvider enabled={false}> <Alf theme={theme}>
<Alf theme={theme}> <ThemeProvider theme={theme}>
<ThemeProvider theme={theme}> <RootSiblingParent>
<RootSiblingParent> <VideoVolumeProvider>
<VideoVolumeProvider> <ActiveVideoProvider>
<ActiveVideoProvider> <React.Fragment
<React.Fragment // Resets the entire tree below when it changes:
// Resets the entire tree below when it changes: key={currentAccount?.did}>
key={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}> <ComposerProvider>
<ComposerProvider> <StatsigProvider>
<StatsigProvider> <MessagesProvider>
<MessagesProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <LabelDefsProvider>
<LabelDefsProvider> <ModerationOptsProvider>
<ModerationOptsProvider> <LoggedOutViewProvider>
<LoggedOutViewProvider> <SelectedFeedProvider>
<SelectedFeedProvider> <HiddenRepliesProvider>
<HiddenRepliesProvider> <UnreadNotifsProvider>
<UnreadNotifsProvider> <BackgroundNotificationPreferencesProvider>
<BackgroundNotificationPreferencesProvider> <MutedThreadsProvider>
<MutedThreadsProvider> <SafeAreaProvider>
<SafeAreaProvider> <ProgressGuideProvider>
<ProgressGuideProvider> <Shell />
<Shell /> <NuxDialogs />
<NuxDialogs /> </ProgressGuideProvider>
</ProgressGuideProvider> </SafeAreaProvider>
</SafeAreaProvider> </MutedThreadsProvider>
</MutedThreadsProvider> </BackgroundNotificationPreferencesProvider>
</BackgroundNotificationPreferencesProvider> </UnreadNotifsProvider>
</UnreadNotifsProvider> </HiddenRepliesProvider>
</HiddenRepliesProvider> </SelectedFeedProvider>
</SelectedFeedProvider> </LoggedOutViewProvider>
</LoggedOutViewProvider> </ModerationOptsProvider>
</ModerationOptsProvider> </LabelDefsProvider>
</LabelDefsProvider> </MessagesProvider>
</MessagesProvider> </StatsigProvider>
</StatsigProvider> </ComposerProvider>
</ComposerProvider> </QueryProvider>
</QueryProvider> <ToastContainer />
<ToastContainer /> </React.Fragment>
</React.Fragment> </ActiveVideoProvider>
</ActiveVideoProvider> </VideoVolumeProvider>
</VideoVolumeProvider> </RootSiblingParent>
</RootSiblingParent> </ThemeProvider>
</ThemeProvider> </Alf>
</Alf>
</KeyboardProvider>
) )
} }
+3 -13
View File
@@ -11,7 +11,6 @@ import {
} from 'react-native' } from 'react-native'
import { import {
KeyboardAwareScrollView, KeyboardAwareScrollView,
useKeyboardController,
useKeyboardHandler, useKeyboardHandler,
} from 'react-native-keyboard-controller' } from 'react-native-keyboard-controller'
import {runOnJS} from 'react-native-reanimated' import {runOnJS} from 'react-native-reanimated'
@@ -20,6 +19,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection' import {isAndroid, isIOS} from '#/platform/detection'
@@ -199,21 +199,11 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
) { ) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const {setEnabled} = useKeyboardController()
useEnableKeyboardController(isIOS)
const [keyboardHeight, setKeyboardHeight] = React.useState(0) const [keyboardHeight, setKeyboardHeight] = React.useState(0)
React.useEffect(() => {
if (!isIOS) {
return
}
setEnabled(true)
return () => {
setEnabled(false)
}
})
useKeyboardHandler( useKeyboardHandler(
{ {
onEnd: e => { onEnd: e => {
@@ -0,0 +1,110 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react'
import {
KeyboardProvider,
useKeyboardController,
} from 'react-native-keyboard-controller'
import {useFocusEffect} from '@react-navigation/native'
import {IS_DEV} from '#/env'
const KeyboardControllerRefCountContext = createContext<{
incrementRefCount: () => void
decrementRefCount: () => void
}>({
incrementRefCount: () => {},
decrementRefCount: () => {},
})
export function KeyboardControllerProvider({
children,
}: {
children: React.ReactNode
}) {
return (
<KeyboardProvider
enabled={false}
// I don't think this is necessary, but Chesterton's fence and all that -sfn
statusBarTranslucent={true}>
<KeyboardControllerProviderInner>
{children}
</KeyboardControllerProviderInner>
</KeyboardProvider>
)
}
function KeyboardControllerProviderInner({
children,
}: {
children: React.ReactNode
}) {
const {setEnabled} = useKeyboardController()
const refCount = useRef(0)
const value = useMemo(
() => ({
incrementRefCount: () => {
refCount.current++
setEnabled(refCount.current > 0)
},
decrementRefCount: () => {
refCount.current--
setEnabled(refCount.current > 0)
if (IS_DEV && refCount.current < 0) {
console.error('KeyboardController ref count < 0')
}
},
}),
[setEnabled],
)
return (
<KeyboardControllerRefCountContext.Provider value={value}>
{children}
</KeyboardControllerRefCountContext.Provider>
)
}
export function useEnableKeyboardController(shouldEnable: boolean) {
const {incrementRefCount, decrementRefCount} = useContext(
KeyboardControllerRefCountContext,
)
useEffect(() => {
if (!shouldEnable) {
return
}
incrementRefCount()
return () => {
decrementRefCount()
}
}, [shouldEnable, incrementRefCount, decrementRefCount])
}
/**
* Like `useEnableKeyboardController`, but using `useFocusEffect`
*/
export function useEnableKeyboardControllerScreen(shouldEnable: boolean) {
const {incrementRefCount, decrementRefCount} = useContext(
KeyboardControllerRefCountContext,
)
useFocusEffect(
useCallback(() => {
if (!shouldEnable) {
return
}
incrementRefCount()
return () => {
decrementRefCount()
}
}, [shouldEnable, incrementRefCount, decrementRefCount]),
)
}
+2 -11
View File
@@ -1,6 +1,5 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useKeyboardController} from 'react-native-keyboard-controller'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -8,6 +7,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useEmail} from '#/lib/hooks/useEmail' import {useEmail} from '#/lib/hooks/useEmail'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -39,16 +39,7 @@ export function MessagesConversationScreen({route}: Props) {
const convoId = route.params.conversation const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId() const {setCurrentConvoId} = useCurrentConvoId()
const {setEnabled} = useKeyboardController() useEnableKeyboardControllerScreen(true)
useFocusEffect(
useCallback(() => {
if (isWeb) return
setEnabled(true)
return () => {
setEnabled(false)
}
}, [setEnabled]),
)
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
+5 -8
View File
@@ -1,9 +1,6 @@
import React from 'react' import React from 'react'
import {Keyboard, TouchableOpacity, View} from 'react-native' import {Keyboard, TouchableOpacity, View} from 'react-native'
import { import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
KeyboardAwareScrollView,
useKeyboardController,
} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import { import {
@@ -20,6 +17,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
@@ -151,7 +149,6 @@ function WizardInner({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {setEnabled} = useKeyboardController()
const [state, dispatch] = useWizardState() const [state, dispatch] = useWizardState()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({ const {data: currentProfile} = useProfileQuery({
@@ -166,16 +163,16 @@ function WizardInner({
}) })
}, [navigation]) }, [navigation])
useEnableKeyboardControllerScreen(true)
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setEnabled(true)
setMinimalShellMode(true) setMinimalShellMode(true)
return () => { return () => {
setMinimalShellMode(false) setMinimalShellMode(false)
setEnabled(false)
} }
}, [setMinimalShellMode, setEnabled]), }, [setMinimalShellMode]),
) )
const getDefaultName = () => { const getDefaultName = () => {