Add PostThreadContext, cache mutator for threadgates on threads, pipe it through

This commit is contained in:
Eric Bailey
2025-10-10 16:09:17 -05:00
parent 761fa0bde9
commit c8ded6291f
6 changed files with 157 additions and 38 deletions
@@ -31,6 +31,10 @@ import {
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 />
<PostThreadContextProvider context={postThreadContext}>
<PostInteractionSettingsDialogControlledInner {...props} /> <PostInteractionSettingsDialogControlledInner {...props} />
</PostThreadContextProvider>
</Dialog.Outer> </Dialog.Outer>
) )
} }
+7 -3
View File
@@ -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'
@@ -495,7 +499,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>
@@ -578,7 +582,7 @@ export function PostThread({uri}: {uri: string}) {
{!gtMobile && canReply && hasSession && ( {!gtMobile && canReply && hasSession && (
<MobileComposePrompt onPressReply={onReplyToAnchor} /> <MobileComposePrompt onPressReply={onReplyToAnchor} />
)} )}
</> </PostThreadContextProvider>
) )
} }
+25 -18
View File
@@ -1,6 +1,5 @@
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
type AppBskyFeedGetPostThread,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AtUri, AtUri,
type BskyAgent, type BskyAgent,
@@ -8,8 +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 {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,
@@ -17,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'
@@ -247,6 +247,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 ({
@@ -271,26 +273,31 @@ 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({ 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>
)
}
@@ -31,6 +31,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 +279,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,8 +316,8 @@ export function usePostThread({anchor}: {anchor?: string}) {
setSort, setSort,
setView, setView,
}, },
}), }
[ }, [
query, query,
mutator.insertReplies, mutator.insertReplies,
otherItemsVisible, otherItemsVisible,
@@ -320,6 +327,7 @@ export function usePostThread({anchor}: {anchor?: string}) {
setView, setView,
threadgate, threadgate,
items, items,
], postThreadQueryKey,
) postThreadOtherQueryKey,
])
} }
+51 -1
View File
@@ -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],
)
}