forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {memo, useCallback, useMemo} from 'react'
2import {type GestureResponderEvent, Text as RNText, View} from 'react-native'
3import {
4 AppBskyFeedDefs,
5 AppBskyFeedPost,
6 type AppBskyFeedThreadgate,
7 AtUri,
8 RichText as RichTextAPI,
9} from '@atproto/api'
10import {msg, Plural, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
14import {useTranslate} from '#/lib/hooks/useTranslate'
15import {makeProfileLink} from '#/lib/routes/links'
16import {sanitizeDisplayName} from '#/lib/strings/display-names'
17import {sanitizeHandle} from '#/lib/strings/handles'
18import {niceDate} from '#/lib/strings/time'
19import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
20import {
21 POST_TOMBSTONE,
22 type Shadow,
23 usePostShadow,
24} from '#/state/cache/post-shadow'
25import {useProfileShadow} from '#/state/cache/profile-shadow'
26import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
27import {useLanguagePrefs} from '#/state/preferences'
28import {type ThreadItem} from '#/state/queries/usePostThread/types'
29import {useSession} from '#/state/session'
30import {type OnPostSuccessData} from '#/state/shell/composer'
31import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
32import {type PostSource} from '#/state/unstable-post-source'
33import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
34import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
35import {
36 LINEAR_AVI_WIDTH,
37 OUTER_SPACE,
38 REPLY_LINE_WIDTH,
39} from '#/screens/PostThread/const'
40import {atoms as a, useTheme} from '#/alf'
41import {colors} from '#/components/Admonition'
42import {Button} from '#/components/Button'
43import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
44import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
45import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
46import {InlineLinkText, Link} from '#/components/Link'
47import {ContentHider} from '#/components/moderation/ContentHider'
48import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
49import {PostAlerts} from '#/components/moderation/PostAlerts'
50import {type AppModerationCause} from '#/components/Pills'
51import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
52import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
53import {useFormatPostStatCount} from '#/components/PostControls/util'
54import {ProfileHoverCard} from '#/components/ProfileHoverCard'
55import * as Prompt from '#/components/Prompt'
56import {RichText} from '#/components/RichText'
57import * as Skele from '#/components/Skeleton'
58import {Text} from '#/components/Typography'
59import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
60import {WhoCanReply} from '#/components/WhoCanReply'
61import {useAnalytics} from '#/analytics'
62import {useActorStatus} from '#/features/liveNow'
63import * as bsky from '#/types/bsky'
64
65export function ThreadItemAnchor({
66 item,
67 onPostSuccess,
68 threadgateRecord,
69 postSource,
70}: {
71 item: Extract<ThreadItem, {type: 'threadPost'}>
72 onPostSuccess?: (data: OnPostSuccessData) => void
73 threadgateRecord?: AppBskyFeedThreadgate.Record
74 postSource?: PostSource
75}) {
76 const postShadow = usePostShadow(item.value.post)
77 const threadRootUri = item.value.post.record.reply?.root?.uri || item.uri
78 const isRoot = threadRootUri === item.uri
79
80 if (postShadow === POST_TOMBSTONE) {
81 return <ThreadItemAnchorDeleted isRoot={isRoot} />
82 }
83
84 return (
85 <ThreadItemAnchorInner
86 // Safeguard from clobbering per-post state below:
87 key={postShadow.uri}
88 item={item}
89 isRoot={isRoot}
90 postShadow={postShadow}
91 onPostSuccess={onPostSuccess}
92 threadgateRecord={threadgateRecord}
93 postSource={postSource}
94 />
95 )
96}
97
98function ThreadItemAnchorDeleted({isRoot}: {isRoot: boolean}) {
99 const t = useTheme()
100
101 return (
102 <>
103 <ThreadItemAnchorParentReplyLine isRoot={isRoot} />
104
105 <View
106 style={[
107 {
108 paddingHorizontal: OUTER_SPACE,
109 paddingBottom: OUTER_SPACE,
110 },
111 isRoot && [a.pt_lg],
112 ]}>
113 <View
114 style={[
115 a.flex_row,
116 a.align_center,
117 a.py_md,
118 a.rounded_sm,
119 t.atoms.bg_contrast_25,
120 ]}>
121 <View
122 style={[
123 a.flex_row,
124 a.align_center,
125 a.justify_center,
126 {
127 width: LINEAR_AVI_WIDTH,
128 },
129 ]}>
130 <TrashIcon style={[t.atoms.text_contrast_medium]} />
131 </View>
132 <Text
133 style={[a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium]}>
134 <Trans>Post has been deleted</Trans>
135 </Text>
136 </View>
137 </View>
138 </>
139 )
140}
141
142function ThreadItemAnchorParentReplyLine({isRoot}: {isRoot: boolean}) {
143 const t = useTheme()
144
145 return !isRoot ? (
146 <View style={[a.pl_lg, a.flex_row, a.pb_xs, {height: a.pt_lg.paddingTop}]}>
147 <View style={{width: 42}}>
148 <View
149 style={[
150 {
151 width: REPLY_LINE_WIDTH,
152 marginLeft: 'auto',
153 marginRight: 'auto',
154 flexGrow: 1,
155 backgroundColor: t.atoms.border_contrast_low.borderColor,
156 },
157 ]}
158 />
159 </View>
160 </View>
161 ) : null
162}
163
164const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
165 item,
166 isRoot,
167 postShadow,
168 onPostSuccess,
169 threadgateRecord,
170 postSource,
171}: {
172 item: Extract<ThreadItem, {type: 'threadPost'}>
173 isRoot: boolean
174 postShadow: Shadow<AppBskyFeedDefs.PostView>
175 onPostSuccess?: (data: OnPostSuccessData) => void
176 threadgateRecord?: AppBskyFeedThreadgate.Record
177 postSource?: PostSource
178}) {
179 const t = useTheme()
180 const ax = useAnalytics()
181 const {_} = useLingui()
182 const {openComposer} = useOpenComposer()
183 const {currentAccount, hasSession} = useSession()
184 const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
185 const formatPostStatCount = useFormatPostStatCount()
186
187 const post = postShadow
188 const record = item.value.post.record
189 const moderation = item.moderation
190 const authorShadow = useProfileShadow(post.author)
191 const {isActive: live} = useActorStatus(post.author)
192 const richText = useMemo(
193 () =>
194 new RichTextAPI({
195 text: record.text,
196 facets: record.facets,
197 }),
198 [record],
199 )
200
201 const threadRootUri = record.reply?.root?.uri || post.uri
202 const authorHref = makeProfileLink(post.author)
203 const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
204
205 const likesHref = useMemo(() => {
206 const urip = new AtUri(post.uri)
207 return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
208 }, [post.uri, post.author])
209 const repostsHref = useMemo(() => {
210 const urip = new AtUri(post.uri)
211 return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
212 }, [post.uri, post.author])
213 const quotesHref = useMemo(() => {
214 const urip = new AtUri(post.uri)
215 return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
216 }, [post.uri, post.author])
217
218 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
219 threadgateRecord,
220 })
221 const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
222 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
223 const isControlledByViewer =
224 new AtUri(threadRootUri).host === currentAccount?.did
225 return isControlledByViewer && isPostHiddenByThreadgate
226 ? [
227 {
228 type: 'reply-hidden',
229 source: {type: 'user', did: currentAccount?.did},
230 priority: 6,
231 },
232 ]
233 : []
234 }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri])
235 const onlyFollowersCanReply = !!threadgateRecord?.allow?.find(
236 rule => rule.$type === 'app.bsky.feed.threadgate#followerRule',
237 )
238 const showFollowButton =
239 currentAccount?.did !== post.author.did && !onlyFollowersCanReply
240
241 const viaRepost = useMemo(() => {
242 const reason = postSource?.post.reason
243
244 if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
245 return {
246 uri: reason.uri,
247 cid: reason.cid,
248 }
249 }
250 }, [postSource])
251
252 const onPressReply = useCallback(() => {
253 openComposer({
254 replyTo: {
255 uri: post.uri,
256 cid: post.cid,
257 text: record.text,
258 author: post.author,
259 embed: post.embed,
260 moderation,
261 langs: record.langs,
262 },
263 onPostSuccess: onPostSuccess,
264 logContext: 'PostReply',
265 })
266
267 if (postSource) {
268 feedFeedback.sendInteraction({
269 item: post.uri,
270 event: 'app.bsky.feed.defs#interactionReply',
271 feedContext: postSource.post.feedContext,
272 reqId: postSource.post.reqId,
273 })
274 }
275 }, [
276 openComposer,
277 post,
278 record,
279 onPostSuccess,
280 moderation,
281 postSource,
282 feedFeedback,
283 ])
284
285 const onOpenAuthor = () => {
286 ax.metric('post:clickthroughAuthor', {
287 uri: post.uri,
288 authorDid: post.author.did,
289 logContext: 'PostThreadItem',
290 feedDescriptor: feedFeedback.feedDescriptor,
291 })
292 if (postSource) {
293 feedFeedback.sendInteraction({
294 item: post.uri,
295 event: 'app.bsky.feed.defs#clickthroughAuthor',
296 feedContext: postSource.post.feedContext,
297 reqId: postSource.post.reqId,
298 })
299 }
300 }
301
302 const onOpenEmbed = () => {
303 ax.metric('post:clickthroughEmbed', {
304 uri: post.uri,
305 authorDid: post.author.did,
306 logContext: 'PostThreadItem',
307 feedDescriptor: feedFeedback.feedDescriptor,
308 })
309 if (postSource) {
310 feedFeedback.sendInteraction({
311 item: post.uri,
312 event: 'app.bsky.feed.defs#clickthroughEmbed',
313 feedContext: postSource.post.feedContext,
314 reqId: postSource.post.reqId,
315 })
316 }
317 }
318
319 return (
320 <>
321 <ThreadItemAnchorParentReplyLine isRoot={isRoot} />
322
323 <View
324 testID={`postThreadItem-by-${post.author.handle}`}
325 style={[
326 {
327 paddingHorizontal: OUTER_SPACE,
328 },
329 isRoot && [a.pt_lg],
330 ]}>
331 <View style={[a.flex_row, a.gap_md, a.pb_md]}>
332 <View collapsable={false}>
333 <PreviewableUserAvatar
334 size={42}
335 profile={post.author}
336 moderation={moderation.ui('avatar')}
337 type={post.author.associated?.labeler ? 'labeler' : 'user'}
338 live={live}
339 onBeforePress={onOpenAuthor}
340 />
341 </View>
342 <Link
343 to={authorHref}
344 style={[a.flex_1]}
345 label={sanitizeDisplayName(
346 post.author.displayName || sanitizeHandle(post.author.handle),
347 moderation.ui('displayName'),
348 )}
349 onPress={onOpenAuthor}>
350 <View style={[a.flex_1, a.align_start]}>
351 <ProfileHoverCard did={post.author.did} style={[a.w_full]}>
352 <View style={[a.flex_row, a.align_center]}>
353 <Text
354 emoji
355 style={[
356 a.flex_shrink,
357 a.text_lg,
358 a.font_semi_bold,
359 a.leading_snug,
360 ]}
361 numberOfLines={1}>
362 {sanitizeDisplayName(
363 post.author.displayName ||
364 sanitizeHandle(post.author.handle),
365 moderation.ui('displayName'),
366 )}
367 </Text>
368
369 <View style={[a.pl_xs]}>
370 <VerificationCheckButton profile={authorShadow} size="md" />
371 </View>
372 </View>
373 <Text
374 style={[
375 a.text_md,
376 a.leading_snug,
377 t.atoms.text_contrast_medium,
378 ]}
379 numberOfLines={1}>
380 {sanitizeHandle(post.author.handle, '@')}
381 </Text>
382 </ProfileHoverCard>
383 </View>
384 </Link>
385 <View collapsable={false} style={[a.self_center]}>
386 <ThreadItemAnchorFollowButton
387 did={post.author.did}
388 enabled={showFollowButton}
389 />
390 </View>
391 </View>
392 <View style={[a.pb_sm]}>
393 <LabelsOnMyPost post={post} style={[a.pb_sm]} />
394 <ContentHider
395 modui={moderation.ui('contentView')}
396 ignoreMute
397 childContainerStyle={[a.pt_sm]}>
398 <PostAlerts
399 modui={moderation.ui('contentView')}
400 size="lg"
401 includeMute
402 style={[a.pb_sm]}
403 additionalCauses={additionalPostAlerts}
404 />
405 {richText?.text ? (
406 <RichText
407 enableTags
408 selectable
409 value={richText}
410 style={[a.flex_1, a.text_lg]}
411 authorHandle={post.author.handle}
412 shouldProxyLinks={true}
413 />
414 ) : undefined}
415 {post.embed && (
416 <View style={[a.py_xs]}>
417 <Embed
418 embed={post.embed}
419 moderation={moderation}
420 viewContext={PostEmbedViewContext.ThreadHighlighted}
421 onOpen={onOpenEmbed}
422 />
423 </View>
424 )}
425 </ContentHider>
426 <ExpandedPostDetails
427 post={item.value.post}
428 isThreadAuthor={isThreadAuthor}
429 />
430 {post.repostCount !== 0 ||
431 post.likeCount !== 0 ||
432 post.quoteCount !== 0 ||
433 post.bookmarkCount !== 0 ? (
434 // Show this section unless we're *sure* it has no engagement.
435 <View
436 style={[
437 a.flex_row,
438 a.flex_wrap,
439 a.align_center,
440 {
441 rowGap: a.gap_sm.gap,
442 columnGap: a.gap_lg.gap,
443 },
444 a.border_t,
445 a.border_b,
446 a.mt_md,
447 a.py_md,
448 t.atoms.border_contrast_low,
449 ]}>
450 {post.repostCount != null && post.repostCount !== 0 ? (
451 <Link to={repostsHref} label={_(msg`Reposts of this post`)}>
452 <Text
453 testID="repostCount-expanded"
454 style={[a.text_md, t.atoms.text_contrast_medium]}>
455 <Trans comment="Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)">
456 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
457 {formatPostStatCount(post.repostCount)}
458 </Text>{' '}
459 <Plural
460 value={post.repostCount}
461 one="repost"
462 other="reposts"
463 />
464 </Trans>
465 </Text>
466 </Link>
467 ) : null}
468 {post.quoteCount != null &&
469 post.quoteCount !== 0 &&
470 !post.viewer?.embeddingDisabled ? (
471 <Link to={quotesHref} label={_(msg`Quotes of this post`)}>
472 <Text
473 testID="quoteCount-expanded"
474 style={[a.text_md, t.atoms.text_contrast_medium]}>
475 <Trans comment="Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)">
476 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
477 {formatPostStatCount(post.quoteCount)}
478 </Text>{' '}
479 <Plural
480 value={post.quoteCount}
481 one="quote"
482 other="quotes"
483 />
484 </Trans>
485 </Text>
486 </Link>
487 ) : null}
488 {post.likeCount != null && post.likeCount !== 0 ? (
489 <Link to={likesHref} label={_(msg`Likes on this post`)}>
490 <Text
491 testID="likeCount-expanded"
492 style={[a.text_md, t.atoms.text_contrast_medium]}>
493 <Trans comment="Like count display, the <0> tags enclose the number of likes in bold (will never be 0)">
494 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
495 {formatPostStatCount(post.likeCount)}
496 </Text>{' '}
497 <Plural value={post.likeCount} one="like" other="likes" />
498 </Trans>
499 </Text>
500 </Link>
501 ) : null}
502 {post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
503 <Text
504 testID="bookmarkCount-expanded"
505 style={[a.text_md, t.atoms.text_contrast_medium]}>
506 <Trans comment="Save count display, the <0> tags enclose the number of saves in bold (will never be 0)">
507 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
508 {formatPostStatCount(post.bookmarkCount)}
509 </Text>{' '}
510 <Plural
511 value={post.bookmarkCount}
512 one="save"
513 other="saves"
514 />
515 </Trans>
516 </Text>
517 ) : null}
518 </View>
519 ) : null}
520 <View
521 style={[
522 a.pt_sm,
523 a.pb_2xs,
524 {
525 marginLeft: -5,
526 },
527 ]}>
528 <FeedFeedbackProvider value={feedFeedback}>
529 <PostControls
530 big
531 post={postShadow}
532 record={record}
533 richText={richText}
534 onPressReply={onPressReply}
535 logContext="PostThreadItem"
536 threadgateRecord={threadgateRecord}
537 feedContext={postSource?.post?.feedContext}
538 reqId={postSource?.post?.reqId}
539 viaRepost={viaRepost}
540 />
541 </FeedFeedbackProvider>
542 </View>
543 <DebugFieldDisplay subject={post} />
544 </View>
545 </View>
546 </>
547 )
548})
549
550function ExpandedPostDetails({
551 post,
552 isThreadAuthor,
553}: {
554 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
555 isThreadAuthor: boolean
556}) {
557 const t = useTheme()
558 const ax = useAnalytics()
559 const {_, i18n} = useLingui()
560 const translate = useTranslate()
561 const isRootPost = !('reply' in post.record)
562 const langPrefs = useLanguagePrefs()
563
564 const needsTranslation = useMemo(
565 () =>
566 Boolean(
567 langPrefs.primaryLanguage &&
568 !isPostInLanguage(post, [langPrefs.primaryLanguage]),
569 ),
570 [post, langPrefs.primaryLanguage],
571 )
572
573 const onTranslatePress = useCallback(
574 (e: GestureResponderEvent) => {
575 e.preventDefault()
576 translate(post.record.text || '', langPrefs.primaryLanguage)
577
578 if (
579 bsky.dangerousIsType<AppBskyFeedPost.Record>(
580 post.record,
581 AppBskyFeedPost.isRecord,
582 )
583 ) {
584 ax.metric('translate', {
585 sourceLanguages: post.record.langs ?? [],
586 targetLanguage: langPrefs.primaryLanguage,
587 textLength: post.record.text.length,
588 })
589 }
590
591 return false
592 },
593 [ax, translate, langPrefs, post],
594 )
595
596 return (
597 <View style={[a.gap_md, a.pt_md, a.align_start]}>
598 <BackdatedPostIndicator post={post} />
599 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
600 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
601 {niceDate(i18n, post.indexedAt, 'dot separated')}
602 </Text>
603 {isRootPost && (
604 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
605 )}
606 {needsTranslation && (
607 <>
608 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
609 ·
610 </Text>
611
612 <InlineLinkText
613 // overridden to open an intent on android, but keep
614 // as anchor tag for accessibility
615 to={getTranslatorLink(
616 post.record.text,
617 langPrefs.primaryLanguage,
618 )}
619 label={_(msg`Translate`)}
620 style={[a.text_sm]}
621 onPress={onTranslatePress}>
622 <Trans>Translate</Trans>
623 </InlineLinkText>
624 </>
625 )}
626 </View>
627 </View>
628 )
629}
630
631function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
632 const t = useTheme()
633 const {_, i18n} = useLingui()
634 const control = Prompt.usePromptControl()
635
636 const indexedAt = new Date(post.indexedAt)
637 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
638 post.record,
639 AppBskyFeedPost.isRecord,
640 )
641 ? new Date(post.record.createdAt)
642 : new Date(post.indexedAt)
643
644 // backdated if createdAt is 24 hours or more before indexedAt
645 const isBackdated =
646 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
647
648 if (!isBackdated) return null
649
650 const orange = colors.warning
651
652 return (
653 <>
654 <Button
655 label={_(msg`Archived post`)}
656 accessibilityHint={_(
657 msg`Shows information about when this post was created`,
658 )}
659 onPress={e => {
660 e.preventDefault()
661 e.stopPropagation()
662 control.open()
663 }}>
664 {({hovered, pressed}) => (
665 <View
666 style={[
667 a.flex_row,
668 a.align_center,
669 a.rounded_full,
670 t.atoms.bg_contrast_25,
671 (hovered || pressed) && t.atoms.bg_contrast_50,
672 {
673 gap: 3,
674 paddingHorizontal: 6,
675 paddingVertical: 3,
676 },
677 ]}>
678 <CalendarClockIcon fill={orange} size="sm" aria-hidden />
679 <Text
680 style={[
681 a.text_xs,
682 a.font_semi_bold,
683 a.leading_tight,
684 t.atoms.text_contrast_medium,
685 ]}>
686 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans>
687 </Text>
688 </View>
689 )}
690 </Button>
691
692 <Prompt.Outer control={control}>
693 <Prompt.Content>
694 <Prompt.TitleText>
695 <Trans>Archived post</Trans>
696 </Prompt.TitleText>
697 <Prompt.DescriptionText>
698 <Trans>
699 This post claims to have been created on{' '}
700 <RNText style={[a.font_semi_bold]}>
701 {niceDate(i18n, createdAt)}
702 </RNText>
703 , but was first seen by Bluesky on{' '}
704 <RNText style={[a.font_semi_bold]}>
705 {niceDate(i18n, indexedAt)}
706 </RNText>
707 .
708 </Trans>
709 </Prompt.DescriptionText>
710 <Prompt.DescriptionText>
711 <Trans>
712 Bluesky cannot confirm the authenticity of the claimed date.
713 </Trans>
714 </Prompt.DescriptionText>
715 </Prompt.Content>
716 <Prompt.Actions>
717 <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
718 </Prompt.Actions>
719 </Prompt.Outer>
720 </>
721 )
722}
723
724function getThreadAuthor(
725 post: AppBskyFeedDefs.PostView,
726 record: AppBskyFeedPost.Record,
727): string {
728 if (!record.reply) {
729 return post.author.did
730 }
731 try {
732 return new AtUri(record.reply.root.uri).host
733 } catch {
734 return ''
735 }
736}
737
738export function ThreadItemAnchorSkeleton() {
739 return (
740 <View style={[a.p_lg, a.gap_md]}>
741 <Skele.Row style={[a.align_center, a.gap_md]}>
742 <Skele.Circle size={42} />
743
744 <Skele.Col>
745 <Skele.Text style={[a.text_lg, {width: '20%'}]} />
746 <Skele.Text blend style={[a.text_md, {width: '40%'}]} />
747 </Skele.Col>
748 </Skele.Row>
749
750 <View>
751 <Skele.Text style={[a.text_xl, {width: '100%'}]} />
752 <Skele.Text style={[a.text_xl, {width: '60%'}]} />
753 </View>
754
755 <Skele.Text style={[a.text_sm, {width: '50%'}]} />
756
757 <PostControlsSkeleton big />
758 </View>
759 )
760}