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 activezio/stable
parent
d4714baf13
commit
f580d4daf0
|
@ -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[] {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
Loading…
Reference in New Issue