diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx
new file mode 100644
index 0000000000..11cd63d0e8
--- /dev/null
+++ b/src/components/Skeleton.tsx
@@ -0,0 +1,102 @@
+import {View} from 'react-native'
+
+import {
+ atoms as a,
+ flatten,
+ type TextStyleProp,
+ useAlf,
+ useTheme,
+ type ViewStyleProp,
+} from '#/alf'
+import {normalizeTextStyles} from '#/alf/typography'
+
+type SkeletonProps = {
+ blend?: boolean
+}
+
+export function Text({blend, style}: TextStyleProp & SkeletonProps) {
+ const {fonts, flags, theme: t} = useAlf()
+ const {width, ...flattened} = flatten(style)
+ const {lineHeight = 14, ...rest} = normalizeTextStyles(
+ [a.text_sm, a.leading_snug, flattened],
+ {
+ fontScale: fonts.scaleMultiplier,
+ fontFamily: fonts.family,
+ flags,
+ },
+ )
+ return (
+
+
+
+ )
+}
+
+export function Circle({
+ size,
+ blend,
+ style,
+}: ViewStyleProp & {size: number} & SkeletonProps) {
+ const t = useTheme()
+ return (
+
+ )
+}
+
+export function Pill({
+ size,
+ blend,
+ style,
+}: ViewStyleProp & {size: number} & SkeletonProps) {
+ const t = useTheme()
+ return (
+
+ )
+}
+
+export function Col({
+ children,
+ style,
+}: ViewStyleProp & {children?: React.ReactNode}) {
+ return {children}
+}
+
+export function Row({
+ children,
+ style,
+}: ViewStyleProp & {children?: React.ReactNode}) {
+ return {children}
+}
diff --git a/src/screens/PostThread/components/ThreadAnchor.tsx b/src/screens/PostThread/components/ThreadAnchor.tsx
index 096c0d0dbe..d72b8fd19c 100644
--- a/src/screens/PostThread/components/ThreadAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadAnchor.tsx
@@ -50,6 +50,7 @@ import {type AppModerationCause} from '#/components/Pills'
import {PostControls} from '#/components/PostControls'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
+import * as Skele from '#/components/Skeleton'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {WhoCanReply} from '#/components/WhoCanReply'
@@ -574,3 +575,33 @@ function getThreadAuthor(
return ''
}
}
+
+export function ThreadAnchorSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/PostThread/components/ThreadPost.tsx b/src/screens/PostThread/components/ThreadPost.tsx
index e43964bd69..55b1105a53 100644
--- a/src/screens/PostThread/components/ThreadPost.tsx
+++ b/src/screens/PostThread/components/ThreadPost.tsx
@@ -42,6 +42,7 @@ import {PostHider} from '#/components/moderation/PostHider'
import {type AppModerationCause} from '#/components/Pills'
import {PostControls} from '#/components/PostControls'
import {RichText} from '#/components/RichText'
+import * as Skele from '#/components/Skeleton'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {Text} from '#/components/Typography'
@@ -320,3 +321,40 @@ function SubtleHover({children}: {children: React.ReactNode}) {
)
}
+
+export function ThreadPostSkeleton() {
+ const t = useTheme()
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index 20e40d7c57..62b76fe567 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -20,8 +20,14 @@ import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShow
import {List, type ListMethods} from '#/view/com/util/List'
import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
import {ReadMore} from '#/screens/PostThread/components/ReadMore'
-import {ThreadAnchor} from '#/screens/PostThread/components/ThreadAnchor'
-import {ThreadPost} from '#/screens/PostThread/components/ThreadPost'
+import {
+ ThreadAnchor,
+ ThreadAnchorSkeleton,
+} from '#/screens/PostThread/components/ThreadAnchor'
+import {
+ ThreadPost,
+ ThreadPostSkeleton,
+} from '#/screens/PostThread/components/ThreadPost'
import {ThreadReply} from '#/screens/PostThread/components/ThreadReply'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import * as Layout from '#/components/Layout'
@@ -305,6 +311,12 @@ export function Inner({uri}: {uri: string | undefined}) {
}
/>
)
+ } else if (item.type === 'skeleton') {
+ if (item.item === 'anchor') {
+ return
+ } else if (item.item === 'reply') {
+ return
+ }
}
return null
}
@@ -358,7 +370,7 @@ export function Inner({uri}: {uri: string | undefined}) {
* Using `isFetching` over `isFetchingNextPage` is done on
* purpose here so we get the loader on initial render
*/
- isFetchingNextPage={isFetching}
+ // isFetchingNextPage={isFetching}
error={cleanError(error)}
onRetry={refetch}
/*
diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts
index 933881251e..7c72e91b4e 100644
--- a/src/state/queries/usePostThread/index.ts
+++ b/src/state/queries/usePostThread/index.ts
@@ -54,7 +54,7 @@ export function usePostThread({
if (placeholder) {
return {thread: [placeholder], hasHiddenReplies: false}
}
- return
+ return {thread: [], hasHiddenReplies: false}
},
select(data) {
const threadgate = getThreadgateRecord(data.threadgate)
@@ -96,6 +96,38 @@ export function usePostThread({
queryClient: qc,
})
+ if (query.isPlaceholderData) {
+ const anchor = items.at(0)
+ const skeletonReplies =
+ anchor && anchor.type === 'threadPost'
+ ? anchor?.value.post.replyCount ?? 4
+ : 4
+
+ if (!items.length) {
+ items.push({
+ type: 'skeleton',
+ key: params.anchor!,
+ item: 'anchor',
+ })
+
+ if (hasSession) {
+ items.push({
+ type: 'skeleton',
+ key: 'replyComposer',
+ item: 'replyComposer',
+ })
+ }
+ }
+
+ for (let i = 0; i < skeletonReplies; i++) {
+ items.push({
+ type: 'skeleton',
+ key: `${params.anchor!}-reply-${i}`,
+ item: 'reply',
+ })
+ }
+ }
+
return {
...query,
data: {
diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts
index 59772432ba..f849543175 100644
--- a/src/state/queries/usePostThread/types.ts
+++ b/src/state/queries/usePostThread/types.ts
@@ -92,6 +92,11 @@ export type ThreadItem =
moreReplies: number
skippedIndentIndices: Set
}
+ | {
+ type: 'skeleton'
+ key: string
+ item: 'anchor' | 'reply' | 'replyComposer'
+ }
export type TraversalMetadata = {
/**