Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] 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>

authored by hailey.at

Samuel Newman and committed by
GitHub
ef0ce951 b15b49a4

+136 -12
+47
src/components/dms/NewMessagesPill.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import Animated from 'react-native-reanimated' 4 + import {Trans} from '@lingui/macro' 5 + 6 + import { 7 + ScaleAndFadeIn, 8 + ScaleAndFadeOut, 9 + } from 'lib/custom-animations/ScaleAndFade' 10 + import {atoms as a, useTheme} from '#/alf' 11 + import {Text} from '#/components/Typography' 12 + 13 + export function NewMessagesPill() { 14 + const t = useTheme() 15 + 16 + React.useEffect(() => {}, []) 17 + 18 + return ( 19 + <Animated.View 20 + style={[ 21 + a.py_sm, 22 + a.rounded_full, 23 + a.shadow_sm, 24 + a.border, 25 + t.atoms.bg_contrast_50, 26 + t.atoms.border_contrast_medium, 27 + { 28 + position: 'absolute', 29 + bottom: 70, 30 + width: '40%', 31 + left: '30%', 32 + alignItems: 'center', 33 + shadowOpacity: 0.125, 34 + shadowRadius: 12, 35 + shadowOffset: {width: 0, height: 5}, 36 + }, 37 + ]} 38 + entering={ScaleAndFadeIn} 39 + exiting={ScaleAndFadeOut}> 40 + <View style={{flex: 1}}> 41 + <Text style={[a.font_bold]}> 42 + <Trans>New messages</Trans> 43 + </Text> 44 + </View> 45 + </Animated.View> 46 + ) 47 + }
+39
src/lib/custom-animations/ScaleAndFade.ts
··· 1 + import {withTiming} from 'react-native-reanimated' 2 + 3 + export function ScaleAndFadeIn() { 4 + 'worklet' 5 + 6 + const animations = { 7 + opacity: withTiming(1), 8 + transform: [{scale: withTiming(1)}], 9 + } 10 + 11 + const initialValues = { 12 + opacity: 0, 13 + transform: [{scale: 0.7}], 14 + } 15 + 16 + return { 17 + animations, 18 + initialValues, 19 + } 20 + } 21 + 22 + export function ScaleAndFadeOut() { 23 + 'worklet' 24 + 25 + const animations = { 26 + opacity: withTiming(0), 27 + transform: [{scale: withTiming(0.7)}], 28 + } 29 + 30 + const initialValues = { 31 + opacity: 1, 32 + transform: [{scale: 1}], 33 + } 34 + 35 + return { 36 + animations, 37 + initialValues, 38 + } 39 + }
+50 -12
src/screens/Messages/Conversation/MessagesList.tsx
··· 1 1 import React, {useCallback, useRef} from 'react' 2 2 import {FlatList, View} from 'react-native' 3 3 import Animated, { 4 + runOnJS, 4 5 useAnimatedKeyboard, 5 6 useAnimatedReaction, 6 7 useAnimatedStyle, ··· 22 23 import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' 23 24 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 24 25 import {MessageItem} from '#/components/dms/MessageItem' 26 + import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 25 27 import {Loader} from '#/components/Loader' 26 28 import {Text} from '#/components/Typography' 27 29 ··· 64 66 const convo = useConvoActive() 65 67 const {getAgent} = useAgent() 66 68 const flatListRef = useRef<FlatList>(null) 69 + 70 + const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false) 67 71 68 72 // 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 69 73 // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to ··· 76 80 // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing 77 81 // onStartReached to fire. 78 82 const contentHeight = useSharedValue(0) 83 + const prevItemCount = useRef(0) 79 84 80 85 // We don't want to call `scrollToEnd` again if we are already scolling to the end, because this creates a bit of jank 81 86 // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not. 82 87 const isMomentumScrolling = useSharedValue(false) 83 88 const hasInitiallyScrolled = useSharedValue(false) 84 89 const keyboardIsOpening = useSharedValue(false) 90 + const layoutHeight = useSharedValue(0) 85 91 86 92 // Every time the content size changes, that means one of two things is happening: 87 93 // 1. New messages are being added from the log or from a message you have sent ··· 96 102 const onContentSizeChange = useCallback( 97 103 (_: number, height: number) => { 98 104 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the 99 - // previous offset whenever we add new content to the previous offset whenever we add new content to the list. 105 + // previous off whenever we add new content to the previous offset whenever we add new content to the list. 100 106 if (isWeb && isAtTop.value && hasInitiallyScrolled.value) { 101 107 flatListRef.current?.scrollToOffset({ 102 108 animated: false, ··· 104 110 }) 105 111 } 106 112 107 - contentHeight.value = height 108 - 109 113 // This number _must_ be the height of the MaybeLoader component 110 - if (height <= 50 || (!isAtBottom.value && !keyboardIsOpening.value)) { 111 - return 112 - } 114 + if (height > 50 && (isAtBottom.value || keyboardIsOpening.value)) { 115 + let newOffset = height 113 116 114 - flatListRef.current?.scrollToOffset({ 115 - animated: hasInitiallyScrolled.value && !keyboardIsOpening.value, 116 - offset: height, 117 - }) 118 - isMomentumScrolling.value = true 117 + // If the size of the content is changing by more than the height of the screen, then we should only 118 + // scroll 1 screen down, and let the user scroll the rest. However, because a single message could be 119 + // really large - and the normal chat behavior would be to still scroll to the end if it's only one 120 + // message - we ignore this rule if there's only one additional message 121 + if ( 122 + hasInitiallyScrolled.value && 123 + height - contentHeight.value > layoutHeight.value - 50 && 124 + convo.items.length - prevItemCount.current > 1 125 + ) { 126 + newOffset = contentHeight.value - 50 127 + setShowNewMessagesPill(true) 128 + } 129 + 130 + flatListRef.current?.scrollToOffset({ 131 + animated: hasInitiallyScrolled.value && !keyboardIsOpening.value, 132 + offset: newOffset, 133 + }) 134 + isMomentumScrolling.value = true 135 + } 136 + contentHeight.value = height 137 + prevItemCount.current = convo.items.length 119 138 }, 120 139 [ 121 140 contentHeight, ··· 123 142 isAtBottom.value, 124 143 isAtTop.value, 125 144 isMomentumScrolling, 145 + layoutHeight.value, 146 + convo.items.length, 126 147 keyboardIsOpening.value, 127 148 ], 128 149 ) ··· 163 184 const onScroll = React.useCallback( 164 185 (e: ReanimatedScrollEvent) => { 165 186 'worklet' 187 + layoutHeight.value = e.layoutMeasurement.height 188 + 166 189 const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height 167 190 191 + if ( 192 + showNewMessagesPill && 193 + e.contentSize.height - e.layoutMeasurement.height / 3 < bottomOffset 194 + ) { 195 + runOnJS(setShowNewMessagesPill)(false) 196 + } 197 + 168 198 // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom 169 199 // when a new message is added, hence the 100 pixel offset 170 200 isAtBottom.value = e.contentSize.height - 100 < bottomOffset ··· 177 207 hasInitiallyScrolled.value = true 178 208 } 179 209 }, 180 - [contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop], 210 + [ 211 + layoutHeight, 212 + showNewMessagesPill, 213 + isAtBottom, 214 + isAtTop, 215 + contentHeight.value, 216 + hasInitiallyScrolled, 217 + ], 181 218 ) 182 219 183 220 const onMomentumEnd = React.useCallback(() => { ··· 267 304 ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />} 268 305 /> 269 306 </ScrollProvider> 307 + {showNewMessagesPill && <NewMessagesPill />} 270 308 <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}> 271 309 <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> 272 310 </Animated.View>