about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx5
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx74
-rw-r--r--src/screens/Messages/Conversation/useScrollToEndOnFocus.ts16
-rw-r--r--src/screens/Messages/Conversation/useScrollToEndOnFocus.web.ts6
4 files changed, 48 insertions, 53 deletions
diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx
index d93607530..3de15e661 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<number | undefined>()
   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<TextInputContentSizeChangeEventData>) => {
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 7a14979e9..73743b5f7 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 onMomentumEnd = React.useCallback(() => {
+    'worklet'
+    isMomentumScrolling.value = false
+  }, [isMomentumScrolling])
+
   const scrollToEnd = React.useCallback(() => {
-    requestAnimationFrame(() =>
-      flatListRef.current?.scrollToEnd({animated: true}),
-    )
-  }, [])
+    requestAnimationFrame(() => {
+      if (isMomentumScrolling.value) return
+
+      flatListRef.current?.scrollToEnd({animated: true})
+      isMomentumScrolling.value = true
+    })
+  }, [isMomentumScrolling])
 
-  const {bottom: bottomInset} = useSafeAreaInsets()
+  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 (
     <KeyboardAvoidingView
       style={[a.flex_1, {marginBottom: bottomInset + bottomBarHeight}]}
-      keyboardVerticalOffset={keyboardVerticalOffset}
+      keyboardVerticalOffset={isIOS ? topInset : 0}
       behavior="padding"
       contentContainerStyle={a.flex_1}>
       {/* This view keeps the scroll bar and content within the CenterView on web, otherwise the entire window would scroll */}
       {/* @ts-expect-error web only */}
       <View style={[{flex: 1}, isWeb && {'overflow-y': 'scroll'}]}>
-        {/* Custom scroll provider so we can use the `onScroll` event in our custom List implementation */}
-        <ScrollProvider onScroll={onScroll}>
+        {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
+        <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
           <List
             ref={flatListRef}
             data={chat.status === ConvoStatus.Ready ? chat.items : undefined}
@@ -222,15 +248,3 @@ export function MessagesList() {
     </KeyboardAvoidingView>
   )
 }
-
-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 e6e04c0b9..000000000
--- 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<FlatList>) {
-  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 8ee30185c..000000000
--- 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<FlatList>) {}