[🐴] 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:
parent
bc07019911
commit
ae7626ce6e
2 changed files with 47 additions and 35 deletions
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -143,6 +143,7 @@ function ListImpl<ItemT>(
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
getScrollableNode()?.scrollTo({top: 0})
|
getScrollableNode()?.scrollTo({top: 0})
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToOffset({
|
scrollToOffset({
|
||||||
animated,
|
animated,
|
||||||
offset,
|
offset,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue