[🐴] List Adjustments (#3857)

zio/stable
Hailey 2024-05-04 13:22:14 -07:00 committed by GitHub
parent c223bcdaf7
commit eb55bdf172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 49 additions and 54 deletions

View File

@ -13,6 +13,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {useHaptics} from 'lib/haptics'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@ -25,6 +26,7 @@ export function MessageInput({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const playHaptic = useHaptics()
const [message, setMessage] = React.useState('') const [message, setMessage] = React.useState('')
const [maxHeight, setMaxHeight] = React.useState<number | undefined>() const [maxHeight, setMaxHeight] = React.useState<number | undefined>()
const [isInputScrollable, setIsInputScrollable] = React.useState(false) const [isInputScrollable, setIsInputScrollable] = React.useState(false)
@ -38,11 +40,12 @@ export function MessageInput({
return return
} }
onSendMessage(message.trimEnd()) onSendMessage(message.trimEnd())
playHaptic()
setMessage('') setMessage('')
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, 100) }, 100)
}, [message, onSendMessage]) }, [message, onSendMessage, playHaptic])
const onInputLayout = React.useCallback( const onInputLayout = React.useCallback(
(e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => { (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {

View File

@ -1,6 +1,9 @@
import React, {useCallback, useRef} from 'react' import React, {useCallback, useRef} from 'react'
import {FlatList, Platform, View} from 'react-native' import {FlatList, View} from 'react-native'
import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import {
KeyboardAvoidingView,
useKeyboardHandler,
} from 'react-native-keyboard-controller'
import {runOnJS, useSharedValue} from 'react-native-reanimated' import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
@ -15,7 +18,6 @@ import {isWeb} from 'platform/detection'
import {List} from 'view/com/util/List' import {List} from 'view/com/util/List'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
import {useScrollToEndOnFocus} from '#/screens/Messages/Conversation/useScrollToEndOnFocus'
import {atoms as a, useBreakpoints} from '#/alf' import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {MessageItem} from '#/components/dms/MessageItem' import {MessageItem} from '#/components/dms/MessageItem'
@ -96,12 +98,11 @@ export function MessagesList() {
// onStartReached to fire. // onStartReached to fire.
const contentHeight = useSharedValue(0) const contentHeight = useSharedValue(0)
const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(false) // 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)
// This is only used on native because `Keyboard` can't be imported on web. On web, an input focus will immediately const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(false)
// trigger scrolling to the bottom. On native however, we need to wait for the keyboard to present before scrolling,
// which is what this hook listens for
useScrollToEndOnFocus(flatListRef)
// Every time the content size changes, that means one of two things is happening: // 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 // 1. New messages are being added from the log or from a message you have sent
@ -126,8 +127,14 @@ export function MessagesList() {
animated: hasInitiallyScrolled, animated: hasInitiallyScrolled,
offset: height, offset: height,
}) })
isMomentumScrolling.value = true
}, },
[contentHeight, hasInitiallyScrolled, isAtBottom.value], [
contentHeight,
hasInitiallyScrolled,
isAtBottom.value,
isMomentumScrolling,
],
) )
// The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached`
@ -168,28 +175,47 @@ export function MessagesList() {
[contentHeight.value, hasInitiallyScrolled, isAtBottom], [contentHeight.value, hasInitiallyScrolled, isAtBottom],
) )
const scrollToEnd = React.useCallback(() => { const onMomentumEnd = React.useCallback(() => {
requestAnimationFrame(() => 'worklet'
flatListRef.current?.scrollToEnd({animated: true}), isMomentumScrolling.value = false
) }, [isMomentumScrolling])
}, [])
const {bottom: bottomInset} = useSafeAreaInsets() const scrollToEnd = React.useCallback(() => {
requestAnimationFrame(() => {
if (isMomentumScrolling.value) return
flatListRef.current?.scrollToEnd({animated: true})
isMomentumScrolling.value = true
})
}, [isMomentumScrolling])
const {bottom: bottomInset, top: topInset} = useSafeAreaInsets()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const bottomBarHeight = gtMobile ? 0 : isIOS ? 40 : 60 const bottomBarHeight = gtMobile ? 0 : isIOS ? 40 : 60
const keyboardVerticalOffset = useKeyboardVerticalOffset()
// This is only used inside the useKeyboardHandler because the worklet won't work with a ref directly.
const scrollToEndNow = React.useCallback(() => {
flatListRef.current?.scrollToEnd({animated: false})
}, [])
useKeyboardHandler({
onMove: () => {
'worklet'
runOnJS(scrollToEndNow)()
},
})
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={[a.flex_1, {marginBottom: bottomInset + bottomBarHeight}]} style={[a.flex_1, {marginBottom: bottomInset + bottomBarHeight}]}
keyboardVerticalOffset={keyboardVerticalOffset} keyboardVerticalOffset={isIOS ? topInset : 0}
behavior="padding" behavior="padding"
contentContainerStyle={a.flex_1}> contentContainerStyle={a.flex_1}>
{/* This view keeps the scroll bar and content within the CenterView on web, otherwise the entire window would scroll */} {/* This view keeps the scroll bar and content within the CenterView on web, otherwise the entire window would scroll */}
{/* @ts-expect-error web only */} {/* @ts-expect-error web only */}
<View style={[{flex: 1}, isWeb && {'overflow-y': 'scroll'}]}> <View style={[{flex: 1}, isWeb && {'overflow-y': 'scroll'}]}>
{/* Custom scroll provider so we can use the `onScroll` event in our custom List implementation */} {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
<ScrollProvider onScroll={onScroll}> <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
<List <List
ref={flatListRef} ref={flatListRef}
data={chat.status === ConvoStatus.Ready ? chat.items : undefined} data={chat.status === ConvoStatus.Ready ? chat.items : undefined}
@ -222,15 +248,3 @@ export function MessagesList() {
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }
function useKeyboardVerticalOffset() {
const {top: topInset} = useSafeAreaInsets()
return Platform.select({
ios: topInset,
// I thought this might be the navigation bar height, but not sure
// 25 is just trial and error
android: 25,
default: 0,
})
}

View File

@ -1,16 +0,0 @@
import React from 'react'
import {FlatList, Keyboard} from 'react-native'
export function useScrollToEndOnFocus(flatListRef: React.RefObject<FlatList>) {
React.useEffect(() => {
const listener = Keyboard.addListener('keyboardDidShow', () => {
requestAnimationFrame(() => {
flatListRef.current?.scrollToEnd({animated: true})
})
})
return () => {
listener.remove()
}
}, [flatListRef])
}

View File

@ -1,6 +0,0 @@
import React from 'react'
import {FlatList} from 'react-native'
// Stub for web
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function useScrollToEndOnFocus(flatListRef: React.RefObject<FlatList>) {}