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 = { /**