Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Adjust post thread scroll for cached posts (#2865)

Co-authored-by: Hailey <me@haileyok.com>

authored by

dan
Hailey
and committed by
GitHub
7e6b666e 836cff30

+81 -60
+81 -60
src/view/com/post-thread/PostThread.tsx
··· 47 47 import {logger} from '#/logger' 48 48 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 49 49 50 - const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1} 50 + const MAINTAIN_VISIBLE_CONTENT_POSITION = { 51 + // We don't insert any elements before the root row while loading. 52 + // So the row we want to use as the scroll anchor is the first row. 53 + minIndexForVisible: 0, 54 + } 51 55 52 56 const TOP_COMPONENT = {_reactKey: '__top_component__'} 53 57 const REPLY_PROMPT = {_reactKey: '__reply__'} ··· 64 68 | typeof CHILD_SPINNER 65 69 | typeof LOAD_MORE 66 70 | typeof BOTTOM_COMPONENT 71 + 72 + type ThreadSkeletonParts = { 73 + parents: YieldedItem[] 74 + highlightedPost: ThreadNode 75 + replies: YieldedItem[] 76 + } 67 77 68 78 export function PostThread({ 69 79 uri, ··· 155 165 const {isMobile, isTabletOrMobile} = useWebMediaQueries() 156 166 const ref = useRef<ListMethods>(null) 157 167 const highlightedPostRef = useRef<View | null>(null) 158 - const needsScrollAdjustment = useRef<boolean>( 159 - !isNative || // web always uses scroll adjustment 160 - (thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder 161 - ) 162 168 const [maxVisible, setMaxVisible] = React.useState(100) 163 169 const [isPTRing, setIsPTRing] = React.useState(false) 164 170 const treeView = React.useMemo( ··· 166 172 [threadViewPrefs, thread], 167 173 ) 168 174 175 + // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed. 176 + // This ensures that the first render contains no parents--even if they are already available in the cache. 177 + // We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen. 178 + // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead. 179 + const [deferParents, setDeferParents] = React.useState(isNative) 180 + 181 + const skeleton = React.useMemo( 182 + () => 183 + createThreadSkeleton( 184 + sortThread(thread, threadViewPrefs), 185 + hasSession, 186 + treeView, 187 + ), 188 + [thread, threadViewPrefs, hasSession, treeView], 189 + ) 190 + 169 191 // construct content 170 192 const posts = React.useMemo(() => { 171 - const root = sortThread(thread, threadViewPrefs) 193 + const {parents, highlightedPost, replies} = skeleton 172 194 let arr: RowItem[] = [] 173 - if (root.type === 'post') { 174 - if (!root.ctx.isParentLoading) { 195 + if (highlightedPost.type === 'post') { 196 + const isRoot = 197 + !highlightedPost.parent && !highlightedPost.ctx.isParentLoading 198 + if (isRoot) { 199 + // No parents to load. 175 200 arr.push(TOP_COMPONENT) 176 - for (const parent of flattenThreadParents(root, hasSession)) { 177 - arr.push(parent) 201 + } else { 202 + if (highlightedPost.ctx.isParentLoading || deferParents) { 203 + // We're loading parents of the highlighted post. 204 + // In this case, we don't render anything above the post. 205 + // If you add something here, you'll need to update both 206 + // maintainVisibleContentPosition and onContentSizeChange 207 + // to "hold onto" the correct row instead of the first one. 208 + } else { 209 + // Everything is loaded. 210 + arr.push(TOP_COMPONENT) 211 + for (const parent of parents) { 212 + arr.push(parent) 213 + } 178 214 } 179 215 } 180 - arr.push(root) 181 - if (!root.post.viewer?.replyDisabled) { 216 + arr.push(highlightedPost) 217 + if (!highlightedPost.post.viewer?.replyDisabled) { 182 218 arr.push(REPLY_PROMPT) 183 219 } 184 - if (root.ctx.isChildLoading) { 220 + if (highlightedPost.ctx.isChildLoading) { 185 221 arr.push(CHILD_SPINNER) 186 222 } else { 187 - for (const reply of flattenThreadReplies(root, hasSession, treeView)) { 223 + for (const reply of replies) { 188 224 arr.push(reply) 189 225 } 190 226 arr.push(BOTTOM_COMPONENT) ··· 194 230 arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) 195 231 } 196 232 return arr 197 - }, [thread, treeView, maxVisible, threadViewPrefs, hasSession]) 233 + }, [skeleton, maxVisible, deferParents]) 198 234 199 - /** 200 - * NOTE 201 - * Scroll positioning 202 - * 203 - * This callback is run if needsScrollAdjustment.current == true, which is... 204 - * - On web: always 205 - * - On native: when the placeholder cache is not being used 206 - * 207 - * It then only runs when viewing a reply, and the goal is to scroll the 208 - * reply into view. 209 - * 210 - * On native, if the placeholder cache is being used then maintainVisibleContentPosition 211 - * is a more effective solution, so we use that. Otherwise, typically we're loading from 212 - * the react-query cache, so we just need to immediately scroll down to the post. 213 - * 214 - * On desktop, maintainVisibleContentPosition isn't supported so we just always use 215 - * this technique. 216 - * 217 - * -prf 218 - */ 219 - const onContentSizeChange = React.useCallback(() => { 235 + // This is only used on the web to keep the post in view when its parents load. 236 + // On native, we rely on `maintainVisibleContentPosition` instead. 237 + const didAdjustScrollWeb = useRef<boolean>(false) 238 + const onContentSizeChangeWeb = React.useCallback(() => { 220 239 // only run once 221 - if (!needsScrollAdjustment.current) { 240 + if (didAdjustScrollWeb.current) { 222 241 return 223 242 } 224 - 225 243 // wait for loading to finish 226 244 if (thread.type === 'post' && !!thread.parent) { 227 245 function onMeasure(pageY: number) { ··· 230 248 offset: pageY, 231 249 }) 232 250 } 233 - if (isNative) { 234 - highlightedPostRef.current?.measure( 235 - (_x, _y, _width, _height, _pageX, pageY) => { 236 - onMeasure(pageY) 237 - }, 238 - ) 239 - } else { 240 - // Measure synchronously to avoid a layout jump. 241 - const domNode = highlightedPostRef.current 242 - if (domNode) { 243 - const pageY = (domNode as any as Element).getBoundingClientRect().top 244 - onMeasure(pageY) 245 - } 251 + // Measure synchronously to avoid a layout jump. 252 + const domNode = highlightedPostRef.current 253 + if (domNode) { 254 + const pageY = (domNode as any as Element).getBoundingClientRect().top 255 + onMeasure(pageY) 246 256 } 247 - needsScrollAdjustment.current = false 257 + didAdjustScrollWeb.current = true 248 258 } 249 259 }, [thread]) 250 260 ··· 337 347 : undefined 338 348 return ( 339 349 <View 340 - ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}> 350 + ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} 351 + onLayout={deferParents ? () => setDeferParents(false) : undefined}> 341 352 <PostThreadItem 342 353 post={item.post} 343 354 record={item.record} ··· 370 381 pal.colors.border, 371 382 posts, 372 383 onRefresh, 384 + deferParents, 373 385 treeView, 374 386 _, 375 387 ], ··· 379 391 <List 380 392 ref={ref} 381 393 data={posts} 382 - initialNumToRender={!isNative ? posts.length : undefined} 383 - maintainVisibleContentPosition={ 384 - !needsScrollAdjustment.current 385 - ? MAINTAIN_VISIBLE_CONTENT_POSITION 386 - : undefined 387 - } 388 394 keyExtractor={item => item._reactKey} 389 395 renderItem={renderItem} 390 396 refreshing={isPTRing} 391 397 onRefresh={onPTR} 392 - onContentSizeChange={onContentSizeChange} 398 + onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} 399 + maintainVisibleContentPosition={ 400 + isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined 401 + } 393 402 style={s.hContentRegion} 394 403 // @ts-ignore our .web version only -prf 395 404 desktopFixedHeight ··· 507 516 508 517 function isThreadBlocked(v: unknown): v is ThreadBlocked { 509 518 return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked' 519 + } 520 + 521 + function createThreadSkeleton( 522 + node: ThreadNode, 523 + hasSession: boolean, 524 + treeView: boolean, 525 + ): ThreadSkeletonParts { 526 + return { 527 + parents: Array.from(flattenThreadParents(node, hasSession)), 528 + highlightedPost: node, 529 + replies: Array.from(flattenThreadReplies(node, hasSession, treeView)), 530 + } 510 531 } 511 532 512 533 function* flattenThreadParents(