Remove remaining usages of old post thread query (#9184)
* Remove remaining usages of old post thread query * Add PostThreadContext, cache mutator for threadgates on threads, pipe it through * Replace getPostThread in threadgate query * Replace in initQuote handling, which isn't even used rn... * Missing import * Revert ext change
This commit is contained in:
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||||
|
import {useGetPost} from '#/state/queries/post'
|
||||||
import {
|
import {
|
||||||
createPostgateQueryKey,
|
createPostgateQueryKey,
|
||||||
getPostgateRecord,
|
getPostgateRecord,
|
||||||
@@ -25,12 +26,15 @@ import {
|
|||||||
} from '#/state/queries/postgate/util'
|
} from '#/state/queries/postgate/util'
|
||||||
import {
|
import {
|
||||||
createThreadgateViewQueryKey,
|
createThreadgateViewQueryKey,
|
||||||
getThreadgateView,
|
|
||||||
type ThreadgateAllowUISetting,
|
type ThreadgateAllowUISetting,
|
||||||
threadgateViewToAllowUISetting,
|
threadgateViewToAllowUISetting,
|
||||||
useSetThreadgateAllowMutation,
|
useSetThreadgateAllowMutation,
|
||||||
useThreadgateViewQuery,
|
useThreadgateViewQuery,
|
||||||
} from '#/state/queries/threadgate'
|
} from '#/state/queries/threadgate'
|
||||||
|
import {
|
||||||
|
PostThreadContextProvider,
|
||||||
|
usePostThreadContext,
|
||||||
|
} from '#/state/queries/usePostThread'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
|
|||||||
export function PostInteractionSettingsDialog(
|
export function PostInteractionSettingsDialog(
|
||||||
props: PostInteractionSettingsDialogProps,
|
props: PostInteractionSettingsDialogProps,
|
||||||
) {
|
) {
|
||||||
|
const postThreadContext = usePostThreadContext()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
<PostThreadContextProvider context={postThreadContext}>
|
||||||
|
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||||
|
</PostThreadContextProvider>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const getPost = useGetPost()
|
||||||
|
|
||||||
return React.useCallback(async () => {
|
return React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
}),
|
}),
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
queryKey: createThreadgateViewQueryKey(rootPostUri),
|
queryKey: createThreadgateViewQueryKey(rootPostUri),
|
||||||
queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
|
queryFn: async () => {
|
||||||
|
const post = await getPost({uri: rootPostUri})
|
||||||
|
return post.threadgate ?? null
|
||||||
|
},
|
||||||
staleTime: STALE.SECONDS.THIRTY,
|
staleTime: STALE.SECONDS.THIRTY,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [queryClient, agent, postUri, rootPostUri])
|
}, [queryClient, agent, postUri, rootPostUri, getPost])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
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 {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
|
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
@@ -17,11 +17,11 @@ export const PostLikedByScreen = ({route}: Props) => {
|
|||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {name, rkey} = route.params
|
const {name, rkey} = route.params
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
const {data: post} = usePostThreadQuery(uri)
|
const {data: post} = usePostQuery(uri)
|
||||||
|
|
||||||
let likeCount
|
let likeCount
|
||||||
if (post?.thread.type === 'post') {
|
if (post) {
|
||||||
likeCount = post.thread.post.likeCount
|
likeCount = post.likeCount
|
||||||
}
|
}
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
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 {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
@@ -17,11 +17,11 @@ export const PostQuotesScreen = ({route}: Props) => {
|
|||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {name, rkey} = route.params
|
const {name, rkey} = route.params
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
const {data: post} = usePostThreadQuery(uri)
|
const {data: post} = usePostQuery(uri)
|
||||||
|
|
||||||
let quoteCount
|
let quoteCount
|
||||||
if (post?.thread.type === 'post') {
|
if (post) {
|
||||||
quoteCount = post.thread.post.quoteCount
|
quoteCount = post.quoteCount
|
||||||
}
|
}
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
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 {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
|
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
@@ -17,11 +17,11 @@ export const PostRepostedByScreen = ({route}: Props) => {
|
|||||||
const {name, rkey} = route.params
|
const {name, rkey} = route.params
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {data: post} = usePostThreadQuery(uri)
|
const {data: post} = usePostQuery(uri)
|
||||||
|
|
||||||
let quoteCount
|
let quoteCount
|
||||||
if (post?.thread.type === 'post') {
|
if (post) {
|
||||||
quoteCount = post.thread.post.repostCount
|
quoteCount = post.repostCount
|
||||||
}
|
}
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
|||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useFeedFeedback} from '#/state/feed-feedback'
|
import {useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
|
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 {useSession} from '#/state/session'
|
||||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
@@ -492,7 +496,7 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
|
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PostThreadContextProvider context={thread.context}>
|
||||||
<Layout.Header.Outer headerRef={headerRef}>
|
<Layout.Header.Outer headerRef={headerRef}>
|
||||||
<Layout.Header.BackButton />
|
<Layout.Header.BackButton />
|
||||||
<Layout.Header.Content>
|
<Layout.Header.Content>
|
||||||
@@ -575,7 +579,7 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
{!gtMobile && canReply && hasSession && (
|
{!gtMobile && canReply && hasSession && (
|
||||||
<MobileComposePrompt onPressReply={onReplyToAnchor} />
|
<MobileComposePrompt onPressReply={onReplyToAnchor} />
|
||||||
)}
|
)}
|
||||||
</>
|
</PostThreadContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
-6
@@ -12,7 +12,6 @@ import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} f
|
|||||||
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
|
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed'
|
||||||
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
|
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
|
||||||
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
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 findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
||||||
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
||||||
import {castAsShadow, type Shadow} from './types'
|
import {castAsShadow, type Shadow} from './types'
|
||||||
@@ -176,11 +175,6 @@ function* findPostsInCache(
|
|||||||
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
|
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
|
||||||
yield post
|
yield post
|
||||||
}
|
}
|
||||||
for (let node of findAllPostsInThreadQueryData(queryClient, uri)) {
|
|
||||||
if (node.type === 'post') {
|
|
||||||
yield node.post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) {
|
for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) {
|
||||||
yield post
|
yield post
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-2
@@ -16,7 +16,6 @@ import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/s
|
|||||||
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
|
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
|
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
|
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 findAllProfilesInProfileQueryData} from '#/state/queries/profile'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
|
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
||||||
@@ -173,7 +172,6 @@ function* findProfilesInCache(
|
|||||||
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInFeedsQueryData(queryClient, did)
|
yield* findAllProfilesInFeedsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInPostThreadQueryData(queryClient, did)
|
|
||||||
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
||||||
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
|
yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
|
||||||
|
|||||||
@@ -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<ThreadNode, ModerationDecision>
|
|
||||||
|
|
||||||
export type PostThreadQueryData = {
|
|
||||||
thread: ThreadNode
|
|
||||||
threadgate?: AppBskyFeedDefs.ThreadgateView
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePostThreadQuery(uri: string | undefined) {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const agent = useAgent()
|
|
||||||
return useQuery<PostThreadQueryData, Error>({
|
|
||||||
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<string>,
|
|
||||||
threadgateRecordHiddenReplies: Set<string>,
|
|
||||||
fetchedAtCache: Map<string, number>,
|
|
||||||
fetchedAt: number,
|
|
||||||
randomCache: Map<string, number>,
|
|
||||||
): 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<AppBskyFeedPost.Record>(
|
|
||||||
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<ThreadNode, void> {
|
|
||||||
const atUri = new AtUri(uri)
|
|
||||||
|
|
||||||
const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
|
|
||||||
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<AppBskyActorDefs.ProfileViewBasic, void> {
|
|
||||||
const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
|
|
||||||
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<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,
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
type AppBskyFeedGetPostThread,
|
|
||||||
AppBskyFeedThreadgate,
|
AppBskyFeedThreadgate,
|
||||||
AtUri,
|
AtUri,
|
||||||
type BskyAgent,
|
type BskyAgent,
|
||||||
@@ -8,9 +7,8 @@ import {
|
|||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {networkRetry, retry} from '#/lib/async/retry'
|
import {networkRetry, retry} from '#/lib/async/retry'
|
||||||
import {until} from '#/lib/async/until'
|
|
||||||
import {STALE} from '#/state/queries'
|
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 {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
|
||||||
import {
|
import {
|
||||||
createThreadgateRecord,
|
createThreadgateRecord,
|
||||||
@@ -18,6 +16,7 @@ import {
|
|||||||
threadgateAllowUISettingToAllowRecordValue,
|
threadgateAllowUISettingToAllowRecordValue,
|
||||||
threadgateViewToAllowUISetting,
|
threadgateViewToAllowUISetting,
|
||||||
} from '#/state/queries/threadgate/util'
|
} from '#/state/queries/threadgate/util'
|
||||||
|
import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
|
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
@@ -71,7 +70,7 @@ export function useThreadgateViewQuery({
|
|||||||
postUri?: string
|
postUri?: string
|
||||||
initialData?: AppBskyFeedDefs.ThreadgateView
|
initialData?: AppBskyFeedDefs.ThreadgateView
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const agent = useAgent()
|
const getPost = useGetPost()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!postUri,
|
enabled: !!postUri,
|
||||||
@@ -79,33 +78,12 @@ export function useThreadgateViewQuery({
|
|||||||
placeholderData: initialData,
|
placeholderData: initialData,
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
return getThreadgateView({
|
const post = await getPost({uri: postUri!})
|
||||||
agent,
|
return post.threadgate ?? null
|
||||||
postUri: postUri!,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
export async function getThreadgateRecord({
|
||||||
agent,
|
agent,
|
||||||
postUri,
|
postUri,
|
||||||
@@ -248,6 +226,8 @@ export async function updateThreadgateAllow({
|
|||||||
export function useSetThreadgateAllowMutation() {
|
export function useSetThreadgateAllowMutation() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const getPost = useGetPost()
|
||||||
|
const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
@@ -272,30 +252,32 @@ export function useSetThreadgateAllowMutation() {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async onSuccess(_, {postUri, allow}) {
|
async onSuccess(_, {postUri, allow}) {
|
||||||
await until(
|
const data = await retry<AppBskyFeedDefs.ThreadgateView | undefined>(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
1e3, // 1s delay between tries
|
_e => true,
|
||||||
(res: AppBskyFeedGetPostThread.Response) => {
|
async () => {
|
||||||
const thread = res.data.thread
|
const post = await getPost({uri: postUri})
|
||||||
if (AppBskyFeedDefs.isThreadViewPost(thread)) {
|
const threadgate = post.threadgate
|
||||||
const fetchedSettings = threadgateViewToAllowUISetting(
|
if (!threadgate) {
|
||||||
thread.post.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
|
||||||
},
|
},
|
||||||
() => {
|
1e3, // 1s delay between tries
|
||||||
return agent.app.bsky.feed.getPostThread({
|
).catch(() => {})
|
||||||
uri: postUri,
|
|
||||||
depth: 0,
|
if (data) updatePostThreadThreadgate(data)
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: [postThreadQueryKeyRoot],
|
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [threadgateRecordQueryKeyRoot],
|
queryKey: [threadgateRecordQueryKeyRoot],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<typeof createPostThreadQueryKey>
|
||||||
|
postThreadOtherQueryKey: ReturnType<typeof createPostThreadOtherQueryKey>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostThreadContext = createContext<PostThreadContextType | undefined>(
|
||||||
|
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 (
|
||||||
|
<PostThreadContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</PostThreadContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TREE_VIEW_BELOW_DESKTOP,
|
TREE_VIEW_BELOW_DESKTOP,
|
||||||
TREE_VIEW_BF,
|
TREE_VIEW_BF,
|
||||||
} from '#/state/queries/usePostThread/const'
|
} from '#/state/queries/usePostThread/const'
|
||||||
|
import {type PostThreadContextType} from '#/state/queries/usePostThread/context'
|
||||||
import {
|
import {
|
||||||
createCacheMutator,
|
createCacheMutator,
|
||||||
getThreadPlaceholder,
|
getThreadPlaceholder,
|
||||||
@@ -31,6 +32,8 @@ import {useAgent, useSession} from '#/state/session'
|
|||||||
import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {useBreakpoints} from '#/alf'
|
import {useBreakpoints} from '#/alf'
|
||||||
|
|
||||||
|
export * from '#/state/queries/usePostThread/context'
|
||||||
|
export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
|
||||||
export * from '#/state/queries/usePostThread/types'
|
export * from '#/state/queries/usePostThread/types'
|
||||||
|
|
||||||
export function usePostThread({anchor}: {anchor?: string}) {
|
export function usePostThread({anchor}: {anchor?: string}) {
|
||||||
@@ -277,8 +280,13 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
|||||||
setOtherItemsVisible,
|
setOtherItemsVisible,
|
||||||
])
|
])
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(() => {
|
||||||
() => ({
|
const context: PostThreadContextType = {
|
||||||
|
postThreadQueryKey,
|
||||||
|
postThreadOtherQueryKey,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
context,
|
||||||
state: {
|
state: {
|
||||||
/*
|
/*
|
||||||
* Copy in any query state that is useful
|
* Copy in any query state that is useful
|
||||||
@@ -309,17 +317,18 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
|||||||
setSort,
|
setSort,
|
||||||
setView,
|
setView,
|
||||||
},
|
},
|
||||||
}),
|
}
|
||||||
[
|
}, [
|
||||||
query,
|
query,
|
||||||
mutator.insertReplies,
|
mutator.insertReplies,
|
||||||
otherItemsVisible,
|
otherItemsVisible,
|
||||||
sort,
|
sort,
|
||||||
view,
|
view,
|
||||||
setSort,
|
setSort,
|
||||||
setView,
|
setView,
|
||||||
threadgate,
|
threadgate,
|
||||||
items,
|
items,
|
||||||
],
|
postThreadQueryKey,
|
||||||
)
|
postThreadOtherQueryKey,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
import {
|
import {
|
||||||
type $Typed,
|
type $Typed,
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
type AppBskyUnspeccedGetPostThreadV2,
|
type AppBskyUnspeccedGetPostThreadV2,
|
||||||
AtUri,
|
AtUri,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {type QueryClient} from '@tanstack/react-query'
|
import {type QueryClient, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
dangerousGetPostShadow,
|
dangerousGetPostShadow,
|
||||||
@@ -18,6 +19,7 @@ import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/
|
|||||||
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
|
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed'
|
||||||
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes'
|
||||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
||||||
|
import {usePostThreadContext} from '#/state/queries/usePostThread'
|
||||||
import {getBranch} from '#/state/queries/usePostThread/traversal'
|
import {getBranch} from '#/state/queries/usePostThread/traversal'
|
||||||
import {
|
import {
|
||||||
type ApiThreadItem,
|
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<T>(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<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
|
||||||
|
context.postThreadQueryKey,
|
||||||
|
data => {
|
||||||
|
if (!data) return
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
thread: mutator<AppBskyUnspeccedGetPostThreadV2.ThreadItem>([
|
||||||
|
...data.thread,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[qc, context],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,9 +44,8 @@ import Animated, {
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
|
||||||
type AppBskyFeedGetPostThread,
|
|
||||||
AppBskyUnspeccedDefs,
|
AppBskyUnspeccedDefs,
|
||||||
|
type AppBskyUnspeccedGetPostThreadV2,
|
||||||
AtUri,
|
AtUri,
|
||||||
type BskyAgent,
|
type BskyAgent,
|
||||||
type RichText,
|
type RichText,
|
||||||
@@ -549,10 +548,10 @@ export const ComposePost = ({
|
|||||||
if (initQuote) {
|
if (initQuote) {
|
||||||
// We want to wait for the quote count to update before we call `onPost`, which will refetch data
|
// We want to wait for the quote count to update before we call `onPost`, which will refetch data
|
||||||
whenAppViewReady(agent, initQuote.uri, res => {
|
whenAppViewReady(agent, initQuote.uri, res => {
|
||||||
const quotedThread = res.data.thread
|
const anchor = res.data.thread.at(0)
|
||||||
if (
|
if (
|
||||||
AppBskyFeedDefs.isThreadViewPost(quotedThread) &&
|
AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) &&
|
||||||
quotedThread.post.quoteCount !== initQuote.quoteCount
|
anchor.value.post.quoteCount !== initQuote.quoteCount
|
||||||
) {
|
) {
|
||||||
onPost?.(postUri)
|
onPost?.(postUri)
|
||||||
onPostSuccess?.(postSuccessData)
|
onPostSuccess?.(postSuccessData)
|
||||||
@@ -1661,16 +1660,18 @@ function useKeyboardVerticalOffset() {
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
agent: BskyAgent,
|
agent: BskyAgent,
|
||||||
uri: string,
|
uri: string,
|
||||||
fn: (res: AppBskyFeedGetPostThread.Response) => boolean,
|
fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
1e3, // 1s delay between tries
|
1e3, // 1s delay between tries
|
||||||
fn,
|
fn,
|
||||||
() =>
|
() =>
|
||||||
agent.app.bsky.feed.getPostThread({
|
agent.app.bsky.unspecced.getPostThreadV2({
|
||||||
uri,
|
anchor: uri,
|
||||||
depth: 0,
|
above: false,
|
||||||
|
below: 0,
|
||||||
|
branchingFactor: 0,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user