Add basic post and anchor skeletons

This commit is contained in:
Eric Bailey
2025-05-30 11:19:48 -05:00
parent d0170c59ea
commit cffdfbe0be
6 changed files with 224 additions and 4 deletions
+102
View File
@@ -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 (
<View
style={[a.flex_1, {maxWidth: width, paddingVertical: lineHeight * 0.15}]}>
<View
style={[
a.rounded_md,
t.atoms.bg_contrast_25,
{
height: lineHeight * 0.7,
opacity: blend ? 0.6 : 1,
},
rest,
]}
/>
</View>
)
}
export function Circle({
size,
blend,
style,
}: ViewStyleProp & {size: number} & SkeletonProps) {
const t = useTheme()
return (
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_25,
{
width: size,
height: size,
opacity: blend ? 0.6 : 1,
},
style,
]}
/>
)
}
export function Pill({
size,
blend,
style,
}: ViewStyleProp & {size: number} & SkeletonProps) {
const t = useTheme()
return (
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_25,
{
width: size * 1.618,
height: size,
opacity: blend ? 0.6 : 1,
},
style,
]}
/>
)
}
export function Col({
children,
style,
}: ViewStyleProp & {children?: React.ReactNode}) {
return <View style={[a.flex_1, style]}>{children}</View>
}
export function Row({
children,
style,
}: ViewStyleProp & {children?: React.ReactNode}) {
return <View style={[a.flex_row, style]}>{children}</View>
}
@@ -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 (
<View style={[a.p_lg, a.gap_md]}>
<Skele.Row style={[a.align_center, a.gap_md]}>
<Skele.Circle size={42} />
<Skele.Col>
<Skele.Text style={[a.text_lg, {width: '20%'}]} />
<Skele.Text blend style={[a.text_md, {width: '40%'}]} />
</Skele.Col>
</Skele.Row>
<View>
<Skele.Text style={[a.text_xl, {width: '100%'}]} />
<Skele.Text style={[a.text_xl, {width: '60%'}]} />
</View>
<Skele.Text style={[a.text_sm, {width: '50%'}]} />
<Skele.Row style={[a.justify_between]}>
<Skele.Pill blend size={24} />
<Skele.Pill blend size={24} />
<Skele.Pill blend size={24} />
<Skele.Circle blend size={24} />
<Skele.Circle blend size={24} />
</Skele.Row>
</View>
)
}
@@ -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}) {
</View>
)
}
export function ThreadPostSkeleton() {
const t = useTheme()
return (
<View
style={[
{paddingHorizontal: OUTER_SPACE, paddingVertical: OUTER_SPACE / 1.5},
a.gap_md,
a.border_t,
t.atoms.border_contrast_low,
]}>
<Skele.Row style={[a.align_start, a.gap_md]}>
<Skele.Circle size={42} />
<Skele.Col style={[a.gap_xs]}>
<Skele.Row style={[a.gap_sm]}>
<Skele.Text style={[a.text_md, {width: '20%'}]} />
<Skele.Text blend style={[a.text_md, {width: '30%'}]} />
</Skele.Row>
<Skele.Col>
<Skele.Text blend style={[a.text_md, {width: '100%'}]} />
<Skele.Text blend style={[a.text_md, {width: '60%'}]} />
</Skele.Col>
<Skele.Row style={[a.justify_between, a.pt_xs]}>
<Skele.Pill blend size={16} />
<Skele.Pill blend size={16} />
<Skele.Pill blend size={16} />
<Skele.Circle blend size={16} />
<View />
</Skele.Row>
</Skele.Col>
</Skele.Row>
</View>
)
}
+15 -3
View File
@@ -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 <ThreadAnchorSkeleton />
} else if (item.item === 'reply') {
return <ThreadPostSkeleton />
}
}
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}
/*
+33 -1
View File
@@ -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: {
+5
View File
@@ -92,6 +92,11 @@ export type ThreadItem =
moreReplies: number
skippedIndentIndices: Set<number>
}
| {
type: 'skeleton'
key: string
item: 'anchor' | 'reply' | 'replyComposer'
}
export type TraversalMetadata = {
/**