[🐴] 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-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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue