diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx index d9360753..3de15e66 100644 --- a/src/screens/Messages/Conversation/MessageInput.tsx +++ b/src/screens/Messages/Conversation/MessageInput.tsx @@ -13,6 +13,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {HITSLOP_10} from '#/lib/constants' +import {useHaptics} from 'lib/haptics' import {atoms as a, useTheme} from '#/alf' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' @@ -25,6 +26,7 @@ export function MessageInput({ }) { const {_} = useLingui() const t = useTheme() + const playHaptic = useHaptics() const [message, setMessage] = React.useState('') const [maxHeight, setMaxHeight] = React.useState() const [isInputScrollable, setIsInputScrollable] = React.useState(false) @@ -38,11 +40,12 @@ export function MessageInput({ return } onSendMessage(message.trimEnd()) + playHaptic() setMessage('') setTimeout(() => { inputRef.current?.focus() }, 100) - }, [message, onSendMessage]) + }, [message, onSendMessage, playHaptic]) const onInputLayout = React.useCallback( (e: NativeSyntheticEvent) => { diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 7a14979e..73743b5f 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -1,6 +1,9 @@ import React, {useCallback, useRef} from 'react' -import {FlatList, Platform, View} from 'react-native' -import {KeyboardAvoidingView} from 'react-native-keyboard-controller' +import {FlatList, View} from 'react-native' +import { + KeyboardAvoidingView, + useKeyboardHandler, +} from 'react-native-keyboard-controller' import {runOnJS, useSharedValue} from 'react-native-reanimated' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -15,7 +18,6 @@ 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 {useScrollToEndOnFocus} from '#/screens/Messages/Conversation/useScrollToEndOnFocus' import {atoms as a, useBreakpoints} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {MessageItem} from '#/components/dms/MessageItem' @@ -96,12 +98,11 @@ export function MessagesList() { // onStartReached to fire. const contentHeight = useSharedValue(0) - const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(false) + // 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) - // This is only used on native because `Keyboard` can't be imported on web. On web, an input focus will immediately - // trigger scrolling to the bottom. On native however, we need to wait for the keyboard to present before scrolling, - // which is what this hook listens for - useScrollToEndOnFocus(flatListRef) + const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(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 @@ -126,8 +127,14 @@ export function MessagesList() { animated: hasInitiallyScrolled, offset: height, }) + isMomentumScrolling.value = true }, - [contentHeight, hasInitiallyScrolled, isAtBottom.value], + [ + contentHeight, + hasInitiallyScrolled, + isAtBottom.value, + isMomentumScrolling, + ], ) // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` @@ -168,28 +175,47 @@ export function MessagesList() { [contentHeight.value, hasInitiallyScrolled, isAtBottom], ) - const scrollToEnd = React.useCallback(() => { - requestAnimationFrame(() => - flatListRef.current?.scrollToEnd({animated: true}), - ) - }, []) + const onMomentumEnd = React.useCallback(() => { + 'worklet' + isMomentumScrolling.value = false + }, [isMomentumScrolling]) - const {bottom: bottomInset} = useSafeAreaInsets() + const scrollToEnd = React.useCallback(() => { + requestAnimationFrame(() => { + if (isMomentumScrolling.value) return + + flatListRef.current?.scrollToEnd({animated: true}) + isMomentumScrolling.value = true + }) + }, [isMomentumScrolling]) + + const {bottom: bottomInset, top: topInset} = useSafeAreaInsets() const {gtMobile} = useBreakpoints() const bottomBarHeight = gtMobile ? 0 : isIOS ? 40 : 60 - const keyboardVerticalOffset = useKeyboardVerticalOffset() + + // 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}) + }, []) + + useKeyboardHandler({ + onMove: () => { + 'worklet' + runOnJS(scrollToEndNow)() + }, + }) return ( {/* This view keeps the scroll bar and content within the CenterView on web, otherwise the entire window would scroll */} {/* @ts-expect-error web only */} - {/* Custom scroll provider so we can use the `onScroll` event in our custom List implementation */} - + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} + ) } - -function useKeyboardVerticalOffset() { - const {top: topInset} = useSafeAreaInsets() - - return Platform.select({ - ios: topInset, - // I thought this might be the navigation bar height, but not sure - // 25 is just trial and error - android: 25, - default: 0, - }) -} diff --git a/src/screens/Messages/Conversation/useScrollToEndOnFocus.ts b/src/screens/Messages/Conversation/useScrollToEndOnFocus.ts deleted file mode 100644 index e6e04c0b..00000000 --- a/src/screens/Messages/Conversation/useScrollToEndOnFocus.ts +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import {FlatList, Keyboard} from 'react-native' - -export function useScrollToEndOnFocus(flatListRef: React.RefObject) { - React.useEffect(() => { - const listener = Keyboard.addListener('keyboardDidShow', () => { - requestAnimationFrame(() => { - flatListRef.current?.scrollToEnd({animated: true}) - }) - }) - - return () => { - listener.remove() - } - }, [flatListRef]) -} diff --git a/src/screens/Messages/Conversation/useScrollToEndOnFocus.web.ts b/src/screens/Messages/Conversation/useScrollToEndOnFocus.web.ts deleted file mode 100644 index 8ee30185..00000000 --- a/src/screens/Messages/Conversation/useScrollToEndOnFocus.web.ts +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import {FlatList} from 'react-native' - -// Stub for web -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useScrollToEndOnFocus(flatListRef: React.RefObject) {}