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 {logger} from '#/logger'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 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 TOP_COMPONENT = {_reactKey: '__top_component__'}
const REPLY_PROMPT = {_reactKey: '__reply__'} const REPLY_PROMPT = {_reactKey: '__reply__'}
@ -65,6 +69,12 @@ type RowItem =
| typeof LOAD_MORE | typeof LOAD_MORE
| typeof BOTTOM_COMPONENT | typeof BOTTOM_COMPONENT
type ThreadSkeletonParts = {
parents: YieldedItem[]
highlightedPost: ThreadNode
replies: YieldedItem[]
}
export function PostThread({ export function PostThread({
uri, uri,
onCanReply, onCanReply,
@ -155,10 +165,6 @@ function PostThreadLoaded({
const {isMobile, isTabletOrMobile} = useWebMediaQueries() const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const ref = useRef<ListMethods>(null) const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(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 [maxVisible, setMaxVisible] = React.useState(100)
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const treeView = React.useMemo( const treeView = React.useMemo(
@ -166,25 +172,55 @@ function PostThreadLoaded({
[threadViewPrefs, thread], [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 // construct content
const posts = React.useMemo(() => { const posts = React.useMemo(() => {
const root = sortThread(thread, threadViewPrefs) const {parents, highlightedPost, replies} = skeleton
let arr: RowItem[] = [] let arr: RowItem[] = []
if (root.type === 'post') { if (highlightedPost.type === 'post') {
if (!root.ctx.isParentLoading) { const isRoot =
!highlightedPost.parent && !highlightedPost.ctx.isParentLoading
if (isRoot) {
// No parents to load.
arr.push(TOP_COMPONENT) arr.push(TOP_COMPONENT)
for (const parent of flattenThreadParents(root, hasSession)) { } 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(parent)
} }
} }
arr.push(root) }
if (!root.post.viewer?.replyDisabled) { arr.push(highlightedPost)
if (!highlightedPost.post.viewer?.replyDisabled) {
arr.push(REPLY_PROMPT) arr.push(REPLY_PROMPT)
} }
if (root.ctx.isChildLoading) { if (highlightedPost.ctx.isChildLoading) {
arr.push(CHILD_SPINNER) arr.push(CHILD_SPINNER)
} else { } else {
for (const reply of flattenThreadReplies(root, hasSession, treeView)) { for (const reply of replies) {
arr.push(reply) arr.push(reply)
} }
arr.push(BOTTOM_COMPONENT) arr.push(BOTTOM_COMPONENT)
@ -194,34 +230,16 @@ function PostThreadLoaded({
arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
} }
return arr return arr
}, [thread, treeView, maxVisible, threadViewPrefs, hasSession]) }, [skeleton, maxVisible, deferParents])
/** // This is only used on the web to keep the post in view when its parents load.
* NOTE // On native, we rely on `maintainVisibleContentPosition` instead.
* Scroll positioning const didAdjustScrollWeb = useRef<boolean>(false)
* const onContentSizeChangeWeb = React.useCallback(() => {
* 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(() => {
// only run once // only run once
if (!needsScrollAdjustment.current) { if (didAdjustScrollWeb.current) {
return return
} }
// wait for loading to finish // wait for loading to finish
if (thread.type === 'post' && !!thread.parent) { if (thread.type === 'post' && !!thread.parent) {
function onMeasure(pageY: number) { function onMeasure(pageY: number) {
@ -230,21 +248,13 @@ function PostThreadLoaded({
offset: pageY, offset: pageY,
}) })
} }
if (isNative) {
highlightedPostRef.current?.measure(
(_x, _y, _width, _height, _pageX, pageY) => {
onMeasure(pageY)
},
)
} else {
// Measure synchronously to avoid a layout jump. // Measure synchronously to avoid a layout jump.
const domNode = highlightedPostRef.current const domNode = highlightedPostRef.current
if (domNode) { if (domNode) {
const pageY = (domNode as any as Element).getBoundingClientRect().top const pageY = (domNode as any as Element).getBoundingClientRect().top
onMeasure(pageY) onMeasure(pageY)
} }
} didAdjustScrollWeb.current = true
needsScrollAdjustment.current = false
} }
}, [thread]) }, [thread])
@ -337,7 +347,8 @@ function PostThreadLoaded({
: undefined : undefined
return ( return (
<View <View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}> ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
onLayout={deferParents ? () => setDeferParents(false) : undefined}>
<PostThreadItem <PostThreadItem
post={item.post} post={item.post}
record={item.record} record={item.record}
@ -370,6 +381,7 @@ function PostThreadLoaded({
pal.colors.border, pal.colors.border,
posts, posts,
onRefresh, onRefresh,
deferParents,
treeView, treeView,
_, _,
], ],
@ -379,17 +391,14 @@ function PostThreadLoaded({
<List <List
ref={ref} ref={ref}
data={posts} data={posts}
initialNumToRender={!isNative ? posts.length : undefined}
maintainVisibleContentPosition={
!needsScrollAdjustment.current
? MAINTAIN_VISIBLE_CONTENT_POSITION
: undefined
}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onPTR} onRefresh={onPTR}
onContentSizeChange={onContentSizeChange} onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
maintainVisibleContentPosition={
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
}
style={s.hContentRegion} style={s.hContentRegion}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
@ -509,6 +518,18 @@ function isThreadBlocked(v: unknown): v is ThreadBlocked {
return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked' 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( function* flattenThreadParents(
node: ThreadNode, node: ThreadNode,
hasSession: boolean, hasSession: boolean,