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/MessagesList.tsx109
-rw-r--r--src/screens/Messages/Conversation/index.tsx33
2 files changed, 57 insertions, 85 deletions
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 182eb63ec..d36fac8ae 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -2,7 +2,6 @@ import React, {useCallback, useRef} from 'react'
 import {FlatList, View} from 'react-native'
 import Animated, {
   runOnJS,
-  runOnUI,
   scrollTo,
   useAnimatedKeyboard,
   useAnimatedReaction,
@@ -24,7 +23,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, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints} from '#/alf'
 import {MessageItem} from '#/components/dms/MessageItem'
 import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
 import {Loader} from '#/components/Loader'
@@ -64,8 +63,13 @@ function onScrollToIndexFailed() {
   // Placeholder function. You have to give FlatList something or else it will error.
 }
 
-export function MessagesList() {
-  const t = useTheme()
+export function MessagesList({
+  hasScrolled,
+  setHasScrolled,
+}: {
+  hasScrolled: boolean
+  setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
+}) {
   const convo = useConvoActive()
   const {getAgent} = useAgent()
   const flatListRef = useAnimatedRef<FlatList>()
@@ -88,22 +92,9 @@ 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 keyboardIsAnimating = useSharedValue(false)
   const layoutHeight = useSharedValue(0)
 
-  // Since we are using the native web methods for scrolling on `List`, we only use the reanimated `scrollTo` on native
-  const scrollToOffset = React.useCallback(
-    (offset: number, animated: boolean) => {
-      if (isWeb) {
-        flatListRef.current?.scrollToOffset({offset, animated})
-      } else {
-        runOnUI(scrollTo)(flatListRef, 0, offset, animated)
-      }
-    },
-    [flatListRef],
-  )
-
   // 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
   // 2. Old messages are being prepended to the top
@@ -118,8 +109,11 @@ export function MessagesList() {
     (_: number, height: number) => {
       // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
       // previous off whenever we add new content to the previous offset whenever we add new content to the list.
-      if (isWeb && isAtTop.value && hasInitiallyScrolled.value) {
-        scrollToOffset(height - contentHeight.value, false)
+      if (isWeb && isAtTop.value && hasScrolled) {
+        flatListRef.current?.scrollToOffset({
+          offset: height - contentHeight.value,
+          animated: false,
+        })
       }
 
       // This number _must_ be the height of the MaybeLoader component
@@ -130,40 +124,46 @@ export function MessagesList() {
         // really large - and the normal chat behavior would be to still scroll to the end if it's only one
         // message - we ignore this rule if there's only one additional message
         if (
-          hasInitiallyScrolled.value &&
+          hasScrolled &&
           height - contentHeight.value > layoutHeight.value - 50 &&
           convo.items.length - prevItemCount.current > 1
         ) {
           newOffset = contentHeight.value - 50
           setShowNewMessagesPill(true)
+        } else if (!hasScrolled && !convo.isFetchingHistory) {
+          setHasScrolled(true)
         }
-        scrollToOffset(newOffset, hasInitiallyScrolled.value)
+
+        flatListRef.current?.scrollToOffset({
+          offset: newOffset,
+          animated: hasScrolled,
+        })
         isMomentumScrolling.value = true
       }
       contentHeight.value = height
       prevItemCount.current = convo.items.length
     },
     [
-      contentHeight,
-      scrollToOffset,
-      isMomentumScrolling,
+      hasScrolled,
       convo.items.length,
-      // All of these are stable
+      convo.isFetchingHistory,
+      setHasScrolled,
+      // all of these are stable
+      contentHeight,
+      flatListRef,
       isAtBottom.value,
+      isAtTop.value,
+      isMomentumScrolling,
       keyboardIsAnimating.value,
       layoutHeight.value,
-      hasInitiallyScrolled.value,
-      isAtTop.value,
     ],
   )
 
-  // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached`
-  // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls.
   const onStartReached = useCallback(() => {
-    if (hasInitiallyScrolled.value) {
+    if (hasScrolled) {
       convo.fetchMessageHistory()
     }
-  }, [convo, hasInitiallyScrolled])
+  }, [convo, hasScrolled])
 
   const onSendMessage = useCallback(
     async (text: string) => {
@@ -208,34 +208,20 @@ export function MessagesList() {
       // when a new message is added, hence the 100 pixel offset
       isAtBottom.value = e.contentSize.height - 100 < bottomOffset
       isAtTop.value = e.contentOffset.y <= 1
-
-      // This number _must_ be the height of the MaybeLoader component.
-      // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which
-      // adds a 50 pixel offset.
-      if (contentHeight.value > 50 && !hasInitiallyScrolled.value) {
-        hasInitiallyScrolled.value = true
-      }
     },
-    [
-      layoutHeight,
-      showNewMessagesPill,
-      isAtBottom,
-      isAtTop,
-      hasInitiallyScrolled,
-      contentHeight.value,
-    ],
+    [layoutHeight, showNewMessagesPill, isAtBottom, isAtTop],
   )
 
+  // This tells us when we are no longer scrolling
   const onMomentumEnd = React.useCallback(() => {
     'worklet'
     isMomentumScrolling.value = false
   }, [isMomentumScrolling])
 
-  const scrollToEnd = React.useCallback(() => {
+  const scrollToEndNow = React.useCallback(() => {
     if (isMomentumScrolling.value) return
-    scrollToOffset(contentHeight.value, true)
-    isMomentumScrolling.value = true
-  }, [contentHeight.value, isMomentumScrolling, scrollToOffset])
+    flatListRef.current?.scrollToEnd({animated: false})
+  }, [flatListRef, isMomentumScrolling.value])
 
   // -- Keyboard animation handling
   const animatedKeyboard = useAnimatedKeyboard()
@@ -269,24 +255,15 @@ export function MessagesList() {
 
   // 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(() => ({
+  const animatedStyle = 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 (
-    <>
+    <Animated.View style={[a.flex_1, animatedStyle]}>
       {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
       <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
         <List
@@ -314,13 +291,13 @@ export function MessagesList() {
           ListHeaderComponent={
             <MaybeLoader isLoading={convo.isFetchingHistory} />
           }
-          ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />}
         />
       </ScrollProvider>
+      <MessageInput
+        onSendMessage={onSendMessage}
+        scrollToEnd={scrollToEndNow}
+      />
       {showNewMessagesPill && <NewMessagesPill />}
-      <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}>
-        <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />
-      </Animated.View>
-    </>
+    </Animated.View>
   )
 }
diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx
index 070175d47..3ab00bca1 100644
--- a/src/screens/Messages/Conversation/index.tsx
+++ b/src/screens/Messages/Conversation/index.tsx
@@ -71,23 +71,15 @@ function Inner() {
   const convoState = useConvo()
   const {_} = useLingui()
 
-  const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false)
-
-  // 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
-  // this, we will wait until the first render has completed to remove the loading overlay.
-  React.useEffect(() => {
-    if (
-      !hasInitiallyRendered &&
-      isConvoActive(convoState) &&
-      !convoState.isFetchingHistory
-    ) {
-      setTimeout(() => {
-        setHasInitiallyRendered(true)
-      }, 15)
-    }
-  }, [convoState, hasInitiallyRendered])
+  // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
+  // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
+  // empty. So, we also check for that possible state as well and render once we can.
+  const [hasScrolled, setHasScrolled] = React.useState(false)
+  const readyToShow =
+    hasScrolled ||
+    (convoState.status === ConvoStatus.Ready &&
+      !convoState.isFetchingHistory &&
+      convoState.items.length === 0)
 
   if (convoState.status === ConvoStatus.Error) {
     return (
@@ -110,11 +102,14 @@ function Inner() {
       <Header profile={convoState.recipients?.[0]} />
       <View style={[a.flex_1]}>
         {isConvoActive(convoState) ? (
-          <MessagesList />
+          <MessagesList
+            hasScrolled={hasScrolled}
+            setHasScrolled={setHasScrolled}
+          />
         ) : (
           <ListMaybePlaceholder isLoading />
         )}
-        {!hasInitiallyRendered && (
+        {!readyToShow && (
           <View
             style={[
               a.absolute,