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 {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 {
|
||||||
arr.push(parent)
|
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)
|
arr.push(highlightedPost)
|
||||||
if (!root.post.viewer?.replyDisabled) {
|
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) {
|
// Measure synchronously to avoid a layout jump.
|
||||||
highlightedPostRef.current?.measure(
|
const domNode = highlightedPostRef.current
|
||||||
(_x, _y, _width, _height, _pageX, pageY) => {
|
if (domNode) {
|
||||||
onMeasure(pageY)
|
const pageY = (domNode as any as Element).getBoundingClientRect().top
|
||||||
},
|
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])
|
}, [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,
|
||||||
|
|
Loading…
Reference in New Issue