Add basic post and anchor skeletons
This commit is contained in:
@@ -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 {PostControls} from '#/components/PostControls'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
|
import * as Skele from '#/components/Skeleton'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
import {WhoCanReply} from '#/components/WhoCanReply'
|
import {WhoCanReply} from '#/components/WhoCanReply'
|
||||||
@@ -574,3 +575,33 @@ function getThreadAuthor(
|
|||||||
return ''
|
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 {type AppModerationCause} from '#/components/Pills'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
|
import * as Skele from '#/components/Skeleton'
|
||||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -320,3 +321,40 @@ function SubtleHover({children}: {children: React.ReactNode}) {
|
|||||||
</View>
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ import {PostThreadShowHiddenReplies} from '#/view/com/post-thread/PostThreadShow
|
|||||||
import {List, type ListMethods} from '#/view/com/util/List'
|
import {List, type ListMethods} from '#/view/com/util/List'
|
||||||
import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
|
import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
|
||||||
import {ReadMore} from '#/screens/PostThread/components/ReadMore'
|
import {ReadMore} from '#/screens/PostThread/components/ReadMore'
|
||||||
import {ThreadAnchor} from '#/screens/PostThread/components/ThreadAnchor'
|
import {
|
||||||
import {ThreadPost} from '#/screens/PostThread/components/ThreadPost'
|
ThreadAnchor,
|
||||||
|
ThreadAnchorSkeleton,
|
||||||
|
} from '#/screens/PostThread/components/ThreadAnchor'
|
||||||
|
import {
|
||||||
|
ThreadPost,
|
||||||
|
ThreadPostSkeleton,
|
||||||
|
} from '#/screens/PostThread/components/ThreadPost'
|
||||||
import {ThreadReply} from '#/screens/PostThread/components/ThreadReply'
|
import {ThreadReply} from '#/screens/PostThread/components/ThreadReply'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
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
|
return null
|
||||||
}
|
}
|
||||||
@@ -358,7 +370,7 @@ export function Inner({uri}: {uri: string | undefined}) {
|
|||||||
* Using `isFetching` over `isFetchingNextPage` is done on
|
* Using `isFetching` over `isFetchingNextPage` is done on
|
||||||
* purpose here so we get the loader on initial render
|
* purpose here so we get the loader on initial render
|
||||||
*/
|
*/
|
||||||
isFetchingNextPage={isFetching}
|
// isFetchingNextPage={isFetching}
|
||||||
error={cleanError(error)}
|
error={cleanError(error)}
|
||||||
onRetry={refetch}
|
onRetry={refetch}
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function usePostThread({
|
|||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
return {thread: [placeholder], hasHiddenReplies: false}
|
return {thread: [placeholder], hasHiddenReplies: false}
|
||||||
}
|
}
|
||||||
return
|
return {thread: [], hasHiddenReplies: false}
|
||||||
},
|
},
|
||||||
select(data) {
|
select(data) {
|
||||||
const threadgate = getThreadgateRecord(data.threadgate)
|
const threadgate = getThreadgateRecord(data.threadgate)
|
||||||
@@ -96,6 +96,38 @@ export function usePostThread({
|
|||||||
queryClient: qc,
|
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 {
|
return {
|
||||||
...query,
|
...query,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ export type ThreadItem =
|
|||||||
moreReplies: number
|
moreReplies: number
|
||||||
skippedIndentIndices: Set<number>
|
skippedIndentIndices: Set<number>
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'skeleton'
|
||||||
|
key: string
|
||||||
|
item: 'anchor' | 'reply' | 'replyComposer'
|
||||||
|
}
|
||||||
|
|
||||||
export type TraversalMetadata = {
|
export type TraversalMetadata = {
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user