[🐴] 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 * nitzio/stable
parent
da2bdf5d6f
commit
b15b49a48f
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue