From 45538dc0d82ad2bc6b05bbb0097023a6cb77472e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 31 May 2025 13:59:52 -0500 Subject: [PATCH] Update types --- src/screens/PostThread/index.tsx | 16 ++++- src/state/queries/usePostThread/index.ts | 63 ++++++++++++------- src/state/queries/usePostThread/queryCache.ts | 30 ++++----- src/state/queries/usePostThread/traversal.ts | 43 ++++--------- src/state/queries/usePostThread/types.ts | 6 +- src/state/queries/usePostThread/utils.ts | 9 +-- src/state/queries/usePostThread/views.ts | 21 +++---- src/view/com/composer/Composer.tsx | 4 +- src/view/com/post-thread/PostThread.tsx | 4 -- 9 files changed, 100 insertions(+), 96 deletions(-) diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index aa22532ede..f1117b0f4f 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -114,6 +114,11 @@ export function Inner({uri}: {uri: string | undefined}) { const anchorOffsetTop = anchorElement.getBoundingClientRect().top const headerHeight = headerElement.getBoundingClientRect().height const scrollPosition = anchorOffsetTop - headerHeight + console.log({ + anchorOffsetTop, + headerHeight, + scrollPosition, + }) /* * If scroll position is negative, it means the anchor post is above the * top of the screen, meaning the user scrolled the list. In that case, @@ -292,7 +297,16 @@ export function Inner({uri}: {uri: string | undefined}) { return ( item.onLoad()} + onPress={() => { + item.onLoad() + /* + * Bit of a hack. This resets the ref value for the anchor so that + * the next time `onContentSizeChangeWebOnly` fires, it won't + * adjust scroll. However, on the next render cycle, it will, which + * will give us time to insert the skeleton state and handle scroll. + */ + anchorRef.current = null + }} /> ) } else if (item.type === 'skeleton') { diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index b563e7b6bf..ee07a8819f 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -1,6 +1,7 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {useQuery, useQueryClient} from '@tanstack/react-query' +import {wait} from '#/lib/async/wait' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { createCacheMutator, @@ -8,8 +9,8 @@ import { } from '#/state/queries/usePostThread/queryCache' import {traverse} from '#/state/queries/usePostThread/traversal' import { - createPostThreadQueryKey, createPostThreadHiddenQueryKey, + createPostThreadQueryKey, type ThreadItem, type UsePostThreadProps, } from '#/state/queries/usePostThread/types' @@ -39,13 +40,16 @@ export function usePostThread({ queryKey, gcTime: 0, async queryFn() { - const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ - anchor: params.anchor!, - branchingFactor: params.view === 'linear' ? 1 : undefined, - below: 4, - sort: params.sort, - prioritizeFollowedUsers: params.prioritizeFollowedUsers, - }) + const {data} = await wait( + 400, + agent.app.bsky.unspecced.getPostThreadV2({ + anchor: params.anchor!, + branchingFactor: params.view === 'linear' ? 1 : undefined, + below: 4, + sort: params.sort, + prioritizeFollowedUsers: params.prioritizeFollowedUsers, + }), + ) return data }, placeholderData() { @@ -84,22 +88,29 @@ export function usePostThread({ const [hiddenReplies, setHiddenReplies] = useState([]) const loadHiddenReplies = useCallback(async () => { setShowHiddenReplies(true) - setHiddenReplies(Array.from({length: 2}).map((_, i) => ({ - type: 'skeleton', - key: `${params.anchor!}-reply-${i}`, - item: 'reply', - }))) + 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 data = await wait( + 400, + 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, @@ -113,7 +124,17 @@ export function usePostThread({ loadHiddenReplies, }) setHiddenReplies(items) - }, [params, setShowHiddenReplies]) + }, [ + agent, + params, + hasSession, + mergeThreadgateHiddenReplies, + moderationOpts, + qc, + query.data?.threadgate?.record, + showHiddenReplies, + setShowHiddenReplies, + ]) const items = useMemo(() => { const results = traverse(query.data?.thread || [], { diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index 5ff8e3fa71..95219ccf74 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -1,8 +1,8 @@ import { type $Typed, type AppBskyFeedDefs, - AppBskyUnspeccedGetPostThreadV2, AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, AtUri, } from '@atproto/api' import {type QueryClient} from '@tanstack/react-query' @@ -35,7 +35,7 @@ export function createCacheMutator({ return { insertReplies( parentUri: string, - replies: AppBskyUnspeccedDefs.ThreadItem[], + replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], ) { queryClient.setQueryData( queryKey, @@ -46,11 +46,7 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const existingParent = thread[i] - if ( - !AppBskyUnspeccedDefs.isThreadItemPost( - existingParent.value, - ) - ) + if (!AppBskyUnspeccedDefs.isThreadItemPost(existingParent.value)) continue if (existingParent.uri !== parentUri) continue @@ -68,12 +64,11 @@ export function createCacheMutator({ const isEndOfReplyChain = !nextItem || nextItem.depth <= existingParent.depth const firstReply = replies.at(0) - const opIsReplier = - AppBskyUnspeccedDefs.isThreadItemPost( - firstReply?.value, - ) - ? opDid === firstReply.value.post.author.did - : false + const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost( + firstReply?.value, + ) + ? opDid === firstReply.value.post.author.did + : false /* * Always insert replies if the following conditions are met. @@ -119,7 +114,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: AppBskyUnspeccedDefs.ThreadItem) { + deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) { queryClient.setQueryData( queryKey, queryData => { @@ -129,8 +124,7 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const existingPost = thread[i] - if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) - continue + if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue if (existingPost.uri === post.uri) { const branch = getBranch(thread, i, existingPost.depth) @@ -152,7 +146,7 @@ export function createCacheMutator({ export function getThreadPlaceholder( queryClient: QueryClient, uri: string, -): $Typed | void { +): $Typed | void { let partial for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { /* @@ -179,7 +173,7 @@ export function* getThreadPlaceholderCandidates( uri: string, ): Generator< $Typed< - Omit & { + Omit & { value: $Typed } >, diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 0a77c9b652..87add9d885 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -5,6 +5,7 @@ import { } from '@atproto/api' import { + type ApiThreadItem, type PostThreadParams, type ThreadItem, type TraversalMetadata, @@ -18,7 +19,7 @@ import { import * as views from '#/state/queries/usePostThread/views' export function traverse( - thread: AppBskyUnspeccedDefs.ThreadItem[], + thread: ApiThreadItem[], { threadgateHiddenReplies, moderationOpts, @@ -67,19 +68,11 @@ export function traverse( * _up_ from there. */ } else if (item.depth === 0) { - if ( - AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( - item.value, - ) - ) { + if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) { items.push(views.threadPostNoUnauthenticated(item)) - } else if ( - AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) - ) { + } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) { items.push(views.threadPostNotFound(item)) - } else if ( - AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) - ) { + } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) { items.push(views.threadPostBlocked(item)) } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { const post = views.threadPost({ @@ -94,25 +87,17 @@ export function traverse( const parent = thread[pi] if ( - AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( - parent.value, - ) + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value) ) { items.unshift(views.threadPostNoUnauthenticated(parent)) break parentTraversal - } else if ( - AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value) - ) { + } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) { items.unshift(views.threadPostNotFound(parent)) break parentTraversal - } else if ( - AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value) - ) { + } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) { items.unshift(views.threadPostBlocked(parent)) break parentTraversal - } else if ( - AppBskyUnspeccedDefs.isThreadItemPost(parent.value) - ) { + } else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) { items.unshift( views.threadPost({ uri: parent.uri, @@ -131,9 +116,7 @@ export function traverse( * we could. */ const shouldBreak = - AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( - item.value, - ) || + AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) || AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) @@ -193,9 +176,7 @@ export function traverse( for (let ci = startIndex; ci <= branch.end; ci++) { const child = thread[ci] - if ( - AppBskyUnspeccedDefs.isThreadItemPost(child.value) - ) { + if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) { const childParentMetadata = metadatas.get( getPostRecord(child.value.post).reply?.parent?.uri || '', ) @@ -432,7 +413,7 @@ export function traverse( * const { start: 1, end: 3 } = getBranch(items, 1, 1) */ export function getBranch( - thread: AppBskyUnspeccedDefs.ThreadItem[], + thread: ApiThreadItem[], branchStartIndex: number, branchStartDepth: number, ) { diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index f346303562..3afbad3c1a 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -1,12 +1,16 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, - type AppBskyUnspeccedGetPostThreadV2, type AppBskyUnspeccedDefs, type AppBskyUnspeccedGetPostThreadHiddenV2, + type AppBskyUnspeccedGetPostThreadV2, type ModerationDecision, } from '@atproto/api' +export type ApiThreadItem = + | AppBskyUnspeccedGetPostThreadV2.ThreadItem + | AppBskyUnspeccedGetPostThreadHiddenV2.ThreadHiddenItem + export const postThreadQueryKeyRoot = 'getPostThreadV2' as const export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts index 5af95bed8b..3b2e6d8c14 100644 --- a/src/state/queries/usePostThread/utils.ts +++ b/src/state/queries/usePostThread/utils.ts @@ -2,12 +2,13 @@ import { type AppBskyFeedDefs, AppBskyFeedPost, AppBskyFeedThreadgate, - AppBskyUnspeccedGetPostThreadV2, AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, AtUri, } from '@atproto/api' import { + type ApiThreadItem, type ThreadItem, type TraversalMetadata, } from '#/state/queries/usePostThread/types' @@ -47,9 +48,9 @@ export function getTraversalMetadata({ nextItem, parentMetadata, }: { - item: AppBskyUnspeccedDefs.ThreadItem - prevItem?: AppBskyUnspeccedDefs.ThreadItem - nextItem?: AppBskyUnspeccedDefs.ThreadItem + item: ApiThreadItem + prevItem?: ApiThreadItem + nextItem?: ApiThreadItem parentMetadata?: TraversalMetadata }): TraversalMetadata { if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts index 52e3944c53..3006425e46 100644 --- a/src/state/queries/usePostThread/views.ts +++ b/src/state/queries/usePostThread/views.ts @@ -3,6 +3,7 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, AtUri, moderatePost, type ModerationOpts, @@ -10,6 +11,7 @@ import { import {makeProfileLink} from '#/lib/routes/links' import { + type ApiThreadItem, type ThreadItem, type TraversalMetadata, } from '#/state/queries/usePostThread/types' @@ -18,10 +20,7 @@ export function threadPostNoUnauthenticated({ uri, depth, value, -}: AppBskyUnspeccedDefs.ThreadItem): Extract< - ThreadItem, - {type: 'threadPostNoUnauthenticated'} -> { +}: ApiThreadItem): Extract { return { type: 'threadPostNoUnauthenticated', key: uri, @@ -35,10 +34,7 @@ export function threadPostNotFound({ uri, depth, value, -}: AppBskyUnspeccedDefs.ThreadItem): Extract< - ThreadItem, - {type: 'threadPostNotFound'} -> { +}: ApiThreadItem): Extract { return { type: 'threadPostNotFound', key: uri, @@ -52,10 +48,7 @@ export function threadPostBlocked({ uri, depth, value, -}: AppBskyUnspeccedDefs.ThreadItem): Extract< - ThreadItem, - {type: 'threadPostBlocked'} -> { +}: ApiThreadItem): Extract { return { type: 'threadPostBlocked', key: uri, @@ -125,12 +118,12 @@ export function readMore({ export function postViewToThreadPlaceholder( post: AppBskyFeedDefs.PostView, ): $Typed< - Omit & { + Omit & { value: $Typed } > { return { - $type: 'app.bsky.unspecced.defs#threadItem', + $type: 'app.bsky.unspecced.getPostThreadV2#threadItem', uri: post.uri, depth: 0, // reset to 0 for highlighted post value: { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 67e6c08437..f5b29664ab 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -45,7 +45,7 @@ import {type ImagePickerAsset} from 'expo-image-picker' import { AppBskyFeedDefs, type AppBskyFeedGetPostThread, - AppBskyUnspeccedGetPostThreadV2, + AppBskyUnspeccedDefs, type BskyAgent, type RichText, } from '@atproto/api' @@ -427,7 +427,7 @@ export const ComposePost = ({ } if ( !res.data.thread.every(p => - AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(p.value), + AppBskyUnspeccedDefs.isThreadItemPost(p.value), ) ) { throw new Error(`composer: app view returned non-post items`) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 2eca26d409..fb16de20d8 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -326,8 +326,6 @@ export function PostThread({uri}: {uri: string | undefined}) { return arr }, [skeleton, deferParents, maxParents, maxReplies]) - console.log({anchorIndex: posts.findIndex(p => p.ctx?.isHighlightedPost)}) - // This is only used on the web to keep the post in view when its parents load. // On native, we rely on `maintainVisibleContentPosition` instead. const didAdjustScrollWeb = useRef(false) @@ -423,8 +421,6 @@ export function PostThread({uri}: {uri: string | undefined}) { (skeleton.highlightedPost.ctx.isParentLoading || Boolean(skeleton?.parents && skeleton.parents.length > 0)) - console.log({hasParents}) - const renderItem = ({item, index}: {item: RowItem; index: number}) => { if (item === REPLY_PROMPT && hasSession) { return (