diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/screens/Messages/Conversation/MessagesList.tsx | 109 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/index.tsx | 33 |
2 files changed, 57 insertions, 85 deletions
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 182eb63ec..d36fac8ae 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useRef} from 'react' import {FlatList, View} from 'react-native' import Animated, { runOnJS, - runOnUI, scrollTo, useAnimatedKeyboard, useAnimatedReaction, @@ -24,7 +23,7 @@ import {isWeb} from 'platform/detection' import {List} from 'view/com/util/List' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useBreakpoints} from '#/alf' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' @@ -64,8 +63,13 @@ function onScrollToIndexFailed() { // Placeholder function. You have to give FlatList something or else it will error. } -export function MessagesList() { - const t = useTheme() +export function MessagesList({ + hasScrolled, + setHasScrolled, +}: { + hasScrolled: boolean + setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> +}) { const convo = useConvoActive() const {getAgent} = useAgent() const flatListRef = useAnimatedRef<FlatList>() @@ -88,22 +92,9 @@ export function MessagesList() { // 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 keyboardIsAnimating = useSharedValue(false) const layoutHeight = useSharedValue(0) - // Since we are using the native web methods for scrolling on `List`, we only use the reanimated `scrollTo` on native - const scrollToOffset = React.useCallback( - (offset: number, animated: boolean) => { - if (isWeb) { - flatListRef.current?.scrollToOffset({offset, animated}) - } else { - runOnUI(scrollTo)(flatListRef, 0, offset, animated) - } - }, - [flatListRef], - ) - // 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 // 2. Old messages are being prepended to the top @@ -118,8 +109,11 @@ export function MessagesList() { (_: number, height: number) => { // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the // 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) { - scrollToOffset(height - contentHeight.value, false) + if (isWeb && isAtTop.value && hasScrolled) { + flatListRef.current?.scrollToOffset({ + offset: height - contentHeight.value, + animated: false, + }) } // This number _must_ be the height of the MaybeLoader component @@ -130,40 +124,46 @@ export function MessagesList() { // 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 && + hasScrolled && height - contentHeight.value > layoutHeight.value - 50 && convo.items.length - prevItemCount.current > 1 ) { newOffset = contentHeight.value - 50 setShowNewMessagesPill(true) + } else if (!hasScrolled && !convo.isFetchingHistory) { + setHasScrolled(true) } - scrollToOffset(newOffset, hasInitiallyScrolled.value) + + flatListRef.current?.scrollToOffset({ + offset: newOffset, + animated: hasScrolled, + }) isMomentumScrolling.value = true } contentHeight.value = height prevItemCount.current = convo.items.length }, [ - contentHeight, - scrollToOffset, - isMomentumScrolling, + hasScrolled, convo.items.length, - // All of these are stable + convo.isFetchingHistory, + setHasScrolled, + // all of these are stable + contentHeight, + flatListRef, isAtBottom.value, + isAtTop.value, + isMomentumScrolling, keyboardIsAnimating.value, layoutHeight.value, - hasInitiallyScrolled.value, - isAtTop.value, ], ) - // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` - // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls. const onStartReached = useCallback(() => { - if (hasInitiallyScrolled.value) { + if (hasScrolled) { convo.fetchMessageHistory() } - }, [convo, hasInitiallyScrolled]) + }, [convo, hasScrolled]) const onSendMessage = useCallback( async (text: string) => { @@ -208,34 +208,20 @@ export function MessagesList() { // when a new message is added, hence the 100 pixel offset isAtBottom.value = e.contentSize.height - 100 < bottomOffset isAtTop.value = e.contentOffset.y <= 1 - - // This number _must_ be the height of the MaybeLoader component. - // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which - // adds a 50 pixel offset. - if (contentHeight.value > 50 && !hasInitiallyScrolled.value) { - hasInitiallyScrolled.value = true - } }, - [ - layoutHeight, - showNewMessagesPill, - isAtBottom, - isAtTop, - hasInitiallyScrolled, - contentHeight.value, - ], + [layoutHeight, showNewMessagesPill, isAtBottom, isAtTop], ) + // This tells us when we are no longer scrolling const onMomentumEnd = React.useCallback(() => { 'worklet' isMomentumScrolling.value = false }, [isMomentumScrolling]) - const scrollToEnd = React.useCallback(() => { + const scrollToEndNow = React.useCallback(() => { if (isMomentumScrolling.value) return - scrollToOffset(contentHeight.value, true) - isMomentumScrolling.value = true - }, [contentHeight.value, isMomentumScrolling, scrollToOffset]) + flatListRef.current?.scrollToEnd({animated: false}) + }, [flatListRef, isMomentumScrolling.value]) // -- Keyboard animation handling const animatedKeyboard = useAnimatedKeyboard() @@ -269,24 +255,15 @@ export function MessagesList() { // This changes the size of the `ListFooterComponent`. Whenever this changes, the content size will change and our // `onContentSizeChange` function will handle scrolling to the appropriate offset. - const animatedFooterStyle = useAnimatedStyle(() => ({ + const animatedStyle = useAnimatedStyle(() => ({ marginBottom: animatedKeyboard.height.value > bottomOffset ? animatedKeyboard.height.value : bottomOffset, })) - // At a minimum we want the bottom to be whatever the height of our insets and bottom bar is. If the keyboard's height - // is greater than that however, we use that value. - const animatedInputStyle = useAnimatedStyle(() => ({ - bottom: - animatedKeyboard.height.value > bottomOffset - ? animatedKeyboard.height.value - : bottomOffset, - })) - return ( - <> + <Animated.View style={[a.flex_1, animatedStyle]}> {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> <List @@ -314,13 +291,13 @@ export function MessagesList() { ListHeaderComponent={ <MaybeLoader isLoading={convo.isFetchingHistory} /> } - ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />} /> </ScrollProvider> + <MessageInput + onSendMessage={onSendMessage} + scrollToEnd={scrollToEndNow} + /> {showNewMessagesPill && <NewMessagesPill />} - <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}> - <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> - </Animated.View> - </> + </Animated.View> ) } diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index 070175d47..3ab00bca1 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -71,23 +71,15 @@ function Inner() { const convoState = useConvo() const {_} = useLingui() - const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) - - // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have - // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause - // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent - // this, we will wait until the first render has completed to remove the loading overlay. - React.useEffect(() => { - if ( - !hasInitiallyRendered && - isConvoActive(convoState) && - !convoState.isFetchingHistory - ) { - setTimeout(() => { - setHasInitiallyRendered(true) - }, 15) - } - }, [convoState, hasInitiallyRendered]) + // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, + // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be + // empty. So, we also check for that possible state as well and render once we can. + const [hasScrolled, setHasScrolled] = React.useState(false) + const readyToShow = + hasScrolled || + (convoState.status === ConvoStatus.Ready && + !convoState.isFetchingHistory && + convoState.items.length === 0) if (convoState.status === ConvoStatus.Error) { return ( @@ -110,11 +102,14 @@ function Inner() { <Header profile={convoState.recipients?.[0]} /> <View style={[a.flex_1]}> {isConvoActive(convoState) ? ( - <MessagesList /> + <MessagesList + hasScrolled={hasScrolled} + setHasScrolled={setHasScrolled} + /> ) : ( <ListMaybePlaceholder isLoading /> )} - {!hasInitiallyRendered && ( + {!readyToShow && ( <View style={[ a.absolute, |