about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-05-16 10:40:12 -0700
committerGitHub <noreply@github.com>2024-05-16 10:40:12 -0700
commitef0ce951e7c95ce3374a3e49db16f72a344ef779 (patch)
treeb263d7613fce22ad4416dee5cd4ab2cb5b3f1f0c /src
parentb15b49a48f2d8242e31ba5fdde52123fa5e7ff64 (diff)
downloadvoidsky-ef0ce951e7c95ce3374a3e49db16f72a344ef779.tar.zst
[🐴] Only scroll down one "screen" in height when foregrounding (#4027)
* maintain position after foreground

* one possibility

* don't overscroll when content size changes.

* ignore the rule on 1 item

* fix

* [🐴] Pill for additional unreads when coming from background (#4043)

* create a pill with some animatons

* add some basic styles to the pill

* make the animations reusable

* bit better styling

* rm logs

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* import

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/components/dms/NewMessagesPill.tsx47
-rw-r--r--src/lib/custom-animations/ScaleAndFade.ts39
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx62
3 files changed, 136 insertions, 12 deletions
diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx
new file mode 100644
index 000000000..4a0ba22c9
--- /dev/null
+++ b/src/components/dms/NewMessagesPill.tsx
@@ -0,0 +1,47 @@
+import React from 'react'
+import {View} from 'react-native'
+import Animated from 'react-native-reanimated'
+import {Trans} from '@lingui/macro'
+
+import {
+  ScaleAndFadeIn,
+  ScaleAndFadeOut,
+} from 'lib/custom-animations/ScaleAndFade'
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+
+export function NewMessagesPill() {
+  const t = useTheme()
+
+  React.useEffect(() => {}, [])
+
+  return (
+    <Animated.View
+      style={[
+        a.py_sm,
+        a.rounded_full,
+        a.shadow_sm,
+        a.border,
+        t.atoms.bg_contrast_50,
+        t.atoms.border_contrast_medium,
+        {
+          position: 'absolute',
+          bottom: 70,
+          width: '40%',
+          left: '30%',
+          alignItems: 'center',
+          shadowOpacity: 0.125,
+          shadowRadius: 12,
+          shadowOffset: {width: 0, height: 5},
+        },
+      ]}
+      entering={ScaleAndFadeIn}
+      exiting={ScaleAndFadeOut}>
+      <View style={{flex: 1}}>
+        <Text style={[a.font_bold]}>
+          <Trans>New messages</Trans>
+        </Text>
+      </View>
+    </Animated.View>
+  )
+}
diff --git a/src/lib/custom-animations/ScaleAndFade.ts b/src/lib/custom-animations/ScaleAndFade.ts
new file mode 100644
index 000000000..ad2c15f8f
--- /dev/null
+++ b/src/lib/custom-animations/ScaleAndFade.ts
@@ -0,0 +1,39 @@
+import {withTiming} from 'react-native-reanimated'
+
+export function ScaleAndFadeIn() {
+  'worklet'
+
+  const animations = {
+    opacity: withTiming(1),
+    transform: [{scale: withTiming(1)}],
+  }
+
+  const initialValues = {
+    opacity: 0,
+    transform: [{scale: 0.7}],
+  }
+
+  return {
+    animations,
+    initialValues,
+  }
+}
+
+export function ScaleAndFadeOut() {
+  'worklet'
+
+  const animations = {
+    opacity: withTiming(0),
+    transform: [{scale: withTiming(0.7)}],
+  }
+
+  const initialValues = {
+    opacity: 1,
+    transform: [{scale: 1}],
+  }
+
+  return {
+    animations,
+    initialValues,
+  }
+}
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index ca5d44877..a8f9d344d 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -1,6 +1,7 @@
 import React, {useCallback, useRef} from 'react'
 import {FlatList, View} from 'react-native'
 import Animated, {
+  runOnJS,
   useAnimatedKeyboard,
   useAnimatedReaction,
   useAnimatedStyle,
@@ -22,6 +23,7 @@ import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
 import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {MessageItem} from '#/components/dms/MessageItem'
+import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 
@@ -65,6 +67,8 @@ export function MessagesList() {
   const {getAgent} = useAgent()
   const flatListRef = useRef<FlatList>(null)
 
+  const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false)
+
   // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items
   // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to
   // the bottom.
@@ -76,12 +80,14 @@ export function MessagesList() {
   // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
   // onStartReached to fire.
   const contentHeight = useSharedValue(0)
+  const prevItemCount = useRef(0)
 
   // 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)
+  const layoutHeight = useSharedValue(0)
 
   // 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
@@ -96,7 +102,7 @@ export function MessagesList() {
   const onContentSizeChange = useCallback(
     (_: number, height: number) => {
       // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
-      // previous offset whenever we add new content to the previous offset whenever we add new content to the list.
+      // 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) {
         flatListRef.current?.scrollToOffset({
           animated: false,
@@ -104,18 +110,31 @@ export function MessagesList() {
         })
       }
 
-      contentHeight.value = height
-
       // This number _must_ be the height of the MaybeLoader component
-      if (height <= 50 || (!isAtBottom.value && !keyboardIsOpening.value)) {
-        return
-      }
+      if (height > 50 && (isAtBottom.value || keyboardIsOpening.value)) {
+        let newOffset = height
 
-      flatListRef.current?.scrollToOffset({
-        animated: hasInitiallyScrolled.value && !keyboardIsOpening.value,
-        offset: height,
-      })
-      isMomentumScrolling.value = true
+        // If the size of the content is changing by more than the height of the screen, then we should only
+        // scroll 1 screen down, and let the user scroll the rest. However, because a single message could be
+        // 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 &&
+          height - contentHeight.value > layoutHeight.value - 50 &&
+          convo.items.length - prevItemCount.current > 1
+        ) {
+          newOffset = contentHeight.value - 50
+          setShowNewMessagesPill(true)
+        }
+
+        flatListRef.current?.scrollToOffset({
+          animated: hasInitiallyScrolled.value && !keyboardIsOpening.value,
+          offset: newOffset,
+        })
+        isMomentumScrolling.value = true
+      }
+      contentHeight.value = height
+      prevItemCount.current = convo.items.length
     },
     [
       contentHeight,
@@ -123,6 +142,8 @@ export function MessagesList() {
       isAtBottom.value,
       isAtTop.value,
       isMomentumScrolling,
+      layoutHeight.value,
+      convo.items.length,
       keyboardIsOpening.value,
     ],
   )
@@ -163,8 +184,17 @@ export function MessagesList() {
   const onScroll = React.useCallback(
     (e: ReanimatedScrollEvent) => {
       'worklet'
+      layoutHeight.value = e.layoutMeasurement.height
+
       const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
 
+      if (
+        showNewMessagesPill &&
+        e.contentSize.height - e.layoutMeasurement.height / 3 < bottomOffset
+      ) {
+        runOnJS(setShowNewMessagesPill)(false)
+      }
+
       // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
       // when a new message is added, hence the 100 pixel offset
       isAtBottom.value = e.contentSize.height - 100 < bottomOffset
@@ -177,7 +207,14 @@ export function MessagesList() {
         hasInitiallyScrolled.value = true
       }
     },
-    [contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop],
+    [
+      layoutHeight,
+      showNewMessagesPill,
+      isAtBottom,
+      isAtTop,
+      contentHeight.value,
+      hasInitiallyScrolled,
+    ],
   )
 
   const onMomentumEnd = React.useCallback(() => {
@@ -267,6 +304,7 @@ export function MessagesList() {
           ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />}
         />
       </ScrollProvider>
+      {showNewMessagesPill && <NewMessagesPill />}
       <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}>
         <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />
       </Animated.View>