diff options
author | Hailey <me@haileyok.com> | 2024-05-16 10:40:12 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-16 10:40:12 -0700 |
commit | ef0ce951e7c95ce3374a3e49db16f72a344ef779 (patch) | |
tree | b263d7613fce22ad4416dee5cd4ab2cb5b3f1f0c /src | |
parent | b15b49a48f2d8242e31ba5fdde52123fa5e7ff64 (diff) | |
download | voidsky-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.tsx | 47 | ||||
-rw-r--r-- | src/lib/custom-animations/ScaleAndFade.ts | 39 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessagesList.tsx | 62 |
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> |