Restore post-thread caching behaviors (react-query refactor) (#2010)

* Rework resolve-did and resolve-uri queries to be smarter about cache reuse

* Precache handle resolutions

* Remove old unused code

* Load placeholder threads from the post-feed and notifications-feed queries

* Remove logs

* Fix bad ref

* Add loading spinners to the cache-loading thread view

* Scroll replies into view when loading threads

* Add caching within a thread

* Fix: dont show bottom border when the child spinner is active
This commit is contained in:
Paul Frazee 2023-11-27 17:41:30 -08:00 committed by GitHub
parent d4714baf13
commit f580d4daf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 369 additions and 111 deletions

View file

@ -39,8 +39,10 @@ import {
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {isNative} from '#/platform/detection'
import {logger} from '#/logger'
// const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} TODO
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
const TOP_COMPONENT = {_reactKey: '__top_component__'}
const PARENT_SPINNER = {_reactKey: '__parent_spinner__'}
@ -72,7 +74,6 @@ export function PostThread({
isError,
error,
refetch,
isRefetching,
data: thread,
} = usePostThreadQuery(uri)
const {data: preferences} = usePreferencesQuery()
@ -110,7 +111,6 @@ export function PostThread({
return (
<PostThreadLoaded
thread={thread}
isRefetching={isRefetching}
threadViewPrefs={preferences.threadViewPrefs}
onRefresh={refetch}
onPressReply={onPressReply}
@ -120,13 +120,11 @@ export function PostThread({
function PostThreadLoaded({
thread,
isRefetching,
threadViewPrefs,
onRefresh,
onPressReply,
}: {
thread: ThreadNode
isRefetching: boolean
threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs']
onRefresh: () => void
onPressReply: () => void
@ -136,29 +134,15 @@ function PostThreadLoaded({
const pal = usePalette('default')
const {isTablet, isDesktop} = useWebMediaQueries()
const ref = useRef<FlatList>(null)
// const hasScrolledIntoView = useRef<boolean>(false) TODO
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)
// TODO
// const posts = React.useMemo(() => {
// if (view.thread) {
// let arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread)))
// if (arr.length > maxVisible) {
// arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
// }
// if (view.isLoadingFromCache) {
// if (view.thread?.postRecord?.reply) {
// arr.unshift(PARENT_SPINNER)
// }
// arr.push(CHILD_SPINNER)
// } else {
// arr.push(BOTTOM_COMPONENT)
// }
// return arr
// }
// return []
// }, [view.isLoadingFromCache, view.thread, maxVisible])
// const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost)
// construct content
const posts = React.useMemo(() => {
let arr = [TOP_COMPONENT].concat(
Array.from(flattenThreadSkeleton(sortThread(thread, threadViewPrefs))),
@ -166,54 +150,61 @@ function PostThreadLoaded({
if (arr.length > maxVisible) {
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
}
arr.push(BOTTOM_COMPONENT)
if (arr.indexOf(CHILD_SPINNER) === -1) {
arr.push(BOTTOM_COMPONENT)
}
return arr
}, [thread, maxVisible, threadViewPrefs])
// TODO
/*const onContentSizeChange = React.useCallback(() => {
/**
* 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(() => {
// only run once
if (hasScrolledIntoView.current) {
if (!needsScrollAdjustment.current) {
return
}
// wait for loading to finish
if (
!view.hasContent ||
(view.isFromCache && view.isLoadingFromCache) ||
view.isLoading
) {
return
if (thread.type === 'post' && !!thread.parent) {
highlightedPostRef.current?.measure(
(_x, _y, _width, _height, _pageX, pageY) => {
ref.current?.scrollToOffset({
animated: false,
offset: pageY - (isDesktop ? 0 : 50),
})
},
)
needsScrollAdjustment.current = false
}
}, [thread, isDesktop])
if (highlightedPostIndex !== -1) {
ref.current?.scrollToIndex({
index: highlightedPostIndex,
animated: false,
viewPosition: 0,
})
hasScrolledIntoView.current = true
const onPTR = React.useCallback(async () => {
setIsPTRing(true)
try {
await onRefresh()
} catch (err) {
logger.error('Failed to refresh posts thread', {error: err})
}
}, [
highlightedPostIndex,
view.hasContent,
view.isFromCache,
view.isLoadingFromCache,
view.isLoading,
])*/
const onScrollToIndexFailed = React.useCallback(
(info: {
index: number
highestMeasuredFrameIndex: number
averageItemLength: number
}) => {
ref.current?.scrollToOffset({
animated: false,
offset: info.averageItemLength * info.index,
})
},
[ref],
)
setIsPTRing(false)
}, [setIsPTRing, onRefresh])
const renderItem = React.useCallback(
({item, index}: {item: YieldedItem; index: number}) => {
@ -290,18 +281,21 @@ function PostThreadLoaded({
? (posts[index - 1] as ThreadPost)
: undefined
return (
<PostThreadItem
post={item.post}
record={item.record}
treeView={threadViewPrefs.lab_treeViewEnabled || false}
depth={item.ctx.depth}
isHighlightedPost={item.ctx.isHighlightedPost}
hasMore={item.ctx.hasMore}
showChildReplyLine={item.ctx.showChildReplyLine}
showParentReplyLine={item.ctx.showParentReplyLine}
hasPrecedingItem={!!prev?.ctx.showChildReplyLine}
onPostReply={onRefresh}
/>
<View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}>
<PostThreadItem
post={item.post}
record={item.record}
treeView={threadViewPrefs.lab_treeViewEnabled || false}
depth={item.ctx.depth}
isHighlightedPost={item.ctx.isHighlightedPost}
hasMore={item.ctx.hasMore}
showChildReplyLine={item.ctx.showChildReplyLine}
showParentReplyLine={item.ctx.showParentReplyLine}
hasPrecedingItem={!!prev?.ctx.showChildReplyLine}
onPostReply={onRefresh}
/>
</View>
)
}
return null
@ -330,25 +324,21 @@ function PostThreadLoaded({
data={posts}
initialNumToRender={posts.length}
maintainVisibleContentPosition={
undefined // TODO
// isNative && view.isFromCache && view.isCachedPostAReply
// ? MAINTAIN_VISIBLE_CONTENT_POSITION
// : undefined
!needsScrollAdjustment.current
? MAINTAIN_VISIBLE_CONTENT_POSITION
: undefined
}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onPTR}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onContentSizeChange={
undefined //TODOisNative && view.isFromCache ? undefined : onContentSizeChange
}
onScrollToIndexFailed={onScrollToIndexFailed}
onContentSizeChange={onContentSizeChange}
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
@ -465,6 +455,8 @@ function* flattenThreadSkeleton(
if (node.type === 'post') {
if (node.parent) {
yield* flattenThreadSkeleton(node.parent)
} else if (node.ctx.isParentLoading) {
yield PARENT_SPINNER
}
yield node
if (node.ctx.isHighlightedPost) {
@ -474,6 +466,8 @@ function* flattenThreadSkeleton(
for (const reply of node.replies) {
yield* flattenThreadSkeleton(reply)
}
} else if (node.ctx.isChildLoading) {
yield CHILD_SPINNER
}
} else if (node.type === 'not-found') {
yield DELETED