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 {useActorStatus} from '#/lib/actor-status'
14import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
15import {useTranslate} from '#/lib/hooks/useTranslate'
16import {makeProfileLink} from '#/lib/routes/links'
17import {sanitizeDisplayName} from '#/lib/strings/display-names'
18import {sanitizeHandle} from '#/lib/strings/handles'
19import {niceDate} from '#/lib/strings/time'
20import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
21import {
22 POST_TOMBSTONE,
23 type Shadow,
24 usePostShadow,
25} from '#/state/cache/post-shadow'
26import {useProfileShadow} from '#/state/cache/profile-shadow'
27import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
28import {useLanguagePrefs} from '#/state/preferences'
29import {type ThreadItem} from '#/state/queries/usePostThread/types'
30import {useSession} from '#/state/session'
31import {type OnPostSuccessData} from '#/state/shell/composer'
32import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
33import {type PostSource} from '#/state/unstable-post-source'
34import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
35import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
36import {
37 LINEAR_AVI_WIDTH,
38 OUTER_SPACE,
39 REPLY_LINE_WIDTH,
40} from '#/screens/PostThread/const'
41import {atoms as a, useTheme} from '#/alf'
42import {colors} from '#/components/Admonition'
43import {Button} from '#/components/Button'
44import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
45import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
46import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
47import {InlineLinkText, Link} from '#/components/Link'
48import {ContentHider} from '#/components/moderation/ContentHider'
49import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
50import {PostAlerts} from '#/components/moderation/PostAlerts'
51import {type AppModerationCause} from '#/components/Pills'
52import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
53import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
54import {useFormatPostStatCount} from '#/components/PostControls/util'
55import {ProfileHoverCard} from '#/components/ProfileHoverCard'
56import * as Prompt from '#/components/Prompt'
57import {RichText} from '#/components/RichText'
58import * as Skele from '#/components/Skeleton'
59import {Text} from '#/components/Typography'
60import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
61import {WhoCanReply} from '#/components/WhoCanReply'
62import {useAnalytics} from '#/analytics'
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 })
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 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
455 {formatPostStatCount(post.repostCount)}
456 </Text>{' '}
457 <Plural
458 value={post.repostCount}
459 one="repost"
460 other="reposts"
461 />
462 </Text>
463 </Link>
464 ) : null}
465 {post.quoteCount != null &&
466 post.quoteCount !== 0 &&
467 !post.viewer?.embeddingDisabled ? (
468 <Link to={quotesHref} label={_(msg`Quotes of this post`)}>
469 <Text
470 testID="quoteCount-expanded"
471 style={[a.text_md, t.atoms.text_contrast_medium]}>
472 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
473 {formatPostStatCount(post.quoteCount)}
474 </Text>{' '}
475 <Plural
476 value={post.quoteCount}
477 one="quote"
478 other="quotes"
479 />
480 </Text>
481 </Link>
482 ) : null}
483 {post.likeCount != null && post.likeCount !== 0 ? (
484 <Link to={likesHref} label={_(msg`Likes on this post`)}>
485 <Text
486 testID="likeCount-expanded"
487 style={[a.text_md, t.atoms.text_contrast_medium]}>
488 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
489 {formatPostStatCount(post.likeCount)}
490 </Text>{' '}
491 <Plural value={post.likeCount} one="like" other="likes" />
492 </Text>
493 </Link>
494 ) : null}
495 {post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
496 <Text
497 testID="bookmarkCount-expanded"
498 style={[a.text_md, t.atoms.text_contrast_medium]}>
499 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
500 {formatPostStatCount(post.bookmarkCount)}
501 </Text>{' '}
502 <Plural value={post.bookmarkCount} one="save" other="saves" />
503 </Text>
504 ) : null}
505 </View>
506 ) : null}
507 <View
508 style={[
509 a.pt_sm,
510 a.pb_2xs,
511 {
512 marginLeft: -5,
513 },
514 ]}>
515 <FeedFeedbackProvider value={feedFeedback}>
516 <PostControls
517 big
518 post={postShadow}
519 record={record}
520 richText={richText}
521 onPressReply={onPressReply}
522 logContext="PostThreadItem"
523 threadgateRecord={threadgateRecord}
524 feedContext={postSource?.post?.feedContext}
525 reqId={postSource?.post?.reqId}
526 viaRepost={viaRepost}
527 />
528 </FeedFeedbackProvider>
529 </View>
530 <DebugFieldDisplay subject={post} />
531 </View>
532 </View>
533 </>
534 )
535})
536
537function ExpandedPostDetails({
538 post,
539 isThreadAuthor,
540}: {
541 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
542 isThreadAuthor: boolean
543}) {
544 const t = useTheme()
545 const ax = useAnalytics()
546 const {_, i18n} = useLingui()
547 const translate = useTranslate()
548 const isRootPost = !('reply' in post.record)
549 const langPrefs = useLanguagePrefs()
550
551 const needsTranslation = useMemo(
552 () =>
553 Boolean(
554 langPrefs.primaryLanguage &&
555 !isPostInLanguage(post, [langPrefs.primaryLanguage]),
556 ),
557 [post, langPrefs.primaryLanguage],
558 )
559
560 const onTranslatePress = useCallback(
561 (e: GestureResponderEvent) => {
562 e.preventDefault()
563 translate(post.record.text || '', langPrefs.primaryLanguage)
564
565 if (
566 bsky.dangerousIsType<AppBskyFeedPost.Record>(
567 post.record,
568 AppBskyFeedPost.isRecord,
569 )
570 ) {
571 ax.metric('translate', {
572 sourceLanguages: post.record.langs ?? [],
573 targetLanguage: langPrefs.primaryLanguage,
574 textLength: post.record.text.length,
575 })
576 }
577
578 return false
579 },
580 [ax, translate, langPrefs, post],
581 )
582
583 return (
584 <View style={[a.gap_md, a.pt_md, a.align_start]}>
585 <BackdatedPostIndicator post={post} />
586 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
587 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
588 {niceDate(i18n, post.indexedAt, 'dot separated')}
589 </Text>
590 {isRootPost && (
591 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
592 )}
593 {needsTranslation && (
594 <>
595 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
596 ·
597 </Text>
598
599 <InlineLinkText
600 // overridden to open an intent on android, but keep
601 // as anchor tag for accessibility
602 to={getTranslatorLink(
603 post.record.text,
604 langPrefs.primaryLanguage,
605 )}
606 label={_(msg`Translate`)}
607 style={[a.text_sm]}
608 onPress={onTranslatePress}>
609 <Trans>Translate</Trans>
610 </InlineLinkText>
611 </>
612 )}
613 </View>
614 </View>
615 )
616}
617
618function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
619 const t = useTheme()
620 const {_, i18n} = useLingui()
621 const control = Prompt.usePromptControl()
622
623 const indexedAt = new Date(post.indexedAt)
624 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
625 post.record,
626 AppBskyFeedPost.isRecord,
627 )
628 ? new Date(post.record.createdAt)
629 : new Date(post.indexedAt)
630
631 // backdated if createdAt is 24 hours or more before indexedAt
632 const isBackdated =
633 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
634
635 if (!isBackdated) return null
636
637 const orange = colors.warning
638
639 return (
640 <>
641 <Button
642 label={_(msg`Archived post`)}
643 accessibilityHint={_(
644 msg`Shows information about when this post was created`,
645 )}
646 onPress={e => {
647 e.preventDefault()
648 e.stopPropagation()
649 control.open()
650 }}>
651 {({hovered, pressed}) => (
652 <View
653 style={[
654 a.flex_row,
655 a.align_center,
656 a.rounded_full,
657 t.atoms.bg_contrast_25,
658 (hovered || pressed) && t.atoms.bg_contrast_50,
659 {
660 gap: 3,
661 paddingHorizontal: 6,
662 paddingVertical: 3,
663 },
664 ]}>
665 <CalendarClockIcon fill={orange} size="sm" aria-hidden />
666 <Text
667 style={[
668 a.text_xs,
669 a.font_semi_bold,
670 a.leading_tight,
671 t.atoms.text_contrast_medium,
672 ]}>
673 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans>
674 </Text>
675 </View>
676 )}
677 </Button>
678
679 <Prompt.Outer control={control}>
680 <Prompt.TitleText>
681 <Trans>Archived post</Trans>
682 </Prompt.TitleText>
683 <Prompt.DescriptionText>
684 <Trans>
685 This post claims to have been created on{' '}
686 <RNText style={[a.font_semi_bold]}>
687 {niceDate(i18n, createdAt)}
688 </RNText>
689 , but was first seen by Bluesky on{' '}
690 <RNText style={[a.font_semi_bold]}>
691 {niceDate(i18n, indexedAt)}
692 </RNText>
693 .
694 </Trans>
695 </Prompt.DescriptionText>
696 <Text
697 style={[
698 a.text_md,
699 a.leading_snug,
700 t.atoms.text_contrast_high,
701 a.pb_xl,
702 ]}>
703 <Trans>
704 Bluesky cannot confirm the authenticity of the claimed date.
705 </Trans>
706 </Text>
707 <Prompt.Actions>
708 <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
709 </Prompt.Actions>
710 </Prompt.Outer>
711 </>
712 )
713}
714
715function getThreadAuthor(
716 post: AppBskyFeedDefs.PostView,
717 record: AppBskyFeedPost.Record,
718): string {
719 if (!record.reply) {
720 return post.author.did
721 }
722 try {
723 return new AtUri(record.reply.root.uri).host
724 } catch {
725 return ''
726 }
727}
728
729export function ThreadItemAnchorSkeleton() {
730 return (
731 <View style={[a.p_lg, a.gap_md]}>
732 <Skele.Row style={[a.align_center, a.gap_md]}>
733 <Skele.Circle size={42} />
734
735 <Skele.Col>
736 <Skele.Text style={[a.text_lg, {width: '20%'}]} />
737 <Skele.Text blend style={[a.text_md, {width: '40%'}]} />
738 </Skele.Col>
739 </Skele.Row>
740
741 <View>
742 <Skele.Text style={[a.text_xl, {width: '100%'}]} />
743 <Skele.Text style={[a.text_xl, {width: '60%'}]} />
744 </View>
745
746 <Skele.Text style={[a.text_sm, {width: '50%'}]} />
747
748 <PostControlsSkeleton big />
749 </View>
750 )
751}