[🐴] 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-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.11.7",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",

View File

@ -65,7 +65,7 @@ export function MessageInput({
const keyboardHeight = Keyboard.metrics()?.height ?? 0
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
setMaxHeight(max)
@ -108,7 +108,6 @@ export function MessageInput({
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
scrollEnabled={isInputScrollable}
blurOnSubmit={false}
onFocus={scrollToEnd}
onContentSizeChange={onInputLayout}
ref={inputRef}
hitSlop={HITSLOP_10}

View File

@ -1,12 +1,17 @@
import React, {useCallback, useRef} from 'react'
import {FlatList, View} from 'react-native'
import {useKeyboardHandler} from 'react-native-keyboard-controller'
import {runOnJS, useSharedValue} from 'react-native-reanimated'
import Animated, {
useAnimatedKeyboard,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
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 {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 {ConvoItem} from '#/state/messages/convo/types'
import {useAgent} from '#/state/session'
@ -15,7 +20,7 @@ import {isWeb} from 'platform/detection'
import {List} from 'view/com/util/List'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
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 {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@ -55,6 +60,7 @@ function onScrollToIndexFailed() {
}
export function MessagesList() {
const t = useTheme()
const convo = useConvoActive()
const {getAgent} = useAgent()
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
// Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not.
const isMomentumScrolling = useSharedValue(false)
const hasInitiallyScrolled = useSharedValue(false)
const keyboardIsOpening = useSharedValue(false)
// 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
@ -101,22 +107,23 @@ export function MessagesList() {
contentHeight.value = height
// This number _must_ be the height of the MaybeLoader component
if (height <= 50 || !isAtBottom.value) {
if (height <= 50 || (!isAtBottom.value && !keyboardIsOpening.value)) {
return
}
flatListRef.current?.scrollToOffset({
animated: hasInitiallyScrolled.value,
animated: hasInitiallyScrolled.value && !keyboardIsOpening.value,
offset: height,
})
isMomentumScrolling.value = true
},
[
contentHeight,
hasInitiallyScrolled,
hasInitiallyScrolled.value,
isAtBottom.value,
isAtTop.value,
isMomentumScrolling,
keyboardIsOpening.value,
],
)
@ -187,17 +194,46 @@ export function MessagesList() {
})
}, [isMomentumScrolling])
// This is only used inside the useKeyboardHandler because the worklet won't work with a ref directly.
const scrollToEndNow = React.useCallback(() => {
flatListRef.current?.scrollToEnd({animated: false})
}, [])
// -- Keyboard animation handling
const animatedKeyboard = useAnimatedKeyboard()
const {gtMobile} = useBreakpoints()
const {bottom: bottomInset} = useSafeAreaInsets()
const nativeBottomBarHeight = isIOS ? 42 : 60
const bottomOffset =
isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight
useKeyboardHandler({
onMove: () => {
'worklet'
runOnJS(scrollToEndNow)()
// We need to keep track of when the keyboard is animating and when it isn't, since we want our `onContentSizeChanged`
// callback to animate the scroll _only_ when the keyboard isn't animating. Any time the previous value of kb height
// is different, we know that it is animating. When it finally settles, now will be equal to prev.
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 (
<>
@ -211,8 +247,9 @@ export function MessagesList() {
containWeb={true}
contentContainerStyle={[a.px_md]}
disableVirtualization={true}
initialNumToRender={isNative ? 30 : 60}
maxToRenderPerBatch={isWeb ? 30 : 60}
// The extra two items account for the header and the footer components
initialNumToRender={isNative ? 32 : 62}
maxToRenderPerBatch={isWeb ? 32 : 62}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{
@ -227,9 +264,12 @@ export function MessagesList() {
ListHeaderComponent={
<MaybeLoader isLoading={convo.isFetchingHistory} />
}
ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />}
/>
</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 {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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
@ -18,7 +15,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {BACK_HITSLOP} from 'lib/constants'
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 {ConvoStatus} from 'state/messages/convo/types'
import {useSetMinimalShellMode} from 'state/shell'
@ -39,8 +36,8 @@ type Props = NativeStackScreenProps<
>
export function MessagesConversationScreen({route}: Props) {
const gate = useGate()
const setMinimalShellMode = useSetMinimalShellMode()
const {gtMobile} = useBreakpoints()
const setMinimalShellMode = useSetMinimalShellMode()
const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId()
@ -57,7 +54,7 @@ export function MessagesConversationScreen({route}: Props) {
setCurrentConvoId(undefined)
setMinimalShellMode(false)
}
}, [convoId, gtMobile, setCurrentConvoId, setMinimalShellMode]),
}, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]),
)
if (!gate('dms')) return <ClipClopGate />
@ -76,9 +73,6 @@ function Inner() {
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
// 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
@ -111,45 +105,33 @@ function Inner() {
/*
* Any other convo states (atm) are "ready" states
*/
return (
<KeyboardProvider>
<KeyboardAvoidingView
style={[
a.flex_1,
isNative && {marginBottom: bottomInset + nativeBottomBarHeight},
]}
keyboardVerticalOffset={isIOS ? topInset : 0}
behavior="padding"
contentContainerStyle={a.flex_1}>
<CenteredView style={a.flex_1} sideBorders>
<Header profile={convoState.recipients?.[0]} />
<View style={[a.flex_1]}>
{isConvoActive(convoState) ? (
<MessagesList />
) : (
<ListMaybePlaceholder isLoading />
)}
{!hasInitiallyRendered && (
<View
style={[
a.absolute,
a.z_10,
a.w_full,
a.h_full,
a.justify_center,
a.align_center,
t.atoms.bg,
]}>
<View style={[{marginBottom: 75}]}>
<Loader size="xl" />
</View>
</View>
)}
<CenteredView style={[a.flex_1]} sideBorders>
<Header profile={convoState.recipients?.[0]} />
<View style={[a.flex_1]}>
{isConvoActive(convoState) ? (
<MessagesList />
) : (
<ListMaybePlaceholder isLoading />
)}
{!hasInitiallyRendered && (
<View
style={[
a.absolute,
a.z_10,
a.w_full,
a.h_full,
a.justify_center,
a.align_center,
t.atoms.bg,
]}>
<View style={[{marginBottom: 75}]}>
<Loader size="xl" />
</View>
</View>
</CenteredView>
</KeyboardAvoidingView>
</KeyboardProvider>
)}
</View>
</CenteredView>
)
}

View File

@ -18496,11 +18496,6 @@ react-native-ios-context-menu@^1.15.3:
dependencies:
"@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:
version "6.2.3"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"