Eureka
This commit is contained in:
@@ -7,7 +7,6 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
|||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {isNative} from '#/platform/detection'
|
|
||||||
import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread'
|
import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread'
|
||||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||||
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
|
import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
|
||||||
@@ -79,65 +78,88 @@ export function Inner({uri}: {uri: string | undefined}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* On native, any time we navigate to a new post/reply (even if the data is
|
|
||||||
* cached), we skip rendering parents so that the anchor post is the first
|
|
||||||
* item in the list. That way,
|
|
||||||
* `maintainVisibleContentPosition={{minIndexForVisible: 0}}` will pin the
|
|
||||||
* anchor post to the top of the screen, and on the next render, we'll
|
|
||||||
* include parents.
|
|
||||||
*
|
|
||||||
* On the web this is not necessary because we can synchronously adjust the
|
|
||||||
* scroll in onContentSizeChange instead.
|
|
||||||
*/
|
|
||||||
const [deferParents, setDeferParents] = useState(isNative)
|
|
||||||
const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE)
|
const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE)
|
||||||
const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE)
|
const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE)
|
||||||
const totalParentCount = useRef(0) // recomputed below
|
const totalParentCount = useRef(0) // recomputed below
|
||||||
const totalChildrenCount = useRef(thread.data.items.length) // recomputed below
|
const totalChildrenCount = useRef(thread.data.items.length) // recomputed below
|
||||||
|
|
||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const headerRef = useRef<View | null>(null)
|
const headerRef = useRef<View | null>(null)
|
||||||
const anchorRef = useRef<View | null>(null)
|
const anchorRef = useRef<View | null>(null)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* On a cold load, parents are not prepended until the anchor post has
|
||||||
|
* rendered as the first item in the list. This gives us a consistent
|
||||||
|
* reference point for which to pin the anchor post to the top of the screen.
|
||||||
|
*
|
||||||
|
* We simulate a cold load any time the user changes the view or sort params
|
||||||
|
* so that this handling is consistent.
|
||||||
|
*
|
||||||
|
* On native, `maintainVisibleContentPosition={{minIndexForVisible: 0}}` gives
|
||||||
|
* us this for free, since the anchor post is the first item in the list.
|
||||||
|
*
|
||||||
|
* On web, `onContentSizeChange` is used to get ahead of next paint and handle
|
||||||
|
* this scrolling.
|
||||||
|
*/
|
||||||
|
const [deferParents, setDeferParents] = useState(true)
|
||||||
|
/**
|
||||||
|
* Used to flag whether we should scroll to the anchor post. On a cold load,
|
||||||
|
* this is always true. And when a user changes thread parameters, we also
|
||||||
|
* manually set this to true.
|
||||||
|
*/
|
||||||
|
const shouldScrollToAnchor = useRef(true)
|
||||||
/**
|
/**
|
||||||
* WEB ONLY
|
* WEB ONLY
|
||||||
*
|
*
|
||||||
* Needed after clicking into a post or on a cold load of a thread screen.
|
* Called any time the content size of the list changes, just before paint.
|
||||||
* This handler ensures that once the parents load in, the anchor post is
|
*
|
||||||
* still at the top of the screen.
|
* We want this to fire every time we change params (which will reset
|
||||||
|
* `deferParents` via `onLayout` on the anchor post, due to the key change),
|
||||||
|
* or click into a new post (which will result in a fresh `deferParents`
|
||||||
|
* hook).
|
||||||
|
*
|
||||||
|
* The result being: any intentional change in view by the user will result
|
||||||
|
* in the anchor being pinned as the first item.
|
||||||
*/
|
*/
|
||||||
const hasScrolledToAnchor = useRef(false)
|
|
||||||
const onContentSizeChangeWebOnly = web(() => {
|
const onContentSizeChangeWebOnly = web(() => {
|
||||||
const anchorElement = anchorRef.current as any as Element
|
const anchorElement = anchorRef.current as any as Element
|
||||||
const headerElement = headerRef.current as any as Element
|
const headerElement = headerRef.current as any as Element
|
||||||
|
|
||||||
/*
|
if (anchorElement && headerElement) {
|
||||||
* When this block runs, all posts in place and are about to paint. Thee
|
|
||||||
* `List` is at scroll position 0, so measurements taken from `top`
|
|
||||||
* correspond to the top of the List (screen).
|
|
||||||
*
|
|
||||||
* We measure this synchronously (before paint) and scroll the list to the
|
|
||||||
* top of the `anchorElement`, minus the height of any fixed elements like
|
|
||||||
* the Header.
|
|
||||||
*
|
|
||||||
* We only want this to run once.
|
|
||||||
*/
|
|
||||||
if (anchorElement && headerElement && !hasScrolledToAnchor.current) {
|
|
||||||
const anchorOffsetTop = anchorElement.getBoundingClientRect().top
|
const anchorOffsetTop = anchorElement.getBoundingClientRect().top
|
||||||
const headerHeight = headerElement.getBoundingClientRect().height
|
const headerHeight = headerElement.getBoundingClientRect().height
|
||||||
const scrollPosition = anchorOffsetTop - headerHeight
|
|
||||||
/*
|
if (shouldScrollToAnchor.current) {
|
||||||
* If scroll position is negative, it means the anchor post is above the
|
/**
|
||||||
* top of the screen, meaning the user scrolled the list. In that case,
|
* `deferParents` is `true` on a cold load, and always reset to
|
||||||
* we want to restore the previous scroll position by not scrolling here
|
* `true` when params change (via the key on the anchor post).
|
||||||
* at all.
|
*
|
||||||
*/
|
* On a cold load, on the first pass of this logic, the anchor post is
|
||||||
if (scrollPosition >= headerHeight) {
|
* the first item in the list. Therefore `anchorOffsetTop - headerHeight`
|
||||||
|
* will be 0.
|
||||||
|
*
|
||||||
|
* On a warm load, on the first pass of this logic, the anchor post may
|
||||||
|
* not move (if there are no parents above it), or it may have gone off
|
||||||
|
* the screen above, because of the sudden lack of parents due to
|
||||||
|
* `deferParents === true`. This negative value (minus `headerHeight`)
|
||||||
|
* will result in a _negative_ `offset` value, which will scroll the
|
||||||
|
* anchor post _down_ to the top of the screen.
|
||||||
|
*
|
||||||
|
* Once parents are prepended, this will fire again. Now, the
|
||||||
|
* `anchorOffsetTop` will be positive, which minus the header height,
|
||||||
|
* will give us a _positive_ offset, which will scroll the anchor post
|
||||||
|
* back _up_ to the top of the screen.
|
||||||
|
*/
|
||||||
listRef.current?.scrollToOffset({
|
listRef.current?.scrollToOffset({
|
||||||
animated: false,
|
offset: anchorOffsetTop - headerHeight,
|
||||||
offset: scrollPosition,
|
|
||||||
})
|
})
|
||||||
hasScrolledToAnchor.current = true
|
|
||||||
|
/*
|
||||||
|
* After the second pass, `deferParents` will be `false`, and we need
|
||||||
|
* to ensure this doesn't run again until scroll handling is requested
|
||||||
|
* again via `shouldScrollToAnchor.current === true` and a params
|
||||||
|
* change.
|
||||||
|
*/
|
||||||
|
if (!deferParents) shouldScrollToAnchor.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -235,8 +257,15 @@ export function Inner({uri}: {uri: string | undefined}) {
|
|||||||
} else if (item.depth === 0) {
|
} else if (item.depth === 0) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
ref={item.ui.isAnchor ? anchorRef : undefined}
|
/*
|
||||||
onLayout={deferParents ? () => setDeferParents(false) : undefined}>
|
* IMPORTANT: this is a load-bearing key. We want to force
|
||||||
|
* `onLayout` to fire any time the thread params change so that
|
||||||
|
* `deferParents` is always reset to `false` once the anchor post is
|
||||||
|
* rendered.
|
||||||
|
*/
|
||||||
|
key={item.uri + thread.state.view + thread.state.sort}
|
||||||
|
ref={anchorRef}
|
||||||
|
onLayout={() => setDeferParents(false)}>
|
||||||
<ThreadAnchor
|
<ThreadAnchor
|
||||||
item={item}
|
item={item}
|
||||||
threadgateRecord={thread.data.threadgate?.record ?? undefined}
|
threadgateRecord={thread.data.threadgate?.record ?? undefined}
|
||||||
@@ -320,9 +349,17 @@ export function Inner({uri}: {uri: string | undefined}) {
|
|||||||
<Layout.Header.Slot>
|
<Layout.Header.Slot>
|
||||||
<HeaderDropdown
|
<HeaderDropdown
|
||||||
sort={thread.state.sort}
|
sort={thread.state.sort}
|
||||||
setSort={thread.actions.setSort}
|
setSort={val => {
|
||||||
|
thread.actions.setSort(val)
|
||||||
|
setDeferParents(true)
|
||||||
|
shouldScrollToAnchor.current = true
|
||||||
|
}}
|
||||||
view={thread.state.view}
|
view={thread.state.view}
|
||||||
setView={thread.actions.setView}
|
setView={val => {
|
||||||
|
thread.actions.setView(val)
|
||||||
|
setDeferParents(true)
|
||||||
|
shouldScrollToAnchor.current = true
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Layout.Header.Slot>
|
</Layout.Header.Slot>
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
@@ -345,11 +382,10 @@ export function Inner({uri}: {uri: string | undefined}) {
|
|||||||
onEndReachedThreshold={2}
|
onEndReachedThreshold={2}
|
||||||
onStartReachedThreshold={1}
|
onStartReachedThreshold={1}
|
||||||
/**
|
/**
|
||||||
* @see https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition
|
* NATIVE ONLY
|
||||||
|
* {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
|
||||||
*/
|
*/
|
||||||
maintainVisibleContentPosition={
|
maintainVisibleContentPosition={{minIndexForVisible: 0}}
|
||||||
isNative ? {minIndexForVisible: 0} : undefined
|
|
||||||
}
|
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
// removeClippedSubviews={isAndroid ? false : undefined}
|
// removeClippedSubviews={isAndroid ? false : undefined}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -264,6 +264,12 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
|||||||
setOtherItemsVisible,
|
setOtherItemsVisible,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// items.forEach(slice => {
|
||||||
|
// if (slice.type === 'threadPost' && slice.depth === 0) {
|
||||||
|
// slice.key = slice.uri + view + sort
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
state: {
|
state: {
|
||||||
|
|||||||
Reference in New Issue
Block a user