diff options
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessageInput.tsx | 3 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessagesList.tsx | 80 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/index.tsx | 74 | ||||
-rw-r--r-- | yarn.lock | 5 |
5 files changed, 89 insertions, 74 deletions
diff --git a/package.json b/package.json index 9f1444d9d..6cb83a3e5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx index 632544723..d937cc3e1 100644 --- a/src/screens/Messages/Conversation/MessageInput.tsx +++ b/src/screens/Messages/Conversation/MessageInput.tsx @@ -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} diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 9c7774e57..ca5d44877 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -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> </> ) } diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index 4a7c4ce9b..070175d47 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -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> ) } diff --git a/yarn.lock b/yarn.lock index ca2ae379c..1e7fd33bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |