Clean up scrolling

This commit is contained in:
Eric Bailey
2025-05-21 14:39:40 -05:00
parent b0cc8ee573
commit 6dd71ebae2
+37 -127
View File
@@ -30,7 +30,7 @@ import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposeP
import {PostThreadItem} from '#/view/com/post-thread/PostThreadItem' import {PostThreadItem} from '#/view/com/post-thread/PostThreadItem'
import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShowHiddenReplies' import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShowHiddenReplies'
import {List, type ListMethods} from '#/view/com/util/List' import {List, type ListMethods} from '#/view/com/util/List'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
@@ -121,56 +121,6 @@ function useThreadPreferences() {
) )
} }
export function useControlledListScrolling({
isResetting,
}: {
isResetting: boolean
}) {
const ref = useRef<ListMethods>(null)
const layoutHeaderRef = useRef<View | null>(null)
const anchorPostRef = useRef<View | null>(null)
const didAdjustScrollWeb = useRef<boolean>(false)
if (didAdjustScrollWeb.current && isResetting) {
ref.current?.scrollToOffset({
animated: false,
offset: 0,
})
didAdjustScrollWeb.current = false
}
return {
listRef: ref,
headerRef: layoutHeaderRef,
anchorRef: anchorPostRef,
handleScrollReset() {
// only run once
// TODO need to handle this any time the query changes
if (didAdjustScrollWeb.current) {
return
}
if (!isResetting) {
// Measure synchronously to avoid a layout jump.
const anchorPost = anchorPostRef.current as any as Element
const headerNode = layoutHeaderRef.current as any as Element
if (anchorPost && headerNode) {
// get new scroll position
let pageY = anchorPost.getBoundingClientRect().top
// subtract header height
pageY -= headerNode.getBoundingClientRect().height
// don't scroll past 0
pageY = Math.max(0, pageY)
ref.current?.scrollToOffset({
animated: false,
offset: pageY,
})
didAdjustScrollWeb.current = true
}
}
}
}
}
export function Inner({uri}: {uri: string | undefined}) { export function Inner({uri}: {uri: string | undefined}) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -192,8 +142,7 @@ export function Inner({uri}: {uri: string | undefined}) {
Set<HiddenReplyKind> Set<HiddenReplyKind>
>(new Set()) >(new Set())
const {isFetching, isPlaceholderData, error, data, refetch, insertReplies, ...rest} = const {isFetching, error, data, refetch, insertReplies} = usePostThread({
usePostThread({
uri, uri,
enabled: isThreadPreferencesLoaded, enabled: isThreadPreferencesLoaded,
params: { params: {
@@ -206,10 +155,6 @@ export function Inner({uri}: {uri: string | undefined}) {
}, },
}) })
const ref = useRef<ListMethods>(null)
const layoutHeaderRef = useRef<View | null>(null)
const anchorPostRef = useRef<View | null>(null)
const optimisticOnPostReply = ( const optimisticOnPostReply = (
_: any, _: any,
posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[],
@@ -246,10 +191,41 @@ export function Inner({uri}: {uri: string | undefined}) {
}) })
} }
const listRef = useRef<ListMethods>(null)
const headerRef = useRef<View | null>(null)
const anchorRef = useRef<View | null>(null)
/**
* WEB ONLY
*
* Fires any time the content of the list changes. If user switches back to a
* sort that was rendered previously, this does NOT fire. Therefore, scroll
* is only reset to the anchor on initial render, or fresh data.
*
* When this fires, the `List` is scrolled all the way to the top, so
* measurements taken from `top` correspond to the top of the screen. This
* handler scrolls the `List` to the top of the highlighted post, minus any
* fixed elements.
*/
const onContentSizeChangeWebOnly = web(() => {
const anchorElement = anchorRef.current as any as Element
const headerElement = headerRef.current as any as Element
if (anchorElement && headerElement) {
// distance from top of the list (screen)
const anchorOffsetTop = anchorElement.getBoundingClientRect().top
const headerHeight = headerElement.getBoundingClientRect().height
// don't scroll past 0
const scrollPosition = Math.max(0, anchorOffsetTop - headerHeight)
listRef.current?.scrollToOffset({
animated: false,
offset: scrollPosition,
})
}
})
const renderItem = ({item, index}: {item: Slice; index: number}) => { const renderItem = ({item, index}: {item: Slice; index: number}) => {
if (item.type === 'threadPost') { if (item.type === 'threadPost') {
return ( return (
<View ref={item.ui.isAnchor ? anchorPostRef : undefined}> <View ref={item.ui.isAnchor ? anchorRef : undefined}>
<PostThreadItem <PostThreadItem
post={item.value.post} post={item.value.post}
record={item.value.post.record} record={item.value.post.record}
@@ -327,75 +303,9 @@ export function Inner({uri}: {uri: string | undefined}) {
return null return null
} }
const controlScroll = () => {
// Measure synchronously to avoid a layout jump.
const anchorPost = anchorPostRef.current as any as Element
const headerNode = layoutHeaderRef.current as any as Element
if (anchorPost && headerNode) {
// get new scroll position
let pageY = anchorPost.getBoundingClientRect().top
console.log('anchor top', pageY)
// subtract header height
pageY -= headerNode.getBoundingClientRect().height
console.log('anchor top minus header', pageY)
// don't scroll past 0
pageY = Math.max(0, pageY)
ref.current?.scrollToOffset({
animated: false,
offset: pageY,
})
// didAdjustScrollWeb.current = true
}
}
/*
* This is only used on the web to keep the post in view when its parents
* load. On native, we rely on `maintainVisibleContentPosition` instead.
*
* This runs synchronously before layout, meaning by the time the page
* paints, we've already measured and adjusted the scroll position of the
* list.
*/
const didAdjustScrollWeb = useRef<boolean>(false)
/*
if (didAdjustScrollWeb.current && rest.fetchStatus === 'fetching') {
ref.current?.scrollToOffset({
animated: false,
offset: 0,
})
didAdjustScrollWeb.current = false
}
*/
const contentSizeChanged = useRef<boolean>(false)
const onContentSizeChangeWeb = () => {
if (!contentSizeChanged.current) {
console.log('content size changed')
controlScroll()
contentSizeChanged.current = true
}
return
}
/**
* This works AFTER the data is in the cache, but not on initial loads
*/
const lastDataUpdate = useRef<number>(rest.dataUpdatedAt)
if (rest.dataUpdatedAt && rest.dataUpdatedAt !== lastDataUpdate.current) {
lastDataUpdate.current = rest.dataUpdatedAt
console.log('data updated, reset scroll')
ref.current?.scrollToOffset({
animated: false,
offset: 0,
})
contentSizeChanged.current = false
controlScroll()
}
return ( return (
<> <>
<Layout.Header.Outer headerRef={layoutHeaderRef}> <Layout.Header.Outer headerRef={headerRef}>
<Layout.Header.BackButton /> <Layout.Header.BackButton />
<Layout.Header.Content> <Layout.Header.Content>
<Layout.Header.TitleText> <Layout.Header.TitleText>
@@ -419,11 +329,11 @@ export function Inner({uri}: {uri: string | undefined}) {
// onMomentumEnd={onMomentumEnd} // onMomentumEnd={onMomentumEnd}
> >
<List <List
ref={ref} ref={listRef}
data={data?.items ?? []} data={data?.items ?? []}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} onContentSizeChange={onContentSizeChangeWebOnly}
// onStartReached={onStartReached} // onStartReached={onStartReached}
// onEndReached={onEndReached} // onEndReached={onEndReached}
onEndReachedThreshold={2} onEndReachedThreshold={2}