···4747import {logger} from '#/logger'
4848import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
49495050-const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1}
5050+const MAINTAIN_VISIBLE_CONTENT_POSITION = {
5151+ // We don't insert any elements before the root row while loading.
5252+ // So the row we want to use as the scroll anchor is the first row.
5353+ minIndexForVisible: 0,
5454+}
51555256const TOP_COMPONENT = {_reactKey: '__top_component__'}
5357const REPLY_PROMPT = {_reactKey: '__reply__'}
···6468 | typeof CHILD_SPINNER
6569 | typeof LOAD_MORE
6670 | typeof BOTTOM_COMPONENT
7171+7272+type ThreadSkeletonParts = {
7373+ parents: YieldedItem[]
7474+ highlightedPost: ThreadNode
7575+ replies: YieldedItem[]
7676+}
67776878export function PostThread({
6979 uri,
···155165 const {isMobile, isTabletOrMobile} = useWebMediaQueries()
156166 const ref = useRef<ListMethods>(null)
157167 const highlightedPostRef = useRef<View | null>(null)
158158- const needsScrollAdjustment = useRef<boolean>(
159159- !isNative || // web always uses scroll adjustment
160160- (thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder
161161- )
162168 const [maxVisible, setMaxVisible] = React.useState(100)
163169 const [isPTRing, setIsPTRing] = React.useState(false)
164170 const treeView = React.useMemo(
···166172 [threadViewPrefs, thread],
167173 )
168174175175+ // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
176176+ // This ensures that the first render contains no parents--even if they are already available in the cache.
177177+ // We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
178178+ // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
179179+ const [deferParents, setDeferParents] = React.useState(isNative)
180180+181181+ const skeleton = React.useMemo(
182182+ () =>
183183+ createThreadSkeleton(
184184+ sortThread(thread, threadViewPrefs),
185185+ hasSession,
186186+ treeView,
187187+ ),
188188+ [thread, threadViewPrefs, hasSession, treeView],
189189+ )
190190+169191 // construct content
170192 const posts = React.useMemo(() => {
171171- const root = sortThread(thread, threadViewPrefs)
193193+ const {parents, highlightedPost, replies} = skeleton
172194 let arr: RowItem[] = []
173173- if (root.type === 'post') {
174174- if (!root.ctx.isParentLoading) {
195195+ if (highlightedPost.type === 'post') {
196196+ const isRoot =
197197+ !highlightedPost.parent && !highlightedPost.ctx.isParentLoading
198198+ if (isRoot) {
199199+ // No parents to load.
175200 arr.push(TOP_COMPONENT)
176176- for (const parent of flattenThreadParents(root, hasSession)) {
177177- arr.push(parent)
201201+ } else {
202202+ if (highlightedPost.ctx.isParentLoading || deferParents) {
203203+ // We're loading parents of the highlighted post.
204204+ // In this case, we don't render anything above the post.
205205+ // If you add something here, you'll need to update both
206206+ // maintainVisibleContentPosition and onContentSizeChange
207207+ // to "hold onto" the correct row instead of the first one.
208208+ } else {
209209+ // Everything is loaded.
210210+ arr.push(TOP_COMPONENT)
211211+ for (const parent of parents) {
212212+ arr.push(parent)
213213+ }
178214 }
179215 }
180180- arr.push(root)
181181- if (!root.post.viewer?.replyDisabled) {
216216+ arr.push(highlightedPost)
217217+ if (!highlightedPost.post.viewer?.replyDisabled) {
182218 arr.push(REPLY_PROMPT)
183219 }
184184- if (root.ctx.isChildLoading) {
220220+ if (highlightedPost.ctx.isChildLoading) {
185221 arr.push(CHILD_SPINNER)
186222 } else {
187187- for (const reply of flattenThreadReplies(root, hasSession, treeView)) {
223223+ for (const reply of replies) {
188224 arr.push(reply)
189225 }
190226 arr.push(BOTTOM_COMPONENT)
···194230 arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
195231 }
196232 return arr
197197- }, [thread, treeView, maxVisible, threadViewPrefs, hasSession])
233233+ }, [skeleton, maxVisible, deferParents])
198234199199- /**
200200- * NOTE
201201- * Scroll positioning
202202- *
203203- * This callback is run if needsScrollAdjustment.current == true, which is...
204204- * - On web: always
205205- * - On native: when the placeholder cache is not being used
206206- *
207207- * It then only runs when viewing a reply, and the goal is to scroll the
208208- * reply into view.
209209- *
210210- * On native, if the placeholder cache is being used then maintainVisibleContentPosition
211211- * is a more effective solution, so we use that. Otherwise, typically we're loading from
212212- * the react-query cache, so we just need to immediately scroll down to the post.
213213- *
214214- * On desktop, maintainVisibleContentPosition isn't supported so we just always use
215215- * this technique.
216216- *
217217- * -prf
218218- */
219219- const onContentSizeChange = React.useCallback(() => {
235235+ // This is only used on the web to keep the post in view when its parents load.
236236+ // On native, we rely on `maintainVisibleContentPosition` instead.
237237+ const didAdjustScrollWeb = useRef<boolean>(false)
238238+ const onContentSizeChangeWeb = React.useCallback(() => {
220239 // only run once
221221- if (!needsScrollAdjustment.current) {
240240+ if (didAdjustScrollWeb.current) {
222241 return
223242 }
224224-225243 // wait for loading to finish
226244 if (thread.type === 'post' && !!thread.parent) {
227245 function onMeasure(pageY: number) {
···230248 offset: pageY,
231249 })
232250 }
233233- if (isNative) {
234234- highlightedPostRef.current?.measure(
235235- (_x, _y, _width, _height, _pageX, pageY) => {
236236- onMeasure(pageY)
237237- },
238238- )
239239- } else {
240240- // Measure synchronously to avoid a layout jump.
241241- const domNode = highlightedPostRef.current
242242- if (domNode) {
243243- const pageY = (domNode as any as Element).getBoundingClientRect().top
244244- onMeasure(pageY)
245245- }
251251+ // Measure synchronously to avoid a layout jump.
252252+ const domNode = highlightedPostRef.current
253253+ if (domNode) {
254254+ const pageY = (domNode as any as Element).getBoundingClientRect().top
255255+ onMeasure(pageY)
246256 }
247247- needsScrollAdjustment.current = false
257257+ didAdjustScrollWeb.current = true
248258 }
249259 }, [thread])
250260···337347 : undefined
338348 return (
339349 <View
340340- ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}>
350350+ ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
351351+ onLayout={deferParents ? () => setDeferParents(false) : undefined}>
341352 <PostThreadItem
342353 post={item.post}
343354 record={item.record}
···370381 pal.colors.border,
371382 posts,
372383 onRefresh,
384384+ deferParents,
373385 treeView,
374386 _,
375387 ],
···379391 <List
380392 ref={ref}
381393 data={posts}
382382- initialNumToRender={!isNative ? posts.length : undefined}
383383- maintainVisibleContentPosition={
384384- !needsScrollAdjustment.current
385385- ? MAINTAIN_VISIBLE_CONTENT_POSITION
386386- : undefined
387387- }
388394 keyExtractor={item => item._reactKey}
389395 renderItem={renderItem}
390396 refreshing={isPTRing}
391397 onRefresh={onPTR}
392392- onContentSizeChange={onContentSizeChange}
398398+ onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
399399+ maintainVisibleContentPosition={
400400+ isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
401401+ }
393402 style={s.hContentRegion}
394403 // @ts-ignore our .web version only -prf
395404 desktopFixedHeight
···507516508517function isThreadBlocked(v: unknown): v is ThreadBlocked {
509518 return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
519519+}
520520+521521+function createThreadSkeleton(
522522+ node: ThreadNode,
523523+ hasSession: boolean,
524524+ treeView: boolean,
525525+): ThreadSkeletonParts {
526526+ return {
527527+ parents: Array.from(flattenThreadParents(node, hasSession)),
528528+ highlightedPost: node,
529529+ replies: Array.from(flattenThreadReplies(node, hasSession, treeView)),
530530+ }
510531}
511532512533function* flattenThreadParents(