diff --git a/package.json b/package.json index 61d562e32d..97918bdc0f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.10", + "@atproto/api": "^0.15.11", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index c414614f8f..b563e7b6bf 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -9,6 +9,8 @@ import { import {traverse} from '#/state/queries/usePostThread/traversal' import { createPostThreadQueryKey, + createPostThreadHiddenQueryKey, + type ThreadItem, type UsePostThreadProps, } from '#/state/queries/usePostThread/types' import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' @@ -32,9 +34,6 @@ export function usePostThread({ params, }) - const hasHiddenReplies = useRef(false) - const [showHiddenReplies, setShowHiddenReplies] = useState(false) - const query = useQuery({ enabled, queryKey, @@ -76,16 +75,48 @@ export function usePostThread({ }, }) + const hasHiddenReplies = useRef(false) if (query?.data?.hasHiddenReplies) { hasHiddenReplies.current = true } + const [showHiddenReplies, setShowHiddenReplies] = useState(false) + const [hiddenReplies, setHiddenReplies] = useState([]) const loadHiddenReplies = useCallback(async () => { setShowHiddenReplies(true) - }, [setShowHiddenReplies]) + setHiddenReplies(Array.from({length: 2}).map((_, i) => ({ + type: 'skeleton', + key: `${params.anchor!}-reply-${i}`, + item: 'reply', + }))) + const queryParams = { + anchor: params.anchor!, + prioritizeFollowedUsers: params.prioritizeFollowedUsers, + } + const data = await qc.fetchQuery({ + queryKey: createPostThreadHiddenQueryKey(queryParams), + async queryFn() { + const {data} = await agent.app.bsky.unspecced.getPostThreadHiddenV2(queryParams) + return data.thread || [] + }, + }) + const items = traverse(data || [], { + threadgateHiddenReplies: mergeThreadgateHiddenReplies( + query.data?.threadgate?.record, + ), + moderationOpts: moderationOpts!, + hasSession, + view: params.view, + hasHiddenReplies: hasHiddenReplies.current, + showHiddenReplies, + skipHiddenReplyHandling: true, + loadHiddenReplies, + }) + setHiddenReplies(items) + }, [params, setShowHiddenReplies]) const items = useMemo(() => { - return traverse(query.data?.thread || [], { + const results = traverse(query.data?.thread || [], { threadgateHiddenReplies: mergeThreadgateHiddenReplies( query.data?.threadgate?.record, ), @@ -96,6 +127,8 @@ export function usePostThread({ showHiddenReplies, loadHiddenReplies, }) + + return results.concat(hiddenReplies) }, [ query.data, mergeThreadgateHiddenReplies, @@ -104,6 +137,7 @@ export function usePostThread({ params.view, showHiddenReplies, loadHiddenReplies, + hiddenReplies, ]) if (query.isPlaceholderData) { diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index 88180d2318..5ff8e3fa71 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -2,6 +2,7 @@ import { type $Typed, type AppBskyFeedDefs, AppBskyUnspeccedGetPostThreadV2, + AppBskyUnspeccedDefs, AtUri, } from '@atproto/api' import {type QueryClient} from '@tanstack/react-query' @@ -34,7 +35,7 @@ export function createCacheMutator({ return { insertReplies( parentUri: string, - replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], + replies: AppBskyUnspeccedDefs.ThreadItem[], ) { queryClient.setQueryData( queryKey, @@ -46,7 +47,7 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const existingParent = thread[i] if ( - !AppBskyUnspeccedGetPostThreadV2.isThreadItemPost( + !AppBskyUnspeccedDefs.isThreadItemPost( existingParent.value, ) ) @@ -68,7 +69,7 @@ export function createCacheMutator({ !nextItem || nextItem.depth <= existingParent.depth const firstReply = replies.at(0) const opIsReplier = - AppBskyUnspeccedGetPostThreadV2.isThreadItemPost( + AppBskyUnspeccedDefs.isThreadItemPost( firstReply?.value, ) ? opDid === firstReply.value.post.author.did @@ -118,7 +119,7 @@ export function createCacheMutator({ * Unused atm, post shadow does the trick, but it would be nice to clean up * the whole sub-tree on deletes. */ - deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) { + deletePost(post: AppBskyUnspeccedDefs.ThreadItem) { queryClient.setQueryData( queryKey, queryData => { @@ -128,7 +129,7 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const existingPost = thread[i] - if (!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(post.value)) + if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue if (existingPost.uri === post.uri) { @@ -151,7 +152,7 @@ export function createCacheMutator({ export function getThreadPlaceholder( queryClient: QueryClient, uri: string, -): $Typed | void { +): $Typed | void { let partial for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { /* @@ -178,8 +179,8 @@ export function* getThreadPlaceholderCandidates( uri: string, ): Generator< $Typed< - Omit & { - value: $Typed + Omit & { + value: $Typed } >, void @@ -233,7 +234,7 @@ export function* findAllPostsInQueryData( const {thread} = queryData for (const item of thread) { - if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { if (didOrHandleUriMatches(atUri, item.value.post)) { yield item.value.post } diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 6e1e61b78a..0a77c9b652 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -1,5 +1,5 @@ import { - AppBskyUnspeccedGetPostThreadV2, + AppBskyUnspeccedDefs, type ModerationDecision, type ModerationOpts, } from '@atproto/api' @@ -18,7 +18,7 @@ import { import * as views from '#/state/queries/usePostThread/views' export function traverse( - thread: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'], + thread: AppBskyUnspeccedDefs.ThreadItem[], { threadgateHiddenReplies, moderationOpts, @@ -26,6 +26,7 @@ export function traverse( view, hasHiddenReplies, showHiddenReplies, + skipHiddenReplyHandling, loadHiddenReplies, }: { threadgateHiddenReplies: Set @@ -34,6 +35,7 @@ export function traverse( view: PostThreadParams['view'] hasHiddenReplies: boolean showHiddenReplies: boolean + skipHiddenReplyHandling?: boolean loadHiddenReplies: () => Promise }, ) { @@ -46,7 +48,7 @@ export function traverse( let parentMetadata: TraversalMetadata | undefined let metadata: TraversalMetadata | undefined - if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { + if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { parentMetadata = metadatas.get( getPostRecord(item.value.post).reply?.parent?.uri || '', ) @@ -66,20 +68,20 @@ export function traverse( */ } else if (item.depth === 0) { if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated( + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( item.value, ) ) { items.push(views.threadPostNoUnauthenticated(item)) } else if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(item.value) + AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) ) { items.push(views.threadPostNotFound(item)) } else if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(item.value) + AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) ) { items.push(views.threadPostBlocked(item)) - } else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { + } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { const post = views.threadPost({ uri: item.uri, depth: item.depth, @@ -92,24 +94,24 @@ export function traverse( const parent = thread[pi] if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated( + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( parent.value, ) ) { items.unshift(views.threadPostNoUnauthenticated(parent)) break parentTraversal } else if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(parent.value) + AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value) ) { items.unshift(views.threadPostNotFound(parent)) break parentTraversal } else if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(parent.value) + AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value) ) { items.unshift(views.threadPostBlocked(parent)) break parentTraversal } else if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(parent.value) + AppBskyUnspeccedDefs.isThreadItemPost(parent.value) ) { items.unshift( views.threadPost({ @@ -129,18 +131,18 @@ export function traverse( * we could. */ const shouldBreak = - AppBskyUnspeccedGetPostThreadV2.isThreadItemNoUnauthenticated( + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( item.value, ) || - AppBskyUnspeccedGetPostThreadV2.isThreadItemNotFound(item.value) || - AppBskyUnspeccedGetPostThreadV2.isThreadItemBlocked(item.value) + AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || + AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) if (shouldBreak) { const branch = getBranch(thread, i, item.depth) // could insert tombstone i = branch.end continue traversal - } else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { + } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { if (parentMetadata) { /* * Set this value before incrementing the parent's repliesSeenCount @@ -161,7 +163,7 @@ export function traverse( const postIsModerated = postIsHiddenByThreadgate || postMod.blurred || postMod.muted - if (!postIsModerated) { + if (!postIsModerated || skipHiddenReplyHandling) { /* * Not moderated, probably need to insert it */ @@ -192,7 +194,7 @@ export function traverse( const child = thread[ci] if ( - AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(child.value) + AppBskyUnspeccedDefs.isThreadItemPost(child.value) ) { const childParentMetadata = metadatas.get( getPostRecord(child.value.post).reply?.parent?.uri || '', @@ -256,15 +258,17 @@ export function traverse( } } - if (hidden.length || hasHiddenReplies) { - if (showHiddenReplies) { - items.push(...hidden) - } else { - items.push({ - type: 'showHiddenReplies', - key: 'showHiddenReplies', - onLoad: loadHiddenReplies, - }) + if (!skipHiddenReplyHandling) { + if (hidden.length || hasHiddenReplies) { + if (showHiddenReplies) { + items.push(...hidden) + } else { + items.push({ + type: 'showHiddenReplies', + key: 'showHiddenReplies', + onLoad: loadHiddenReplies, + }) + } } } @@ -428,7 +432,7 @@ export function traverse( * const { start: 1, end: 3 } = getBranch(items, 1, 1) */ export function getBranch( - thread: AppBskyUnspeccedGetPostThreadV2.OutputSchema['thread'], + thread: AppBskyUnspeccedDefs.ThreadItem[], branchStartIndex: number, branchStartDepth: number, ) { diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 2035729c95..f346303562 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -2,15 +2,22 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyUnspeccedGetPostThreadV2, + type AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadHiddenV2, type ModerationDecision, } from '@atproto/api' export const postThreadQueryKeyRoot = 'getPostThreadV2' as const +export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const export const createPostThreadQueryKey = ( props: Pick, ) => [postThreadQueryKeyRoot, props] as const +export const createPostThreadHiddenQueryKey = ( + props: AppBskyUnspeccedGetPostThreadHiddenV2.QueryParams, +) => [postThreadHiddenQueryKeyRoot, props] as const + export type PostThreadParams = Pick< AppBskyUnspeccedGetPostThreadV2.QueryParams, 'sort' | 'prioritizeFollowedUsers' @@ -30,7 +37,7 @@ export type ThreadItem = key: string uri: string depth: number - value: Omit & { + value: Omit & { post: Omit & { record: AppBskyFeedPost.Record } @@ -51,21 +58,21 @@ export type ThreadItem = key: string uri: string depth: number - value: AppBskyUnspeccedGetPostThreadV2.ThreadItemNoUnauthenticated + value: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated } | { type: 'threadPostNotFound' key: string uri: string depth: number - value: AppBskyUnspeccedGetPostThreadV2.ThreadItemNotFound + value: AppBskyUnspeccedDefs.ThreadItemNotFound } | { type: 'threadPostBlocked' key: string uri: string depth: number - value: AppBskyUnspeccedGetPostThreadV2.ThreadItemBlocked + value: AppBskyUnspeccedDefs.ThreadItemBlocked } | { type: 'replyComposer' diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts index 2a0884e2eb..5af95bed8b 100644 --- a/src/state/queries/usePostThread/utils.ts +++ b/src/state/queries/usePostThread/utils.ts @@ -3,6 +3,7 @@ import { AppBskyFeedPost, AppBskyFeedThreadgate, AppBskyUnspeccedGetPostThreadV2, + AppBskyUnspeccedDefs, AtUri, } from '@atproto/api' @@ -46,12 +47,12 @@ export function getTraversalMetadata({ nextItem, parentMetadata, }: { - item: AppBskyUnspeccedGetPostThreadV2.ThreadItem - prevItem?: AppBskyUnspeccedGetPostThreadV2.ThreadItem - nextItem?: AppBskyUnspeccedGetPostThreadV2.ThreadItem + item: AppBskyUnspeccedDefs.ThreadItem + prevItem?: AppBskyUnspeccedDefs.ThreadItem + nextItem?: AppBskyUnspeccedDefs.ThreadItem parentMetadata?: TraversalMetadata }): TraversalMetadata { - if (!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { throw new Error(`Expected thread item to be a post`) } const repliesCount = item.value.post.replyCount || 0 diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts index 53734f0b0c..52e3944c53 100644 --- a/src/state/queries/usePostThread/views.ts +++ b/src/state/queries/usePostThread/views.ts @@ -2,7 +2,7 @@ import { type $Typed, type AppBskyFeedDefs, type AppBskyFeedPost, - type AppBskyUnspeccedGetPostThreadV2, + type AppBskyUnspeccedDefs, AtUri, moderatePost, type ModerationOpts, @@ -18,7 +18,7 @@ export function threadPostNoUnauthenticated({ uri, depth, value, -}: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract< +}: AppBskyUnspeccedDefs.ThreadItem): Extract< ThreadItem, {type: 'threadPostNoUnauthenticated'} > { @@ -27,7 +27,7 @@ export function threadPostNoUnauthenticated({ key: uri, uri, depth, - value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemNoUnauthenticated, + value: value as AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated, } } @@ -35,7 +35,7 @@ export function threadPostNotFound({ uri, depth, value, -}: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract< +}: AppBskyUnspeccedDefs.ThreadItem): Extract< ThreadItem, {type: 'threadPostNotFound'} > { @@ -44,7 +44,7 @@ export function threadPostNotFound({ key: uri, uri, depth, - value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemNotFound, + value: value as AppBskyUnspeccedDefs.ThreadItemNotFound, } } @@ -52,7 +52,7 @@ export function threadPostBlocked({ uri, depth, value, -}: AppBskyUnspeccedGetPostThreadV2.ThreadItem): Extract< +}: AppBskyUnspeccedDefs.ThreadItem): Extract< ThreadItem, {type: 'threadPostBlocked'} > { @@ -61,7 +61,7 @@ export function threadPostBlocked({ key: uri, uri, depth, - value: value as AppBskyUnspeccedGetPostThreadV2.ThreadItemBlocked, + value: value as AppBskyUnspeccedDefs.ThreadItemBlocked, } } @@ -73,7 +73,7 @@ export function threadPost({ }: { uri: string depth: number - value: $Typed + value: $Typed moderationOpts: ModerationOpts }): Extract { return { @@ -125,20 +125,22 @@ export function readMore({ export function postViewToThreadPlaceholder( post: AppBskyFeedDefs.PostView, ): $Typed< - Omit & { - value: $Typed + Omit & { + value: $Typed } > { return { - $type: 'app.bsky.unspecced.getPostThreadV2#threadItem', + $type: 'app.bsky.unspecced.defs#threadItem', uri: post.uri, depth: 0, // reset to 0 for highlighted post value: { - $type: 'app.bsky.unspecced.getPostThreadV2#threadItemPost', + $type: 'app.bsky.unspecced.defs#threadItemPost', post, opThread: false, moreParents: false, moreReplies: 0, + hiddenByThreadgate: false, + mutedByViewer: false, }, } } diff --git a/yarn.lock b/yarn.lock index 05c33f8b51..678081b094 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,10 +63,10 @@ "@atproto/xrpc" "^0.7.0" "@atproto/xrpc-server" "^0.7.18" -"@atproto/api@^0.15.10": - version "0.15.10" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.10.tgz#ff56f27e293f1916c1cc6b725da0e836f7a351e7" - integrity sha512-/PsvYoYMA6VGAbMEOU2rOuaNQHkWPU6CVQAUDK2XRlIgFO2d21KEjZsZ4Z3lELvZlcw25fuMp7gLgFRijpk78w== +"@atproto/api@^0.15.11": + version "0.15.11" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.11.tgz#f3f0718216c00bc405d35e0ce89ad30cedb2fb30" + integrity sha512-+XNOIqNPa1BWXzoi0mw6Qmx6kYlQPo60bhSrlxdhRYYH9CIgFAGmXrtb+MuAJoKgtSKX/2CBPihDsKEKEj8mfw== dependencies: "@atproto/common-web" "^0.4.2" "@atproto/lexicon" "^0.4.11"