Adjust post thread scroll for cached posts (#2865)
Co-authored-by: Hailey <me@haileyok.com>zio/stable
parent
836cff306e
commit
7e6b666ee3
|
@ -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)) {
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
|
Loading…
Reference in New Issue