WIP hidden fetching

This commit is contained in:
Eric Bailey
2025-05-30 16:15:44 -05:00
parent 4ecaa951ee
commit 8289d625c7
8 changed files with 115 additions and 66 deletions
+1 -1
View File
@@ -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",
+39 -5
View File
@@ -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<ThreadItem[]>([])
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) {
+10 -9
View File
@@ -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<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
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<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
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<AppBskyUnspeccedGetPostThreadV2.ThreadItem> | void {
): $Typed<AppBskyUnspeccedDefs.ThreadItem> | void {
let partial
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
/*
@@ -178,8 +179,8 @@ export function* getThreadPlaceholderCandidates(
uri: string,
): Generator<
$Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
Omit<AppBskyUnspeccedDefs.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
}
>,
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
}
+31 -27
View File
@@ -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<string>
@@ -34,6 +35,7 @@ export function traverse(
view: PostThreadParams['view']
hasHiddenReplies: boolean
showHiddenReplies: boolean
skipHiddenReplyHandling?: boolean
loadHiddenReplies: () => Promise<void>
},
) {
@@ -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,
) {
+11 -4
View File
@@ -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<UsePostThreadProps, 'params'>,
) => [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<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost, 'post'> & {
value: Omit<AppBskyUnspeccedDefs.ThreadItemPost, 'post'> & {
post: Omit<AppBskyFeedDefs.PostView, 'record'> & {
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'
+5 -4
View File
@@ -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
+14 -12
View File
@@ -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<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
moderationOpts: ModerationOpts
}): Extract<ThreadItem, {type: 'threadPost'}> {
return {
@@ -125,20 +125,22 @@ export function readMore({
export function postViewToThreadPlaceholder(
post: AppBskyFeedDefs.PostView,
): $Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItemPost>
Omit<AppBskyUnspeccedDefs.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
}
> {
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,
},
}
}
+4 -4
View File
@@ -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"