Update types

This commit is contained in:
Eric Bailey
2025-05-31 13:59:52 -05:00
parent 8289d625c7
commit 45538dc0d8
9 changed files with 100 additions and 96 deletions
+15 -1
View File
@@ -114,6 +114,11 @@ export function Inner({uri}: {uri: string | undefined}) {
const anchorOffsetTop = anchorElement.getBoundingClientRect().top const anchorOffsetTop = anchorElement.getBoundingClientRect().top
const headerHeight = headerElement.getBoundingClientRect().height const headerHeight = headerElement.getBoundingClientRect().height
const scrollPosition = anchorOffsetTop - headerHeight const scrollPosition = anchorOffsetTop - headerHeight
console.log({
anchorOffsetTop,
headerHeight,
scrollPosition,
})
/* /*
* If scroll position is negative, it means the anchor post is above the * 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, * 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 ( return (
<PostThreadShowHiddenReplies <PostThreadShowHiddenReplies
type="hidden" type="hidden"
onPress={() => 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') { } else if (item.type === 'skeleton') {
+42 -21
View File
@@ -1,6 +1,7 @@
import {useCallback, useMemo, useRef, useState} from 'react' import {useCallback, useMemo, useRef, useState} from 'react'
import {useQuery, useQueryClient} from '@tanstack/react-query' import {useQuery, useQueryClient} from '@tanstack/react-query'
import {wait} from '#/lib/async/wait'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
createCacheMutator, createCacheMutator,
@@ -8,8 +9,8 @@ import {
} from '#/state/queries/usePostThread/queryCache' } from '#/state/queries/usePostThread/queryCache'
import {traverse} from '#/state/queries/usePostThread/traversal' import {traverse} from '#/state/queries/usePostThread/traversal'
import { import {
createPostThreadQueryKey,
createPostThreadHiddenQueryKey, createPostThreadHiddenQueryKey,
createPostThreadQueryKey,
type ThreadItem, type ThreadItem,
type UsePostThreadProps, type UsePostThreadProps,
} from '#/state/queries/usePostThread/types' } from '#/state/queries/usePostThread/types'
@@ -39,13 +40,16 @@ export function usePostThread({
queryKey, queryKey,
gcTime: 0, gcTime: 0,
async queryFn() { async queryFn() {
const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ const {data} = await wait(
anchor: params.anchor!, 400,
branchingFactor: params.view === 'linear' ? 1 : undefined, agent.app.bsky.unspecced.getPostThreadV2({
below: 4, anchor: params.anchor!,
sort: params.sort, branchingFactor: params.view === 'linear' ? 1 : undefined,
prioritizeFollowedUsers: params.prioritizeFollowedUsers, below: 4,
}) sort: params.sort,
prioritizeFollowedUsers: params.prioritizeFollowedUsers,
}),
)
return data return data
}, },
placeholderData() { placeholderData() {
@@ -84,22 +88,29 @@ export function usePostThread({
const [hiddenReplies, setHiddenReplies] = useState<ThreadItem[]>([]) const [hiddenReplies, setHiddenReplies] = useState<ThreadItem[]>([])
const loadHiddenReplies = useCallback(async () => { const loadHiddenReplies = useCallback(async () => {
setShowHiddenReplies(true) setShowHiddenReplies(true)
setHiddenReplies(Array.from({length: 2}).map((_, i) => ({ setHiddenReplies(
type: 'skeleton', Array.from({length: 2}).map((_, i) => ({
key: `${params.anchor!}-reply-${i}`, type: 'skeleton',
item: 'reply', key: `${params.anchor!}-reply-${i}`,
}))) item: 'reply',
})),
)
const queryParams = { const queryParams = {
anchor: params.anchor!, anchor: params.anchor!,
prioritizeFollowedUsers: params.prioritizeFollowedUsers, prioritizeFollowedUsers: params.prioritizeFollowedUsers,
} }
const data = await qc.fetchQuery({ const data = await wait(
queryKey: createPostThreadHiddenQueryKey(queryParams), 400,
async queryFn() { qc.fetchQuery({
const {data} = await agent.app.bsky.unspecced.getPostThreadHiddenV2(queryParams) queryKey: createPostThreadHiddenQueryKey(queryParams),
return data.thread || [] async queryFn() {
}, const {data} = await agent.app.bsky.unspecced.getPostThreadHiddenV2(
}) queryParams,
)
return data.thread || []
},
}),
)
const items = traverse(data || [], { const items = traverse(data || [], {
threadgateHiddenReplies: mergeThreadgateHiddenReplies( threadgateHiddenReplies: mergeThreadgateHiddenReplies(
query.data?.threadgate?.record, query.data?.threadgate?.record,
@@ -113,7 +124,17 @@ export function usePostThread({
loadHiddenReplies, loadHiddenReplies,
}) })
setHiddenReplies(items) setHiddenReplies(items)
}, [params, setShowHiddenReplies]) }, [
agent,
params,
hasSession,
mergeThreadgateHiddenReplies,
moderationOpts,
qc,
query.data?.threadgate?.record,
showHiddenReplies,
setShowHiddenReplies,
])
const items = useMemo(() => { const items = useMemo(() => {
const results = traverse(query.data?.thread || [], { const results = traverse(query.data?.thread || [], {
+12 -18
View File
@@ -1,8 +1,8 @@
import { import {
type $Typed, type $Typed,
type AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyUnspeccedGetPostThreadV2,
AppBskyUnspeccedDefs, AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri, AtUri,
} from '@atproto/api' } from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
@@ -35,7 +35,7 @@ export function createCacheMutator({
return { return {
insertReplies( insertReplies(
parentUri: string, parentUri: string,
replies: AppBskyUnspeccedDefs.ThreadItem[], replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[],
) { ) {
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
queryKey, queryKey,
@@ -46,11 +46,7 @@ export function createCacheMutator({
for (let i = 0; i < thread.length; i++) { for (let i = 0; i < thread.length; i++) {
const existingParent = thread[i] const existingParent = thread[i]
if ( if (!AppBskyUnspeccedDefs.isThreadItemPost(existingParent.value))
!AppBskyUnspeccedDefs.isThreadItemPost(
existingParent.value,
)
)
continue continue
if (existingParent.uri !== parentUri) continue if (existingParent.uri !== parentUri) continue
@@ -68,12 +64,11 @@ export function createCacheMutator({
const isEndOfReplyChain = const isEndOfReplyChain =
!nextItem || nextItem.depth <= existingParent.depth !nextItem || nextItem.depth <= existingParent.depth
const firstReply = replies.at(0) const firstReply = replies.at(0)
const opIsReplier = const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost(
AppBskyUnspeccedDefs.isThreadItemPost( firstReply?.value,
firstReply?.value, )
) ? opDid === firstReply.value.post.author.did
? opDid === firstReply.value.post.author.did : false
: false
/* /*
* Always insert replies if the following conditions are met. * 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 * Unused atm, post shadow does the trick, but it would be nice to clean up
* the whole sub-tree on deletes. * the whole sub-tree on deletes.
*/ */
deletePost(post: AppBskyUnspeccedDefs.ThreadItem) { deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) {
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
queryKey, queryKey,
queryData => { queryData => {
@@ -129,8 +124,7 @@ export function createCacheMutator({
for (let i = 0; i < thread.length; i++) { for (let i = 0; i < thread.length; i++) {
const existingPost = thread[i] const existingPost = thread[i]
if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue
continue
if (existingPost.uri === post.uri) { if (existingPost.uri === post.uri) {
const branch = getBranch(thread, i, existingPost.depth) const branch = getBranch(thread, i, existingPost.depth)
@@ -152,7 +146,7 @@ export function createCacheMutator({
export function getThreadPlaceholder( export function getThreadPlaceholder(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): $Typed<AppBskyUnspeccedDefs.ThreadItem> | void { ): $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItem> | void {
let partial let partial
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
/* /*
@@ -179,7 +173,7 @@ export function* getThreadPlaceholderCandidates(
uri: string, uri: string,
): Generator< ): Generator<
$Typed< $Typed<
Omit<AppBskyUnspeccedDefs.ThreadItem, 'value'> & { Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
} }
>, >,
+12 -31
View File
@@ -5,6 +5,7 @@ import {
} from '@atproto/api' } from '@atproto/api'
import { import {
type ApiThreadItem,
type PostThreadParams, type PostThreadParams,
type ThreadItem, type ThreadItem,
type TraversalMetadata, type TraversalMetadata,
@@ -18,7 +19,7 @@ import {
import * as views from '#/state/queries/usePostThread/views' import * as views from '#/state/queries/usePostThread/views'
export function traverse( export function traverse(
thread: AppBskyUnspeccedDefs.ThreadItem[], thread: ApiThreadItem[],
{ {
threadgateHiddenReplies, threadgateHiddenReplies,
moderationOpts, moderationOpts,
@@ -67,19 +68,11 @@ export function traverse(
* _up_ from there. * _up_ from there.
*/ */
} else if (item.depth === 0) { } else if (item.depth === 0) {
if ( if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) {
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(
item.value,
)
) {
items.push(views.threadPostNoUnauthenticated(item)) items.push(views.threadPostNoUnauthenticated(item))
} else if ( } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) {
AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)
) {
items.push(views.threadPostNotFound(item)) items.push(views.threadPostNotFound(item))
} else if ( } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) {
AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)
) {
items.push(views.threadPostBlocked(item)) items.push(views.threadPostBlocked(item))
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
const post = views.threadPost({ const post = views.threadPost({
@@ -94,25 +87,17 @@ export function traverse(
const parent = thread[pi] const parent = thread[pi]
if ( if (
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value)
parent.value,
)
) { ) {
items.unshift(views.threadPostNoUnauthenticated(parent)) items.unshift(views.threadPostNoUnauthenticated(parent))
break parentTraversal break parentTraversal
} else if ( } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) {
AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)
) {
items.unshift(views.threadPostNotFound(parent)) items.unshift(views.threadPostNotFound(parent))
break parentTraversal break parentTraversal
} else if ( } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) {
AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)
) {
items.unshift(views.threadPostBlocked(parent)) items.unshift(views.threadPostBlocked(parent))
break parentTraversal break parentTraversal
} else if ( } else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) {
AppBskyUnspeccedDefs.isThreadItemPost(parent.value)
) {
items.unshift( items.unshift(
views.threadPost({ views.threadPost({
uri: parent.uri, uri: parent.uri,
@@ -131,9 +116,7 @@ export function traverse(
* we could. * we could.
*/ */
const shouldBreak = const shouldBreak =
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated( AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) ||
item.value,
) ||
AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) ||
AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)
@@ -193,9 +176,7 @@ export function traverse(
for (let ci = startIndex; ci <= branch.end; ci++) { for (let ci = startIndex; ci <= branch.end; ci++) {
const child = thread[ci] const child = thread[ci]
if ( if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) {
AppBskyUnspeccedDefs.isThreadItemPost(child.value)
) {
const childParentMetadata = metadatas.get( const childParentMetadata = metadatas.get(
getPostRecord(child.value.post).reply?.parent?.uri || '', getPostRecord(child.value.post).reply?.parent?.uri || '',
) )
@@ -432,7 +413,7 @@ export function traverse(
* const { start: 1, end: 3 } = getBranch(items, 1, 1) * const { start: 1, end: 3 } = getBranch(items, 1, 1)
*/ */
export function getBranch( export function getBranch(
thread: AppBskyUnspeccedDefs.ThreadItem[], thread: ApiThreadItem[],
branchStartIndex: number, branchStartIndex: number,
branchStartDepth: number, branchStartDepth: number,
) { ) {
+5 -1
View File
@@ -1,12 +1,16 @@
import { import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyUnspeccedGetPostThreadV2,
type AppBskyUnspeccedDefs, type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadHiddenV2, type AppBskyUnspeccedGetPostThreadHiddenV2,
type AppBskyUnspeccedGetPostThreadV2,
type ModerationDecision, type ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
export type ApiThreadItem =
| AppBskyUnspeccedGetPostThreadV2.ThreadItem
| AppBskyUnspeccedGetPostThreadHiddenV2.ThreadHiddenItem
export const postThreadQueryKeyRoot = 'getPostThreadV2' as const export const postThreadQueryKeyRoot = 'getPostThreadV2' as const
export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const export const postThreadHiddenQueryKeyRoot = 'getPostThreadHiddenV2' as const
+5 -4
View File
@@ -2,12 +2,13 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AppBskyUnspeccedGetPostThreadV2,
AppBskyUnspeccedDefs, AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri, AtUri,
} from '@atproto/api' } from '@atproto/api'
import { import {
type ApiThreadItem,
type ThreadItem, type ThreadItem,
type TraversalMetadata, type TraversalMetadata,
} from '#/state/queries/usePostThread/types' } from '#/state/queries/usePostThread/types'
@@ -47,9 +48,9 @@ export function getTraversalMetadata({
nextItem, nextItem,
parentMetadata, parentMetadata,
}: { }: {
item: AppBskyUnspeccedDefs.ThreadItem item: ApiThreadItem
prevItem?: AppBskyUnspeccedDefs.ThreadItem prevItem?: ApiThreadItem
nextItem?: AppBskyUnspeccedDefs.ThreadItem nextItem?: ApiThreadItem
parentMetadata?: TraversalMetadata parentMetadata?: TraversalMetadata
}): TraversalMetadata { }): TraversalMetadata {
if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
+7 -14
View File
@@ -3,6 +3,7 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyUnspeccedDefs, type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri, AtUri,
moderatePost, moderatePost,
type ModerationOpts, type ModerationOpts,
@@ -10,6 +11,7 @@ import {
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import { import {
type ApiThreadItem,
type ThreadItem, type ThreadItem,
type TraversalMetadata, type TraversalMetadata,
} from '#/state/queries/usePostThread/types' } from '#/state/queries/usePostThread/types'
@@ -18,10 +20,7 @@ export function threadPostNoUnauthenticated({
uri, uri,
depth, depth,
value, value,
}: AppBskyUnspeccedDefs.ThreadItem): Extract< }: ApiThreadItem): Extract<ThreadItem, {type: 'threadPostNoUnauthenticated'}> {
ThreadItem,
{type: 'threadPostNoUnauthenticated'}
> {
return { return {
type: 'threadPostNoUnauthenticated', type: 'threadPostNoUnauthenticated',
key: uri, key: uri,
@@ -35,10 +34,7 @@ export function threadPostNotFound({
uri, uri,
depth, depth,
value, value,
}: AppBskyUnspeccedDefs.ThreadItem): Extract< }: ApiThreadItem): Extract<ThreadItem, {type: 'threadPostNotFound'}> {
ThreadItem,
{type: 'threadPostNotFound'}
> {
return { return {
type: 'threadPostNotFound', type: 'threadPostNotFound',
key: uri, key: uri,
@@ -52,10 +48,7 @@ export function threadPostBlocked({
uri, uri,
depth, depth,
value, value,
}: AppBskyUnspeccedDefs.ThreadItem): Extract< }: ApiThreadItem): Extract<ThreadItem, {type: 'threadPostBlocked'}> {
ThreadItem,
{type: 'threadPostBlocked'}
> {
return { return {
type: 'threadPostBlocked', type: 'threadPostBlocked',
key: uri, key: uri,
@@ -125,12 +118,12 @@ export function readMore({
export function postViewToThreadPlaceholder( export function postViewToThreadPlaceholder(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
): $Typed< ): $Typed<
Omit<AppBskyUnspeccedDefs.ThreadItem, 'value'> & { Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
} }
> { > {
return { return {
$type: 'app.bsky.unspecced.defs#threadItem', $type: 'app.bsky.unspecced.getPostThreadV2#threadItem',
uri: post.uri, uri: post.uri,
depth: 0, // reset to 0 for highlighted post depth: 0, // reset to 0 for highlighted post
value: { value: {
+2 -2
View File
@@ -45,7 +45,7 @@ import {type ImagePickerAsset} from 'expo-image-picker'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
type AppBskyFeedGetPostThread, type AppBskyFeedGetPostThread,
AppBskyUnspeccedGetPostThreadV2, AppBskyUnspeccedDefs,
type BskyAgent, type BskyAgent,
type RichText, type RichText,
} from '@atproto/api' } from '@atproto/api'
@@ -427,7 +427,7 @@ export const ComposePost = ({
} }
if ( if (
!res.data.thread.every(p => !res.data.thread.every(p =>
AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(p.value), AppBskyUnspeccedDefs.isThreadItemPost(p.value),
) )
) { ) {
throw new Error(`composer: app view returned non-post items`) throw new Error(`composer: app view returned non-post items`)
-4
View File
@@ -326,8 +326,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
return arr return arr
}, [skeleton, deferParents, maxParents, maxReplies]) }, [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. // This is only used on the web to keep the post in view when its parents load.
// On native, we rely on `maintainVisibleContentPosition` instead. // On native, we rely on `maintainVisibleContentPosition` instead.
const didAdjustScrollWeb = useRef<boolean>(false) const didAdjustScrollWeb = useRef<boolean>(false)
@@ -423,8 +421,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
(skeleton.highlightedPost.ctx.isParentLoading || (skeleton.highlightedPost.ctx.isParentLoading ||
Boolean(skeleton?.parents && skeleton.parents.length > 0)) Boolean(skeleton?.parents && skeleton.parents.length > 0))
console.log({hasParents})
const renderItem = ({item, index}: {item: RowItem; index: number}) => { const renderItem = ({item, index}: {item: RowItem; index: number}) => {
if (item === REPLY_PROMPT && hasSession) { if (item === REPLY_PROMPT && hasSession) {
return ( return (