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
zio/stable
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

@ -7,11 +7,18 @@ import {
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryKey,
useQueryClient,
QueryClient,
} from '@tanstack/react-query'
import {getAgent} from '../../session' import {getAgent} from '../../session'
import {useModerationOpts} from '../preferences' import {useModerationOpts} from '../preferences'
import {shouldFilterNotif} from './util' import {shouldFilterNotif} from './util'
import {useMutedThreads} from '#/state/muted-threads' import {useMutedThreads} from '#/state/muted-threads'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
const GROUPABLE_REASONS = ['like', 'repost', 'follow'] const GROUPABLE_REASONS = ['like', 'repost', 'follow']
const PAGE_SIZE = 30 const PAGE_SIZE = 30
@ -48,6 +55,7 @@ export interface FeedPage {
} }
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads() const threadMutes = useMutedThreads()
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
@ -80,6 +88,9 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
for (const notif of notifsGrouped) { for (const notif of notifsGrouped) {
if (notif.subjectUri) { if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri) notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
}
} }
} }
@ -99,6 +110,32 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
}) })
} }
/**
* This helper is used by the post-thread placeholder function to
* find a post in the query-data cache
*/
export function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): AppBskyFeedDefs.PostView | undefined {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: ['notification-feed'],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const item of page.items) {
if (item.subject?.uri === uri) {
return item.subject
}
}
}
}
return undefined
}
function groupNotifications( function groupNotifications(
notifs: AppBskyNotificationListNotifications.Notification[], notifs: AppBskyNotificationListNotifications.Notification[],
): FeedNotification[] { ): FeedNotification[] {

View File

@ -1,6 +1,12 @@
import {useCallback, useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api' import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' import {
useInfiniteQuery,
InfiniteData,
QueryKey,
QueryClient,
useQueryClient,
} from '@tanstack/react-query'
import {getAgent} from '../session' import {getAgent} from '../session'
import {useFeedTuners} from '../preferences/feed-tuners' import {useFeedTuners} from '../preferences/feed-tuners'
import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip' import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip'
@ -14,6 +20,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
type ActorDid = string type ActorDid = string
type AuthorFilter = type AuthorFilter =
@ -66,6 +73,7 @@ export function usePostFeedQuery(
params?: FeedParams, params?: FeedParams,
opts?: {enabled?: boolean}, opts?: {enabled?: boolean},
) { ) {
const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc) const feedTuners = useFeedTuners(feedDesc)
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@ -141,6 +149,7 @@ export function usePostFeedQuery(
tuner.reset() tuner.reset()
} }
const res = await api.fetch({cursor: pageParam, limit: 30}) const res = await api.fetch({cursor: pageParam, limit: 30})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
const slices = tuner.tune(res.feed, feedTuners) const slices = tuner.tune(res.feed, feedTuners)
return { return {
cursor: res.cursor, cursor: res.cursor,
@ -152,7 +161,6 @@ export function usePostFeedQuery(
slice.items.every( slice.items.every(
item => item.post.author.did === slice.items[0].post.author.did, item => item.post.author.did === slice.items[0].post.author.did,
), ),
source: undefined, // TODO
items: slice.items items: slice.items
.map((item, i) => { .map((item, i) => {
if ( if (
@ -180,3 +188,31 @@ export function usePostFeedQuery(
return {...out, pollLatest} return {...out, pollLatest}
} }
/**
* This helper is used by the post-thread placeholder function to
* find a post in the query-data cache
*/
export function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): FeedPostSliceItem | undefined {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: ['post-feed'],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const slice of page.slices) {
for (const item of slice.items) {
if (item.uri === uri) {
return item
}
}
}
}
}
return undefined
}

View File

@ -3,11 +3,17 @@ import {
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedGetPostThread, AppBskyFeedGetPostThread,
} from '@atproto/api' } from '@atproto/api'
import {useQuery} from '@tanstack/react-query' import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {
findPostInQueryData as findPostInFeedQueryData,
FeedPostSliceItem,
} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
export const RQKEY = (uri: string) => ['post-thread', uri] export const RQKEY = (uri: string) => ['post-thread', uri]
type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
@ -18,6 +24,8 @@ export interface ThreadCtx {
hasMore?: boolean hasMore?: boolean
showChildReplyLine?: boolean showChildReplyLine?: boolean
showParentReplyLine?: boolean showParentReplyLine?: boolean
isParentLoading?: boolean
isChildLoading?: boolean
} }
export type ThreadPost = { export type ThreadPost = {
@ -58,17 +66,44 @@ export type ThreadNode =
| ThreadUnknown | ThreadUnknown
export function usePostThreadQuery(uri: string | undefined) { export function usePostThreadQuery(uri: string | undefined) {
const queryClient = useQueryClient()
return useQuery<ThreadNode, Error>({ return useQuery<ThreadNode, Error>({
staleTime: STALE.MINUTES.ONE, staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''), queryKey: RQKEY(uri || ''),
async queryFn() { async queryFn() {
const res = await getAgent().getPostThread({uri: uri!}) const res = await getAgent().getPostThread({uri: uri!})
if (res.success) { if (res.success) {
return responseToThreadNodes(res.data.thread) const nodes = responseToThreadNodes(res.data.thread)
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
return nodes
} }
return {type: 'unknown', uri: uri!} return {type: 'unknown', uri: uri!}
}, },
enabled: !!uri, enabled: !!uri,
placeholderData: () => {
if (!uri) {
return undefined
}
{
const item = findPostInQueryData(queryClient, uri)
if (item) {
return threadNodeToPlaceholderThread(item)
}
}
{
const item = findPostInFeedQueryData(queryClient, uri)
if (item) {
return feedItemToPlaceholderThread(item)
}
}
{
const item = findPostInNotifsQueryData(queryClient, uri)
if (item) {
return postViewToPlaceholderThread(item)
}
}
return undefined
},
}) })
} }
@ -178,3 +213,110 @@ function responseToThreadNodes(
return {type: 'unknown', uri: ''} return {type: 'unknown', uri: ''}
} }
} }
function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): ThreadNode | undefined {
const queryDatas = queryClient.getQueriesData<ThreadNode>({
queryKey: ['post-thread'],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
continue
}
for (const item of traverseThread(queryData)) {
if (item.uri === uri) {
return item
}
}
}
return undefined
}
function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {
if (node.type === 'post') {
if (node.parent) {
yield* traverseThread(node.parent)
}
yield node
if (node.replies?.length) {
for (const reply of node.replies) {
yield* traverseThread(reply)
}
}
}
}
function threadNodeToPlaceholderThread(
node: ThreadNode,
): ThreadNode | undefined {
if (node.type !== 'post') {
return undefined
}
return {
type: node.type,
_reactKey: node._reactKey,
uri: node.uri,
post: node.post,
record: node.record,
parent: undefined,
replies: undefined,
viewer: node.viewer,
ctx: {
depth: 0,
isHighlightedPost: true,
hasMore: false,
showChildReplyLine: false,
showParentReplyLine: false,
isParentLoading: !!node.record.reply,
isChildLoading: !!node.post.replyCount,
},
}
}
function feedItemToPlaceholderThread(item: FeedPostSliceItem): ThreadNode {
return {
type: 'post',
_reactKey: item.post.uri,
uri: item.post.uri,
post: item.post,
record: item.record,
parent: undefined,
replies: undefined,
viewer: item.post.viewer,
ctx: {
depth: 0,
isHighlightedPost: true,
hasMore: false,
showChildReplyLine: false,
showParentReplyLine: false,
isParentLoading: !!item.record.reply,
isChildLoading: !!item.post.replyCount,
},
}
}
function postViewToPlaceholderThread(
post: AppBskyFeedDefs.PostView,
): ThreadNode {
return {
type: 'post',
_reactKey: post.uri,
uri: post.uri,
post: post,
record: post.record as AppBskyFeedPost.Record, // validate in notifs
parent: undefined,
replies: undefined,
viewer: post.viewer,
ctx: {
depth: 0,
isHighlightedPost: true,
hasMore: false,
showChildReplyLine: false,
showParentReplyLine: false,
isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply,
isChildLoading: !!post.replyCount,
},
}
}

View File

@ -1,27 +1,76 @@
import {useQuery} from '@tanstack/react-query' import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
import {AtUri} from '@atproto/api' import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {ThreadNode} from './post-thread'
export const RQKEY = (uri: string) => ['resolved-uri', uri] export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
export function useResolveUriQuery(uri: string | undefined) { type UriUseQueryResult = UseQueryResult<{did: string; uri: string}, Error>
return useQuery<{uri: string; did: string}, Error>({ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
staleTime: STALE.INFINITY, const urip = new AtUri(uri || '')
queryKey: RQKEY(uri || ''), const res = useResolveDidQuery(urip.host)
async queryFn() { if (res.data) {
const urip = new AtUri(uri || '') urip.host = res.data
if (!urip.host.startsWith('did:')) { return {
const res = await getAgent().resolveHandle({handle: urip.host}) ...res,
urip.host = res.data.did data: {did: urip.host, uri: urip.toString()},
} } as UriUseQueryResult
return {did: urip.host, uri: urip.toString()} }
}, return res as UriUseQueryResult
enabled: !!uri,
})
} }
export function useResolveDidQuery(didOrHandle: string | undefined) { export function useResolveDidQuery(didOrHandle: string | undefined) {
return useResolveUriQuery(didOrHandle ? `at://${didOrHandle}/` : undefined) return useQuery<string, Error>({
staleTime: STALE.INFINITY,
queryKey: RQKEY(didOrHandle || ''),
async queryFn() {
if (!didOrHandle) {
return ''
}
if (!didOrHandle.startsWith('did:')) {
const res = await getAgent().resolveHandle({handle: didOrHandle})
didOrHandle = res.data.did
}
return didOrHandle
},
enabled: !!didOrHandle,
})
}
export function precacheProfile(
queryClient: QueryClient,
profile:
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
}
export function precacheFeedPosts(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
precacheProfile(queryClient, post.post.author)
}
}
export function precacheThreadPosts(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPosts(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPosts(queryClient, reply)
}
}
}
} }

View File

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

View File

@ -28,7 +28,7 @@ export function ProfileFollowers({name}: {name: string}) {
isError, isError,
error, error,
refetch, refetch,
} = useProfileFollowersQuery(resolvedDid?.did) } = useProfileFollowersQuery(resolvedDid)
const followers = React.useMemo(() => { const followers = React.useMemo(() => {
if (data?.pages) { if (data?.pages) {

View File

@ -28,7 +28,7 @@ export function ProfileFollows({name}: {name: string}) {
isError, isError,
error, error,
refetch, refetch,
} = useProfileFollowsQuery(resolvedDid?.did) } = useProfileFollowsQuery(resolvedDid)
const follows = React.useMemo(() => { const follows = React.useMemo(() => {
if (data?.pages) { if (data?.pages) {

View File

@ -58,7 +58,7 @@ export function ProfileScreen({route}: Props) {
refetch: refetchProfile, refetch: refetchProfile,
isFetching: isFetchingProfile, isFetching: isFetchingProfile,
} = useProfileQuery({ } = useProfileQuery({
did: resolvedDid?.did, did: resolvedDid,
}) })
const onPressTryAgain = React.useCallback(() => { const onPressTryAgain = React.useCallback(() => {