diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx
index 01194ef651..5b9fc262dc 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists'
+import {useGetPost} from '#/state/queries/post'
import {
createPostgateQueryKey,
getPostgateRecord,
@@ -25,12 +26,15 @@ import {
} from '#/state/queries/postgate/util'
import {
createThreadgateViewQueryKey,
- getThreadgateView,
type ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation,
useThreadgateViewQuery,
} from '#/state/queries/threadgate'
+import {
+ PostThreadContextProvider,
+ usePostThreadContext,
+} from '#/state/queries/usePostThread'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
export function PostInteractionSettingsDialog(
props: PostInteractionSettingsDialogProps,
) {
+ const postThreadContext = usePostThreadContext()
return (
-
+
+
+
)
}
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
}) {
const queryClient = useQueryClient()
const agent = useAgent()
+ const getPost = useGetPost()
return React.useCallback(async () => {
try {
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
}),
queryClient.prefetchQuery({
queryKey: createThreadgateViewQueryKey(rootPostUri),
- queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
+ queryFn: async () => {
+ const post = await getPost({uri: rootPostUri})
+ return post.threadgate ?? null
+ },
staleTime: STALE.SECONDS.THIRTY,
}),
])
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
safeMessage: e.message,
})
}
- }, [queryClient, agent, postUri, rootPostUri])
+ }, [queryClient, agent, postUri, rootPostUri, getPost])
}
diff --git a/src/screens/Post/PostLikedBy.tsx b/src/screens/Post/PostLikedBy.tsx
index e1a482452f..b8a2302468 100644
--- a/src/screens/Post/PostLikedBy.tsx
+++ b/src/screens/Post/PostLikedBy.tsx
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {usePostThreadQuery} from '#/state/queries/post-thread'
+import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
- const {data: post} = usePostThreadQuery(uri)
+ const {data: post} = usePostQuery(uri)
let likeCount
- if (post?.thread.type === 'post') {
- likeCount = post.thread.post.likeCount
+ if (post) {
+ likeCount = post.likeCount
}
useFocusEffect(
diff --git a/src/screens/Post/PostQuotes.tsx b/src/screens/Post/PostQuotes.tsx
index 3fa5d80275..33ebe94f33 100644
--- a/src/screens/Post/PostQuotes.tsx
+++ b/src/screens/Post/PostQuotes.tsx
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {usePostThreadQuery} from '#/state/queries/post-thread'
+import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
- const {data: post} = usePostThreadQuery(uri)
+ const {data: post} = usePostQuery(uri)
let quoteCount
- if (post?.thread.type === 'post') {
- quoteCount = post.thread.post.quoteCount
+ if (post) {
+ quoteCount = post.quoteCount
}
useFocusEffect(
diff --git a/src/screens/Post/PostRepostedBy.tsx b/src/screens/Post/PostRepostedBy.tsx
index d79321d0a9..37f31ce22b 100644
--- a/src/screens/Post/PostRepostedBy.tsx
+++ b/src/screens/Post/PostRepostedBy.tsx
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {usePostThreadQuery} from '#/state/queries/post-thread'
+import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode()
- const {data: post} = usePostThreadQuery(uri)
+ const {data: post} = usePostQuery(uri)
let quoteCount
- if (post?.thread.type === 'post') {
- quoteCount = post.thread.post.repostCount
+ if (post) {
+ quoteCount = post.repostCount
}
useFocusEffect(
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index 7918f4474c..64a6f0f295 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -7,7 +7,11 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useFeedFeedback} from '#/state/feed-feedback'
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
-import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread'
+import {
+ PostThreadContextProvider,
+ type ThreadItem,
+ usePostThread,
+} from '#/state/queries/usePostThread'
import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
import {useShellLayout} from '#/state/shell/shell-layout'
@@ -492,7 +496,7 @@ export function PostThread({uri}: {uri: string}) {
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
return (
- <>
+
@@ -575,7 +579,7 @@ export function PostThread({uri}: {uri: string}) {
{!gtMobile && canReply && hasSession && (
)}
- >
+
)
}
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index 312bc6df8d..05a702cd5f 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -12,7 +12,6 @@ import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} f
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
-import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
import {castAsShadow, type Shadow} from './types'
@@ -176,11 +175,6 @@ function* findPostsInCache(
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
yield post
}
- for (let node of findAllPostsInThreadQueryData(queryClient, uri)) {
- if (node.type === 'post') {
- yield node.post
- }
- }
for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) {
yield post
}
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 1489e65fd9..168661e0d1 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -16,7 +16,6 @@ import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/s
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
-import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '#/state/queries/post-thread'
import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
@@ -173,7 +172,6 @@ function* findProfilesInCache(
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
yield* findAllProfilesInListConvosQueryData(queryClient, did)
yield* findAllProfilesInFeedsQueryData(queryClient, did)
- yield* findAllProfilesInPostThreadQueryData(queryClient, did)
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
deleted file mode 100644
index 551fedc8b4..0000000000
--- a/src/state/queries/post-thread.ts
+++ /dev/null
@@ -1,631 +0,0 @@
-import {
- type AppBskyActorDefs,
- type AppBskyEmbedRecord,
- AppBskyFeedDefs,
- type AppBskyFeedGetPostThread,
- AppBskyFeedPost,
- AtUri,
- moderatePost,
- type ModerationDecision,
- type ModerationOpts,
-} from '@atproto/api'
-import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
-
-import {
- findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData,
- findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData,
-} from '#/state/queries/explore-feed-previews'
-import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
-import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
-import {
- findAllPostsInQueryData as findAllPostsInSearchQueryData,
- findAllProfilesInQueryData as findAllProfilesInSearchQueryData,
-} from '#/state/queries/search-posts'
-import {useAgent} from '#/state/session'
-import * as bsky from '#/types/bsky'
-import {
- findAllPostsInQueryData as findAllPostsInNotifsQueryData,
- findAllProfilesInQueryData as findAllProfilesInNotifsQueryData,
-} from './notifications/feed'
-import {
- findAllPostsInQueryData as findAllPostsInFeedQueryData,
- findAllProfilesInQueryData as findAllProfilesInFeedQueryData,
-} from './post-feed'
-import {
- didOrHandleUriMatches,
- embedViewRecordToPostView,
- getEmbeddedPost,
-} from './util'
-
-const REPLY_TREE_DEPTH = 10
-export const RQKEY_ROOT = 'post-thread'
-export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
-type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
-
-export interface ThreadCtx {
- depth: number
- isHighlightedPost?: boolean
- hasMore?: boolean
- isParentLoading?: boolean
- isChildLoading?: boolean
- isSelfThread?: boolean
- hasMoreSelfThread?: boolean
-}
-
-export type ThreadPost = {
- type: 'post'
- _reactKey: string
- uri: string
- post: AppBskyFeedDefs.PostView
- record: AppBskyFeedPost.Record
- parent: ThreadNode | undefined
- replies: ThreadNode[] | undefined
- hasOPLike: boolean | undefined
- ctx: ThreadCtx
-}
-
-export type ThreadNotFound = {
- type: 'not-found'
- _reactKey: string
- uri: string
- ctx: ThreadCtx
-}
-
-export type ThreadBlocked = {
- type: 'blocked'
- _reactKey: string
- uri: string
- ctx: ThreadCtx
-}
-
-export type ThreadUnknown = {
- type: 'unknown'
- uri: string
-}
-
-export type ThreadNode =
- | ThreadPost
- | ThreadNotFound
- | ThreadBlocked
- | ThreadUnknown
-
-export type ThreadModerationCache = WeakMap
-
-export type PostThreadQueryData = {
- thread: ThreadNode
- threadgate?: AppBskyFeedDefs.ThreadgateView
-}
-
-export function usePostThreadQuery(uri: string | undefined) {
- const queryClient = useQueryClient()
- const agent = useAgent()
- return useQuery({
- gcTime: 0,
- queryKey: RQKEY(uri || ''),
- async queryFn() {
- const res = await agent.getPostThread({
- uri: uri!,
- depth: REPLY_TREE_DEPTH,
- })
- if (res.success) {
- const thread = responseToThreadNodes(res.data.thread)
- annotateSelfThread(thread)
- return {
- thread,
- threadgate: res.data.threadgate as
- | AppBskyFeedDefs.ThreadgateView
- | undefined,
- }
- }
- return {thread: {type: 'unknown', uri: uri!}}
- },
- enabled: !!uri,
- placeholderData: () => {
- if (!uri) return
- const post = findPostInQueryData(queryClient, uri)
- if (post) {
- return {thread: post}
- }
- return undefined
- },
- })
-}
-
-export function fillThreadModerationCache(
- cache: ThreadModerationCache,
- node: ThreadNode,
- moderationOpts: ModerationOpts,
-) {
- if (node.type === 'post') {
- cache.set(node, moderatePost(node.post, moderationOpts))
- if (node.parent) {
- fillThreadModerationCache(cache, node.parent, moderationOpts)
- }
- if (node.replies) {
- for (const reply of node.replies) {
- fillThreadModerationCache(cache, reply, moderationOpts)
- }
- }
- }
-}
-
-export function sortThread(
- node: ThreadNode,
- opts: UsePreferencesQueryResponse['threadViewPrefs'],
- modCache: ThreadModerationCache,
- currentDid: string | undefined,
- justPostedUris: Set,
- threadgateRecordHiddenReplies: Set,
- fetchedAtCache: Map,
- fetchedAt: number,
- randomCache: Map,
-): ThreadNode {
- if (node.type !== 'post') {
- return node
- }
- if (node.replies) {
- node.replies.sort((a: ThreadNode, b: ThreadNode) => {
- if (a.type !== 'post') {
- return 1
- }
- if (b.type !== 'post') {
- return -1
- }
-
- if (node.ctx.isHighlightedPost || opts.lab_treeViewEnabled) {
- const aIsJustPosted =
- a.post.author.did === currentDid && justPostedUris.has(a.post.uri)
- const bIsJustPosted =
- b.post.author.did === currentDid && justPostedUris.has(b.post.uri)
- if (aIsJustPosted && bIsJustPosted) {
- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
- } else if (aIsJustPosted) {
- return -1 // reply while onscreen
- } else if (bIsJustPosted) {
- return 1 // reply while onscreen
- }
- }
-
- const aIsByOp = a.post.author.did === node.post?.author.did
- const bIsByOp = b.post.author.did === node.post?.author.did
- if (aIsByOp && bIsByOp) {
- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
- } else if (aIsByOp) {
- return -1 // op's own reply
- } else if (bIsByOp) {
- return 1 // op's own reply
- }
-
- const aIsBySelf = a.post.author.did === currentDid
- const bIsBySelf = b.post.author.did === currentDid
- if (aIsBySelf && bIsBySelf) {
- return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
- } else if (aIsBySelf) {
- return -1 // current account's reply
- } else if (bIsBySelf) {
- return 1 // current account's reply
- }
-
- const aHidden = threadgateRecordHiddenReplies.has(a.uri)
- const bHidden = threadgateRecordHiddenReplies.has(b.uri)
- if (aHidden && !aIsBySelf && !bHidden) {
- return 1
- } else if (bHidden && !bIsBySelf && !aHidden) {
- return -1
- }
-
- const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur)
- const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur)
- if (aBlur !== bBlur) {
- if (aBlur) {
- return 1
- }
- if (bBlur) {
- return -1
- }
- }
-
- const aPin = Boolean(a.record.text.trim() === '📌')
- const bPin = Boolean(b.record.text.trim() === '📌')
- if (aPin !== bPin) {
- if (aPin) {
- return 1
- }
- if (bPin) {
- return -1
- }
- }
-
- if (opts.prioritizeFollowedUsers) {
- const af = a.post.author.viewer?.following
- const bf = b.post.author.viewer?.following
- if (af && !bf) {
- return -1
- } else if (!af && bf) {
- return 1
- }
- }
-
- // Split items from different fetches into separate generations.
- let aFetchedAt = fetchedAtCache.get(a.uri)
- if (aFetchedAt === undefined) {
- fetchedAtCache.set(a.uri, fetchedAt)
- aFetchedAt = fetchedAt
- }
- let bFetchedAt = fetchedAtCache.get(b.uri)
- if (bFetchedAt === undefined) {
- fetchedAtCache.set(b.uri, fetchedAt)
- bFetchedAt = fetchedAt
- }
-
- if (aFetchedAt !== bFetchedAt) {
- return aFetchedAt - bFetchedAt // older fetches first
- } else if (opts.sort === 'hotness') {
- const aHotness = getHotness(a, aFetchedAt)
- const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */)
- return bHotness - aHotness
- } else if (opts.sort === 'oldest') {
- return a.post.indexedAt.localeCompare(b.post.indexedAt)
- } else if (opts.sort === 'newest') {
- return b.post.indexedAt.localeCompare(a.post.indexedAt)
- } else if (opts.sort === 'most-likes') {
- if (a.post.likeCount === b.post.likeCount) {
- return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
- } else {
- return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
- }
- } else if (opts.sort === 'random') {
- let aRandomScore = randomCache.get(a.uri)
- if (aRandomScore === undefined) {
- aRandomScore = Math.random()
- randomCache.set(a.uri, aRandomScore)
- }
- let bRandomScore = randomCache.get(b.uri)
- if (bRandomScore === undefined) {
- bRandomScore = Math.random()
- randomCache.set(b.uri, bRandomScore)
- }
- // this is vaguely criminal but we can get away with it
- return aRandomScore - bRandomScore
- } else {
- return b.post.indexedAt.localeCompare(a.post.indexedAt)
- }
- })
- node.replies.forEach(reply =>
- sortThread(
- reply,
- opts,
- modCache,
- currentDid,
- justPostedUris,
- threadgateRecordHiddenReplies,
- fetchedAtCache,
- fetchedAt,
- randomCache,
- ),
- )
- }
- return node
-}
-
-// internal methods
-// =
-
-// Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html
-// We want to give recent comments a real chance (and not bury them deep below the fold)
-// while also surfacing well-liked comments from the past. In the future, we can explore
-// something more sophisticated, but we don't have much data on the client right now.
-function getHotness(threadPost: ThreadPost, fetchedAt: number) {
- const {post, hasOPLike} = threadPost
- const hoursAgo = Math.max(
- 0,
- (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
- (1000 * 60 * 60),
- )
- const likeCount = post.likeCount ?? 0
- const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
- const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
- const opLikeBoost = hasOPLike ? 0.8 : 1.0
- const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost)
- return likeOrder / timePenalty
-}
-
-function responseToThreadNodes(
- node: ThreadViewNode,
- depth = 0,
- direction: 'up' | 'down' | 'start' = 'start',
-): ThreadNode {
- if (
- AppBskyFeedDefs.isThreadViewPost(node) &&
- bsky.dangerousIsType(
- node.post.record,
- AppBskyFeedPost.isRecord,
- )
- ) {
- const post = node.post
- // These should normally be present. They're missing only for
- // posts that were *just* created. Ideally, the backend would
- // know to return zeros. Fill them in manually to compensate.
- post.replyCount ??= 0
- post.likeCount ??= 0
- post.repostCount ??= 0
- return {
- type: 'post',
- _reactKey: node.post.uri,
- uri: node.post.uri,
- post: post,
- record: node.post.record,
- parent:
- node.parent && direction !== 'down'
- ? responseToThreadNodes(node.parent, depth - 1, 'up')
- : undefined,
- replies:
- node.replies?.length && direction !== 'up'
- ? node.replies
- .map(reply => responseToThreadNodes(reply, depth + 1, 'down'))
- // do not show blocked posts in replies
- .filter(node => node.type !== 'blocked')
- : undefined,
- hasOPLike: Boolean(node?.threadContext?.rootAuthorLike),
- ctx: {
- depth,
- isHighlightedPost: depth === 0,
- hasMore:
- direction === 'down' && !node.replies?.length && !!post.replyCount,
- isSelfThread: false, // populated `annotateSelfThread`
- hasMoreSelfThread: false, // populated in `annotateSelfThread`
- },
- }
- } else if (AppBskyFeedDefs.isBlockedPost(node)) {
- return {type: 'blocked', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
- } else if (AppBskyFeedDefs.isNotFoundPost(node)) {
- return {type: 'not-found', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
- } else {
- return {type: 'unknown', uri: ''}
- }
-}
-
-function annotateSelfThread(thread: ThreadNode) {
- if (thread.type !== 'post') {
- return
- }
- const selfThreadNodes: ThreadPost[] = [thread]
-
- let parent: ThreadNode | undefined = thread.parent
- while (parent) {
- if (
- parent.type !== 'post' ||
- parent.post.author.did !== thread.post.author.did
- ) {
- // not a self-thread
- return
- }
- selfThreadNodes.unshift(parent)
- parent = parent.parent
- }
-
- let node = thread
- for (let i = 0; i < 10; i++) {
- const reply = node.replies?.find(
- r => r.type === 'post' && r.post.author.did === thread.post.author.did,
- )
- if (reply?.type !== 'post') {
- break
- }
- selfThreadNodes.push(reply)
- node = reply
- }
-
- if (selfThreadNodes.length > 1) {
- for (const selfThreadNode of selfThreadNodes) {
- selfThreadNode.ctx.isSelfThread = true
- }
- const last = selfThreadNodes[selfThreadNodes.length - 1]
- if (
- last &&
- last.ctx.depth === REPLY_TREE_DEPTH && // at the edge of the tree depth
- last.post.replyCount && // has replies
- !last.replies?.length // replies were not hydrated
- ) {
- last.ctx.hasMoreSelfThread = true
- }
- }
-}
-
-function findPostInQueryData(
- queryClient: QueryClient,
- uri: string,
-): ThreadNode | void {
- let partial
- for (let item of findAllPostsInQueryData(queryClient, uri)) {
- if (item.type === 'post') {
- // Currently, the backend doesn't send full post info in some cases
- // (for example, for quoted posts). We use missing `likeCount`
- // as a way to detect that. In the future, we should fix this on
- // the backend, which will let us always stop on the first result.
- const hasAllInfo = item.post.likeCount != null
- if (hasAllInfo) {
- return item
- } else {
- partial = item
- // Keep searching, we might still find a full post in the cache.
- }
- }
- }
- return partial
-}
-
-export function* findAllPostsInQueryData(
- queryClient: QueryClient,
- uri: string,
-): Generator {
- const atUri = new AtUri(uri)
-
- const queryDatas = queryClient.getQueriesData({
- queryKey: [RQKEY_ROOT],
- })
- for (const [_queryKey, queryData] of queryDatas) {
- if (!queryData) {
- continue
- }
- const {thread} = queryData
- for (const item of traverseThread(thread)) {
- if (item.type === 'post' && didOrHandleUriMatches(atUri, item.post)) {
- const placeholder = threadNodeToPlaceholderThread(item)
- if (placeholder) {
- yield placeholder
- }
- }
- const quotedPost =
- item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
- if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
- yield embedViewRecordToPlaceholderThread(quotedPost)
- }
- }
- }
- for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
- // Check notifications first. If you have a post in notifications,
- // it's often due to a like or a repost, and we want to prioritize
- // a post object with >0 likes/reposts over a stale version with no
- // metrics in order to avoid a notification->post scroll jump.
- yield postViewToPlaceholderThread(post)
- }
- for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
- yield postViewToPlaceholderThread(post)
- }
- for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
- yield postViewToPlaceholderThread(post)
- }
- for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
- yield postViewToPlaceholderThread(post)
- }
- for (let post of findAllPostsInExploreFeedPreviewsQueryData(
- queryClient,
- uri,
- )) {
- yield postViewToPlaceholderThread(post)
- }
-}
-
-export function* findAllProfilesInQueryData(
- queryClient: QueryClient,
- did: string,
-): Generator {
- const queryDatas = queryClient.getQueriesData({
- queryKey: [RQKEY_ROOT],
- })
- for (const [_queryKey, queryData] of queryDatas) {
- if (!queryData) {
- continue
- }
- const {thread} = queryData
- for (const item of traverseThread(thread)) {
- if (item.type === 'post' && item.post.author.did === did) {
- yield item.post.author
- }
- const quotedPost =
- item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
- if (quotedPost?.author.did === did) {
- yield quotedPost?.author
- }
- }
- }
- for (let profile of findAllProfilesInFeedQueryData(queryClient, did)) {
- yield profile
- }
- for (let profile of findAllProfilesInNotifsQueryData(queryClient, did)) {
- yield profile
- }
- for (let profile of findAllProfilesInSearchQueryData(queryClient, did)) {
- yield profile
- }
- for (let profile of findAllProfilesInExploreFeedPreviewsQueryData(
- queryClient,
- did,
- )) {
- yield profile
- }
-}
-
-function* traverseThread(node: ThreadNode): Generator {
- 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,
- hasOPLike: undefined,
- ctx: {
- depth: 0,
- isHighlightedPost: true,
- hasMore: false,
- isParentLoading: !!node.record.reply,
- isChildLoading: !!node.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, // validated in notifs
- parent: undefined,
- replies: undefined,
- hasOPLike: undefined,
- ctx: {
- depth: 0,
- isHighlightedPost: true,
- hasMore: false,
- isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply,
- isChildLoading: true, // assume yes (show the spinner) just in case
- },
- }
-}
-
-function embedViewRecordToPlaceholderThread(
- record: AppBskyEmbedRecord.ViewRecord,
-): ThreadNode {
- return {
- type: 'post',
- _reactKey: record.uri,
- uri: record.uri,
- post: embedViewRecordToPostView(record),
- record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
- parent: undefined,
- replies: undefined,
- hasOPLike: undefined,
- ctx: {
- depth: 0,
- isHighlightedPost: true,
- hasMore: false,
- isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply,
- isChildLoading: true, // not available, so assume yes (to show the spinner)
- },
- }
-}
diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts
index 305acc5d05..5a34eb3104 100644
--- a/src/state/queries/threadgate/index.ts
+++ b/src/state/queries/threadgate/index.ts
@@ -1,6 +1,5 @@
import {
- AppBskyFeedDefs,
- type AppBskyFeedGetPostThread,
+ type AppBskyFeedDefs,
AppBskyFeedThreadgate,
AtUri,
type BskyAgent,
@@ -8,9 +7,8 @@ import {
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry'
-import {until} from '#/lib/async/until'
import {STALE} from '#/state/queries'
-import {RQKEY_ROOT as postThreadQueryKeyRoot} from '#/state/queries/post-thread'
+import {useGetPost} from '#/state/queries/post'
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
import {
createThreadgateRecord,
@@ -18,6 +16,7 @@ import {
threadgateAllowUISettingToAllowRecordValue,
threadgateViewToAllowUISetting,
} from '#/state/queries/threadgate/util'
+import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread'
import {useAgent} from '#/state/session'
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
import * as bsky from '#/types/bsky'
@@ -71,7 +70,7 @@ export function useThreadgateViewQuery({
postUri?: string
initialData?: AppBskyFeedDefs.ThreadgateView
} = {}) {
- const agent = useAgent()
+ const getPost = useGetPost()
return useQuery({
enabled: !!postUri,
@@ -79,33 +78,12 @@ export function useThreadgateViewQuery({
placeholderData: initialData,
staleTime: STALE.MINUTES.ONE,
async queryFn() {
- return getThreadgateView({
- agent,
- postUri: postUri!,
- })
+ const post = await getPost({uri: postUri!})
+ return post.threadgate ?? null
},
})
}
-export async function getThreadgateView({
- agent,
- postUri,
-}: {
- agent: BskyAgent
- postUri: string
-}) {
- const {data} = await agent.app.bsky.feed.getPostThread({
- uri: postUri!,
- depth: 0,
- })
-
- if (AppBskyFeedDefs.isThreadViewPost(data.thread)) {
- return data.thread.post.threadgate ?? null
- }
-
- return null
-}
-
export async function getThreadgateRecord({
agent,
postUri,
@@ -248,6 +226,8 @@ export async function updateThreadgateAllow({
export function useSetThreadgateAllowMutation() {
const agent = useAgent()
const queryClient = useQueryClient()
+ const getPost = useGetPost()
+ const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache()
return useMutation({
mutationFn: async ({
@@ -272,30 +252,32 @@ export function useSetThreadgateAllowMutation() {
})
},
async onSuccess(_, {postUri, allow}) {
- await until(
+ const data = await retry(
5, // 5 tries
- 1e3, // 1s delay between tries
- (res: AppBskyFeedGetPostThread.Response) => {
- const thread = res.data.thread
- if (AppBskyFeedDefs.isThreadViewPost(thread)) {
- const fetchedSettings = threadgateViewToAllowUISetting(
- thread.post.threadgate,
+ _e => true,
+ async () => {
+ const post = await getPost({uri: postUri})
+ const threadgate = post.threadgate
+ if (!threadgate) {
+ throw new Error(
+ `useSetThreadgateAllowMutation: could not fetch threadgate, appview may not be ready yet`,
)
- return JSON.stringify(fetchedSettings) === JSON.stringify(allow)
}
- return false
+ const fetchedSettings = threadgateViewToAllowUISetting(threadgate)
+ const isReady =
+ JSON.stringify(fetchedSettings) === JSON.stringify(allow)
+ if (!isReady) {
+ throw new Error(
+ `useSetThreadgateAllowMutation: appview isn't ready yet`,
+ ) // try again
+ }
+ return threadgate
},
- () => {
- return agent.app.bsky.feed.getPostThread({
- uri: postUri,
- depth: 0,
- })
- },
- )
+ 1e3, // 1s delay between tries
+ ).catch(() => {})
+
+ if (data) updatePostThreadThreadgate(data)
- queryClient.invalidateQueries({
- queryKey: [postThreadQueryKeyRoot],
- })
queryClient.invalidateQueries({
queryKey: [threadgateRecordQueryKeyRoot],
})
diff --git a/src/state/queries/usePostThread/context.tsx b/src/state/queries/usePostThread/context.tsx
new file mode 100644
index 0000000000..f825965416
--- /dev/null
+++ b/src/state/queries/usePostThread/context.tsx
@@ -0,0 +1,43 @@
+import {createContext, useContext} from 'react'
+
+import {
+ type createPostThreadOtherQueryKey,
+ type createPostThreadQueryKey,
+} from '#/state/queries/usePostThread/types'
+
+/**
+ * Contains static metadata about the post thread query, suitable for
+ * context e.g. query keys and other things that don't update frequently.
+ *
+ * Be careful adding things here, as it could cause unnecessary re-renders.
+ */
+export type PostThreadContextType = {
+ postThreadQueryKey: ReturnType
+ postThreadOtherQueryKey: ReturnType
+}
+
+const PostThreadContext = createContext(
+ undefined,
+)
+
+/**
+ * Use the current {@link PostThreadContext}, if one is available. If not,
+ * returns `undefined`.
+ */
+export function usePostThreadContext() {
+ return useContext(PostThreadContext)
+}
+
+export function PostThreadContextProvider({
+ children,
+ context,
+}: {
+ children: React.ReactNode
+ context?: PostThreadContextType
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts
index 8495df04c8..e00250f740 100644
--- a/src/state/queries/usePostThread/index.ts
+++ b/src/state/queries/usePostThread/index.ts
@@ -11,6 +11,7 @@ import {
TREE_VIEW_BELOW_DESKTOP,
TREE_VIEW_BF,
} from '#/state/queries/usePostThread/const'
+import {type PostThreadContextType} from '#/state/queries/usePostThread/context'
import {
createCacheMutator,
getThreadPlaceholder,
@@ -31,6 +32,8 @@ import {useAgent, useSession} from '#/state/session'
import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useBreakpoints} from '#/alf'
+export * from '#/state/queries/usePostThread/context'
+export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
export * from '#/state/queries/usePostThread/types'
export function usePostThread({anchor}: {anchor?: string}) {
@@ -277,8 +280,13 @@ export function usePostThread({anchor}: {anchor?: string}) {
setOtherItemsVisible,
])
- return useMemo(
- () => ({
+ return useMemo(() => {
+ const context: PostThreadContextType = {
+ postThreadQueryKey,
+ postThreadOtherQueryKey,
+ }
+ return {
+ context,
state: {
/*
* Copy in any query state that is useful
@@ -309,17 +317,18 @@ export function usePostThread({anchor}: {anchor?: string}) {
setSort,
setView,
},
- }),
- [
- query,
- mutator.insertReplies,
- otherItemsVisible,
- sort,
- view,
- setSort,
- setView,
- threadgate,
- items,
- ],
- )
+ }
+ }, [
+ query,
+ mutator.insertReplies,
+ otherItemsVisible,
+ sort,
+ view,
+ setSort,
+ setView,
+ threadgate,
+ items,
+ postThreadQueryKey,
+ postThreadOtherQueryKey,
+ ])
}
diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts
index 5e27ebb875..5f9f263280 100644
--- a/src/state/queries/usePostThread/queryCache.ts
+++ b/src/state/queries/usePostThread/queryCache.ts
@@ -1,3 +1,4 @@
+import {useCallback} from 'react'
import {
type $Typed,
type AppBskyActorDefs,
@@ -7,7 +8,7 @@ import {
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
} from '@atproto/api'
-import {type QueryClient} from '@tanstack/react-query'
+import {type QueryClient, useQueryClient} from '@tanstack/react-query'
import {
dangerousGetPostShadow,
@@ -18,6 +19,7 @@ import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
+import {usePostThreadContext} from '#/state/queries/usePostThread'
import {getBranch} from '#/state/queries/usePostThread/traversal'
import {
type ApiThreadItem,
@@ -322,3 +324,51 @@ export function* findAllProfilesInQueryData(
}
}
}
+
+export function useUpdatePostThreadThreadgateQueryCache() {
+ const qc = useQueryClient()
+ const context = usePostThreadContext()
+
+ return useCallback(
+ (threadgate: AppBskyFeedDefs.ThreadgateView) => {
+ if (!context) return
+
+ function mutator(thread: ApiThreadItem[]): T[] {
+ for (let i = 0; i < thread.length; i++) {
+ const item = thread[i]
+
+ if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) continue
+
+ if (item.depth === 0) {
+ thread.splice(i, 1, {
+ ...item,
+ value: {
+ ...item.value,
+ post: {
+ ...item.value.post,
+ threadgate,
+ },
+ },
+ })
+ }
+ }
+
+ return thread as T[]
+ }
+
+ qc.setQueryData(
+ context.postThreadQueryKey,
+ data => {
+ if (!data) return
+ return {
+ ...data,
+ thread: mutator([
+ ...data.thread,
+ ]),
+ }
+ },
+ )
+ },
+ [qc, context],
+ )
+}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 8cbb2d37ba..305790cb18 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -44,9 +44,8 @@ import Animated, {
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type ImagePickerAsset} from 'expo-image-picker'
import {
- AppBskyFeedDefs,
- type AppBskyFeedGetPostThread,
AppBskyUnspeccedDefs,
+ type AppBskyUnspeccedGetPostThreadV2,
AtUri,
type BskyAgent,
type RichText,
@@ -549,10 +548,10 @@ export const ComposePost = ({
if (initQuote) {
// We want to wait for the quote count to update before we call `onPost`, which will refetch data
whenAppViewReady(agent, initQuote.uri, res => {
- const quotedThread = res.data.thread
+ const anchor = res.data.thread.at(0)
if (
- AppBskyFeedDefs.isThreadViewPost(quotedThread) &&
- quotedThread.post.quoteCount !== initQuote.quoteCount
+ AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) &&
+ anchor.value.post.quoteCount !== initQuote.quoteCount
) {
onPost?.(postUri)
onPostSuccess?.(postSuccessData)
@@ -1661,16 +1660,18 @@ function useKeyboardVerticalOffset() {
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
- fn: (res: AppBskyFeedGetPostThread.Response) => boolean,
+ fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() =>
- agent.app.bsky.feed.getPostThread({
- uri,
- depth: 0,
+ agent.app.bsky.unspecced.getPostThreadV2({
+ anchor: uri,
+ above: false,
+ below: 0,
+ branchingFactor: 0,
}),
)
}