Adjust post thread scroll for cached posts (#2865)

Co-authored-by: Hailey <me@haileyok.com>
zio/stable
dan 2024-02-14 03:17:22 +00:00 committed by GitHub
parent 836cff306e
commit 7e6b666ee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 81 additions and 60 deletions

View File

@ -47,7 +47,11 @@ import {isAndroid, isNative} from '#/platform/detection'
import {logger} from '#/logger'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1}
const MAINTAIN_VISIBLE_CONTENT_POSITION = {
// We don't insert any elements before the root row while loading.
// So the row we want to use as the scroll anchor is the first row.
minIndexForVisible: 0,
}
const TOP_COMPONENT = {_reactKey: '__top_component__'}
const REPLY_PROMPT = {_reactKey: '__reply__'}
@ -65,6 +69,12 @@ type RowItem =
| typeof LOAD_MORE
| typeof BOTTOM_COMPONENT
type ThreadSkeletonParts = {
parents: YieldedItem[]
highlightedPost: ThreadNode
replies: YieldedItem[]
}
export function PostThread({
uri,
onCanReply,
@ -155,10 +165,6 @@ function PostThreadLoaded({
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null)
const needsScrollAdjustment = useRef<boolean>(
!isNative || // web always uses scroll adjustment
(thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder
)
const [maxVisible, setMaxVisible] = React.useState(100)
const [isPTRing, setIsPTRing] = React.useState(false)
const treeView = React.useMemo(
@ -166,25 +172,55 @@ function PostThreadLoaded({
[threadViewPrefs, thread],
)
// On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
// This ensures that the first render contains no parents--even if they are already available in the cache.
// We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
// On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
const [deferParents, setDeferParents] = React.useState(isNative)
const skeleton = React.useMemo(
() =>
createThreadSkeleton(
sortThread(thread, threadViewPrefs),
hasSession,
treeView,
),
[thread, threadViewPrefs, hasSession, treeView],
)
// construct content
const posts = React.useMemo(() => {
const root = sortThread(thread, threadViewPrefs)
const {parents, highlightedPost, replies} = skeleton
let arr: RowItem[] = []
if (root.type === 'post') {
if (!root.ctx.isParentLoading) {
if (highlightedPost.type === 'post') {
const isRoot =
!highlightedPost.parent && !highlightedPost.ctx.isParentLoading
if (isRoot) {
// No parents to load.
arr.push(TOP_COMPONENT)
for (const parent of flattenThreadParents(root, hasSession)) {
arr.push(parent)
} else {
if (highlightedPost.ctx.isParentLoading || deferParents) {
// We're loading parents of the highlighted post.
// In this case, we don't render anything above the post.
// If you add something here, you'll need to update both
// maintainVisibleContentPosition and onContentSizeChange
// to "hold onto" the correct row instead of the first one.
} else {
// Everything is loaded.
arr.push(TOP_COMPONENT)
for (const parent of parents) {
arr.push(parent)
}
}
}
arr.push(root)
if (!root.post.viewer?.replyDisabled) {
arr.push(highlightedPost)
if (!highlightedPost.post.viewer?.replyDisabled) {
arr.push(REPLY_PROMPT)
}
if (root.ctx.isChildLoading) {
if (highlightedPost.ctx.isChildLoading) {
arr.push(CHILD_SPINNER)
} else {
for (const reply of flattenThreadReplies(root, hasSession, treeView)) {
for (const reply of replies) {
arr.push(reply)
}
arr.push(BOTTOM_COMPONENT)
@ -194,34 +230,16 @@ function PostThreadLoaded({
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
}
return arr
}, [thread, treeView, maxVisible, threadViewPrefs, hasSession])
}, [skeleton, maxVisible, deferParents])
/**
* NOTE
* Scroll positioning
*
* This callback is run if needsScrollAdjustment.current == true, which is...
* - On web: always
* - On native: when the placeholder cache is not being used
*
* It then only runs when viewing a reply, and the goal is to scroll the
* reply into view.
*
* On native, if the placeholder cache is being used then maintainVisibleContentPosition
* is a more effective solution, so we use that. Otherwise, typically we're loading from
* the react-query cache, so we just need to immediately scroll down to the post.
*
* On desktop, maintainVisibleContentPosition isn't supported so we just always use
* this technique.
*
* -prf
*/
const onContentSizeChange = React.useCallback(() => {
// This is only used on the web to keep the post in view when its parents load.
// On native, we rely on `maintainVisibleContentPosition` instead.
const didAdjustScrollWeb = useRef<boolean>(false)
const onContentSizeChangeWeb = React.useCallback(() => {
// only run once
if (!needsScrollAdjustment.current) {
if (didAdjustScrollWeb.current) {
return
}
// wait for loading to finish
if (thread.type === 'post' && !!thread.parent) {
function onMeasure(pageY: number) {
@ -230,21 +248,13 @@ function PostThreadLoaded({
offset: pageY,
})
}
if (isNative) {
highlightedPostRef.current?.measure(
(_x, _y, _width, _height, _pageX, pageY) => {
onMeasure(pageY)
},
)
} else {
// Measure synchronously to avoid a layout jump.
const domNode = highlightedPostRef.current
if (domNode) {
const pageY = (domNode as any as Element).getBoundingClientRect().top
onMeasure(pageY)
}
// Measure synchronously to avoid a layout jump.
const domNode = highlightedPostRef.current
if (domNode) {
const pageY = (domNode as any as Element).getBoundingClientRect().top
onMeasure(pageY)
}
needsScrollAdjustment.current = false
didAdjustScrollWeb.current = true
}
}, [thread])
@ -337,7 +347,8 @@ function PostThreadLoaded({
: undefined
return (
<View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}>
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
onLayout={deferParents ? () => setDeferParents(false) : undefined}>
<PostThreadItem
post={item.post}
record={item.record}
@ -370,6 +381,7 @@ function PostThreadLoaded({
pal.colors.border,
posts,
onRefresh,
deferParents,
treeView,
_,
],
@ -379,17 +391,14 @@ function PostThreadLoaded({
<List
ref={ref}
data={posts}
initialNumToRender={!isNative ? posts.length : undefined}
maintainVisibleContentPosition={
!needsScrollAdjustment.current
? MAINTAIN_VISIBLE_CONTENT_POSITION
: undefined
}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshing={isPTRing}
onRefresh={onPTR}
onContentSizeChange={onContentSizeChange}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
maintainVisibleContentPosition={
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
}
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
@ -509,6 +518,18 @@ function isThreadBlocked(v: unknown): v is ThreadBlocked {
return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
}
function createThreadSkeleton(
node: ThreadNode,
hasSession: boolean,
treeView: boolean,
): ThreadSkeletonParts {
return {
parents: Array.from(flattenThreadParents(node, hasSession)),
highlightedPost: node,
replies: Array.from(flattenThreadReplies(node, hasSession, treeView)),
}
}
function* flattenThreadParents(
node: ThreadNode,
hasSession: boolean,