[🐴] Remove keyboard controller lib (#4038)

* remove library

* implement using just reanimated

* always return false for `keyboardIsOpening` on web

* undo comment

* handle input focus scroll more elegantly

* add back minimal shell toggle on mobile web

* adjust initialnumtorender

* oops

* nit
zio/stable
Hailey 2024-05-16 09:32:10 -07:00 committed by GitHub
parent da2bdf5d6f
commit b15b49a48f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 74 deletions

View File

@ -171,7 +171,6 @@
"react-native-get-random-values": "~1.11.0", "react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1", "react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3", "react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.11.7",
"react-native-pager-view": "6.2.3", "react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0", "react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",

View File

@ -65,7 +65,7 @@ export function MessageInput({
const keyboardHeight = Keyboard.metrics()?.height ?? 0 const keyboardHeight = Keyboard.metrics()?.height ?? 0
const windowHeight = Dimensions.get('window').height const windowHeight = Dimensions.get('window').height
const max = windowHeight - keyboardHeight - topInset - 100 const max = windowHeight - keyboardHeight - topInset - 150
const availableSpace = max - e.nativeEvent.contentSize.height const availableSpace = max - e.nativeEvent.contentSize.height
setMaxHeight(max) setMaxHeight(max)
@ -108,7 +108,6 @@ export function MessageInput({
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
scrollEnabled={isInputScrollable} scrollEnabled={isInputScrollable}
blurOnSubmit={false} blurOnSubmit={false}
onFocus={scrollToEnd}
onContentSizeChange={onInputLayout} onContentSizeChange={onInputLayout}
ref={inputRef} ref={inputRef}
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}

View File

@ -1,12 +1,17 @@
import React, {useCallback, useRef} from 'react' import React, {useCallback, useRef} from 'react'
import {FlatList, View} from 'react-native' import {FlatList, View} from 'react-native'
import {useKeyboardHandler} from 'react-native-keyboard-controller' import Animated, {
import {runOnJS, useSharedValue} from 'react-native-reanimated' useAnimatedKeyboard,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {shortenLinks} from '#/lib/strings/rich-text-manip' import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {isNative} from '#/platform/detection' import {isIOS, isNative} from '#/platform/detection'
import {useConvoActive} from '#/state/messages/convo' import {useConvoActive} from '#/state/messages/convo'
import {ConvoItem} from '#/state/messages/convo/types' import {ConvoItem} from '#/state/messages/convo/types'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@ -15,7 +20,7 @@ import {isWeb} from 'platform/detection'
import {List} from 'view/com/util/List' import {List} from 'view/com/util/List'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
import {atoms as a} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {MessageItem} from '#/components/dms/MessageItem' import {MessageItem} from '#/components/dms/MessageItem'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@ -55,6 +60,7 @@ function onScrollToIndexFailed() {
} }
export function MessagesList() { export function MessagesList() {
const t = useTheme()
const convo = useConvoActive() const convo = useConvoActive()
const {getAgent} = useAgent() const {getAgent} = useAgent()
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
@ -74,8 +80,8 @@ export function MessagesList() {
// We don't want to call `scrollToEnd` again if we are already scolling to the end, because this creates a bit of jank // We don't want to call `scrollToEnd` again if we are already scolling to the end, because this creates a bit of jank
// Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not. // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not.
const isMomentumScrolling = useSharedValue(false) const isMomentumScrolling = useSharedValue(false)
const hasInitiallyScrolled = useSharedValue(false) const hasInitiallyScrolled = useSharedValue(false)
const keyboardIsOpening = useSharedValue(false)
// Every time the content size changes, that means one of two things is happening: // Every time the content size changes, that means one of two things is happening:
// 1. New messages are being added from the log or from a message you have sent // 1. New messages are being added from the log or from a message you have sent
@ -101,22 +107,23 @@ export function MessagesList() {
contentHeight.value = height contentHeight.value = height
// This number _must_ be the height of the MaybeLoader component // This number _must_ be the height of the MaybeLoader component
if (height <= 50 || !isAtBottom.value) { if (height <= 50 || (!isAtBottom.value && !keyboardIsOpening.value)) {
return return
} }
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
animated: hasInitiallyScrolled.value, animated: hasInitiallyScrolled.value && !keyboardIsOpening.value,
offset: height, offset: height,
}) })
isMomentumScrolling.value = true isMomentumScrolling.value = true
}, },
[ [
contentHeight, contentHeight,
hasInitiallyScrolled, hasInitiallyScrolled.value,
isAtBottom.value, isAtBottom.value,
isAtTop.value, isAtTop.value,
isMomentumScrolling, isMomentumScrolling,
keyboardIsOpening.value,
], ],
) )
@ -187,17 +194,46 @@ export function MessagesList() {
}) })
}, [isMomentumScrolling]) }, [isMomentumScrolling])
// This is only used inside the useKeyboardHandler because the worklet won't work with a ref directly. // -- Keyboard animation handling
const scrollToEndNow = React.useCallback(() => { const animatedKeyboard = useAnimatedKeyboard()
flatListRef.current?.scrollToEnd({animated: false}) const {gtMobile} = useBreakpoints()
}, []) const {bottom: bottomInset} = useSafeAreaInsets()
const nativeBottomBarHeight = isIOS ? 42 : 60
const bottomOffset =
isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight
useKeyboardHandler({ // We need to keep track of when the keyboard is animating and when it isn't, since we want our `onContentSizeChanged`
onMove: () => { // callback to animate the scroll _only_ when the keyboard isn't animating. Any time the previous value of kb height
'worklet' // is different, we know that it is animating. When it finally settles, now will be equal to prev.
runOnJS(scrollToEndNow)() useAnimatedReaction(
() => animatedKeyboard.height.value,
(now, prev) => {
// This never applies on web
if (isWeb) {
keyboardIsOpening.value = false
} else {
keyboardIsOpening.value = now !== prev
}
}, },
}) )
// This changes the size of the `ListFooterComponent`. Whenever this changes, the content size will change and our
// `onContentSizeChange` function will handle scrolling to the appropriate offset.
const animatedFooterStyle = useAnimatedStyle(() => ({
marginBottom:
animatedKeyboard.height.value > bottomOffset
? animatedKeyboard.height.value
: bottomOffset,
}))
// At a minimum we want the bottom to be whatever the height of our insets and bottom bar is. If the keyboard's height
// is greater than that however, we use that value.
const animatedInputStyle = useAnimatedStyle(() => ({
bottom:
animatedKeyboard.height.value > bottomOffset
? animatedKeyboard.height.value
: bottomOffset,
}))
return ( return (
<> <>
@ -211,8 +247,9 @@ export function MessagesList() {
containWeb={true} containWeb={true}
contentContainerStyle={[a.px_md]} contentContainerStyle={[a.px_md]}
disableVirtualization={true} disableVirtualization={true}
initialNumToRender={isNative ? 30 : 60} // The extra two items account for the header and the footer components
maxToRenderPerBatch={isWeb ? 30 : 60} initialNumToRender={isNative ? 32 : 62}
maxToRenderPerBatch={isWeb ? 32 : 62}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{ maintainVisibleContentPosition={{
@ -227,9 +264,12 @@ export function MessagesList() {
ListHeaderComponent={ ListHeaderComponent={
<MaybeLoader isLoading={convo.isFetchingHistory} /> <MaybeLoader isLoading={convo.isFetchingHistory} />
} }
ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />}
/> />
</ScrollProvider> </ScrollProvider>
<MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}>
<MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />
</Animated.View>
</> </>
) )
} }

View File

@ -1,8 +1,5 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {TouchableOpacity, View} from 'react-native' import {TouchableOpacity, View} from 'react-native'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@ -18,7 +15,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {BACK_HITSLOP} from 'lib/constants' import {BACK_HITSLOP} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {isIOS, isNative, isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo' import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo/types' import {ConvoStatus} from 'state/messages/convo/types'
import {useSetMinimalShellMode} from 'state/shell' import {useSetMinimalShellMode} from 'state/shell'
@ -39,8 +36,8 @@ type Props = NativeStackScreenProps<
> >
export function MessagesConversationScreen({route}: Props) { export function MessagesConversationScreen({route}: Props) {
const gate = useGate() const gate = useGate()
const setMinimalShellMode = useSetMinimalShellMode()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const setMinimalShellMode = useSetMinimalShellMode()
const convoId = route.params.conversation const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId() const {setCurrentConvoId} = useCurrentConvoId()
@ -57,7 +54,7 @@ export function MessagesConversationScreen({route}: Props) {
setCurrentConvoId(undefined) setCurrentConvoId(undefined)
setMinimalShellMode(false) setMinimalShellMode(false)
} }
}, [convoId, gtMobile, setCurrentConvoId, setMinimalShellMode]), }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]),
) )
if (!gate('dms')) return <ClipClopGate /> if (!gate('dms')) return <ClipClopGate />
@ -76,9 +73,6 @@ function Inner() {
const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false)
const {bottom: bottomInset, top: topInset} = useSafeAreaInsets()
const nativeBottomBarHeight = isIOS ? 42 : 60
// HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have
// to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause
// a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent
@ -111,45 +105,33 @@ function Inner() {
/* /*
* Any other convo states (atm) are "ready" states * Any other convo states (atm) are "ready" states
*/ */
return ( return (
<KeyboardProvider> <CenteredView style={[a.flex_1]} sideBorders>
<KeyboardAvoidingView <Header profile={convoState.recipients?.[0]} />
style={[ <View style={[a.flex_1]}>
a.flex_1, {isConvoActive(convoState) ? (
isNative && {marginBottom: bottomInset + nativeBottomBarHeight}, <MessagesList />
]} ) : (
keyboardVerticalOffset={isIOS ? topInset : 0} <ListMaybePlaceholder isLoading />
behavior="padding" )}
contentContainerStyle={a.flex_1}> {!hasInitiallyRendered && (
<CenteredView style={a.flex_1} sideBorders> <View
<Header profile={convoState.recipients?.[0]} /> style={[
<View style={[a.flex_1]}> a.absolute,
{isConvoActive(convoState) ? ( a.z_10,
<MessagesList /> a.w_full,
) : ( a.h_full,
<ListMaybePlaceholder isLoading /> a.justify_center,
)} a.align_center,
{!hasInitiallyRendered && ( t.atoms.bg,
<View ]}>
style={[ <View style={[{marginBottom: 75}]}>
a.absolute, <Loader size="xl" />
a.z_10, </View>
a.w_full,
a.h_full,
a.justify_center,
a.align_center,
t.atoms.bg,
]}>
<View style={[{marginBottom: 75}]}>
<Loader size="xl" />
</View>
</View>
)}
</View> </View>
</CenteredView> )}
</KeyboardAvoidingView> </View>
</KeyboardProvider> </CenteredView>
) )
} }

View File

@ -18496,11 +18496,6 @@ react-native-ios-context-menu@^1.15.3:
dependencies: dependencies:
"@dominicstop/ts-event-emitter" "^1.1.0" "@dominicstop/ts-event-emitter" "^1.1.0"
react-native-keyboard-controller@^1.11.7:
version "1.11.7"
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.11.7.tgz#85640374e4c3627c3b667256a1d308698ff80393"
integrity sha512-K2zlqVyWX4QO7r+dHMQgZT41G2dSEWtDYgBdht1WVyTaMQmwTMalZcHCWBVOnzyGaJq/hMKhF1kSPqJP1xqSFA==
react-native-pager-view@6.2.3: react-native-pager-view@6.2.3:
version "6.2.3" version "6.2.3"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"