about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx3
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx80
-rw-r--r--src/screens/Messages/Conversation/index.tsx74
-rw-r--r--yarn.lock5
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"