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