[🐴] Finalize web message screen (#3868)

* add `onStartReached` to web list

* fix `rootMargin`

* Add `contain`, handle scroll events

* improve types, fix typo

* simplify

* adjust `scrollToTop` and `scrollToOffset` to support `contain`, add `scrollToEnd`

* rename `handleWindowScroll` to `handleScroll`

* support basic `maintainVisibleContentPosition`

* rename `contain` to `containWeb`

* remove unnecessary `flex: 1`

* add missing props

* add root prop to `Visibility`

* add root prop to `Visibility`

* revert adding `maintainVisibleContentPosition`

* remove unnecessary wrapper

* add style

* oops

* maintain position for web

* always apply `flex: 1` to styles when contained

* add a contained list to storybook

* make `onScroll` a worklet in storybook

* revert test code

* remove unnecessary `flex: 1`
This commit is contained in:
Hailey 2024-05-06 08:48:08 -07:00 committed by GitHub
parent bc07019911
commit ae7626ce6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 35 deletions

View file

@ -94,6 +94,9 @@ export function MessagesList() {
// the bottom. // the bottom.
const isAtBottom = useSharedValue(true) const isAtBottom = useSharedValue(true)
// This will be used on web to assist in determing if we need to maintain the content offset
const isAtTop = useSharedValue(true)
// Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing // 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. // onStartReached to fire.
const contentHeight = useSharedValue(0) const contentHeight = useSharedValue(0)
@ -116,6 +119,15 @@ export function MessagesList() {
// we will not scroll whenever new items get prepended to the top. // we will not scroll whenever new items get prepended to the top.
const onContentSizeChange = useCallback( const onContentSizeChange = useCallback(
(_: number, height: number) => { (_: 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.
if (isWeb && isAtTop.value && hasInitiallyScrolled) {
flatListRef.current?.scrollToOffset({
animated: false,
offset: height - contentHeight.value,
})
}
contentHeight.value = height contentHeight.value = height
// This number _must_ be the height of the MaybeLoader component // This number _must_ be the height of the MaybeLoader component
@ -133,6 +145,7 @@ export function MessagesList() {
contentHeight, contentHeight,
hasInitiallyScrolled, hasInitiallyScrolled,
isAtBottom.value, isAtBottom.value,
isAtTop.value,
isMomentumScrolling, isMomentumScrolling,
], ],
) )
@ -164,6 +177,7 @@ export function MessagesList() {
// Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom // 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 // when a new message is added, hence the 100 pixel offset
isAtBottom.value = e.contentSize.height - 100 < bottomOffset isAtBottom.value = e.contentSize.height - 100 < bottomOffset
isAtTop.value = e.contentOffset.y <= 1
// This number _must_ be the height of the MaybeLoader component. // 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 // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which
@ -172,7 +186,7 @@ export function MessagesList() {
runOnJS(setHasInitiallyScrolled)(true) runOnJS(setHasInitiallyScrolled)(true)
} }
}, },
[contentHeight.value, hasInitiallyScrolled, isAtBottom], [contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop],
) )
const onMomentumEnd = React.useCallback(() => { const onMomentumEnd = React.useCallback(() => {
@ -211,40 +225,37 @@ export function MessagesList() {
keyboardVerticalOffset={isIOS ? topInset : 0} 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 */} {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
{/* @ts-expect-error web only */} <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
<View style={[a.flex_1, isWeb && {'overflow-y': 'scroll'}]}> <List
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} ref={flatListRef}
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> data={chat.status === ConvoStatus.Ready ? chat.items : undefined}
<List renderItem={renderItem}
ref={flatListRef} keyExtractor={keyExtractor}
data={chat.status === ConvoStatus.Ready ? chat.items : undefined} disableVirtualization={true}
renderItem={renderItem} initialNumToRender={isWeb ? 50 : 25}
keyExtractor={keyExtractor} maxToRenderPerBatch={isWeb ? 50 : 25}
disableVirtualization={true} keyboardDismissMode="on-drag"
initialNumToRender={isWeb ? 50 : 25} keyboardShouldPersistTaps="handled"
maxToRenderPerBatch={isWeb ? 50 : 25} maintainVisibleContentPosition={{
keyboardDismissMode="on-drag" minIndexForVisible: 1,
keyboardShouldPersistTaps="handled" }}
maintainVisibleContentPosition={{ containWeb={true}
minIndexForVisible: 1, contentContainerStyle={{paddingHorizontal: 10}}
}} removeClippedSubviews={false}
contentContainerStyle={{paddingHorizontal: 10}} onContentSizeChange={onContentSizeChange}
removeClippedSubviews={false} onStartReached={onStartReached}
onContentSizeChange={onContentSizeChange} onScrollToIndexFailed={onScrollToIndexFailed}
onStartReached={onStartReached} scrollEventThrottle={100}
onScrollToIndexFailed={onScrollToIndexFailed} ListHeaderComponent={
scrollEventThrottle={100} <MaybeLoader
ListHeaderComponent={ isLoading={
<MaybeLoader chat.status === ConvoStatus.Ready && chat.isFetchingHistory
isLoading={ }
chat.status === ConvoStatus.Ready && chat.isFetchingHistory />
} }
/> />
} </ScrollProvider>
/>
</ScrollProvider>
</View>
<MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )

View file

@ -143,6 +143,7 @@ function ListImpl<ItemT>(
scrollToTop() { scrollToTop() {
getScrollableNode()?.scrollTo({top: 0}) getScrollableNode()?.scrollTo({top: 0})
}, },
scrollToOffset({ scrollToOffset({
animated, animated,
offset, offset,