Skip to content

Commit

Permalink
[🐴] Only scroll down one "screen" in height when foregrounding (#4027)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* import

---------

Co-authored-by: Samuel Newman <[email protected]>
  • Loading branch information
haileyok and mozzius authored May 16, 2024
1 parent b15b49a commit ef0ce95
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 12 deletions.
47 changes: 47 additions & 0 deletions src/components/dms/NewMessagesPill.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
39 changes: 39 additions & 0 deletions src/lib/custom-animations/ScaleAndFade.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
62 changes: 50 additions & 12 deletions src/screens/Messages/Conversation/MessagesList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useCallback, useRef} from 'react'
import {FlatList, View} from 'react-native'
import Animated, {
runOnJS,
useAnimatedKeyboard,
useAnimatedReaction,
useAnimatedStyle,
Expand All @@ -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'

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -96,33 +102,48 @@ 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,
offset: height - contentHeight.value,
})
}

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,
hasInitiallyScrolled.value,
isAtBottom.value,
isAtTop.value,
isMomentumScrolling,
layoutHeight.value,
convo.items.length,
keyboardIsOpening.value,
],
)
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit ef0ce95

Please sign in to comment.