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 {logger} from '#/logger'
22import {
23 POST_TOMBSTONE,
24 type Shadow,
25 usePostShadow,
26} from '#/state/cache/post-shadow'
27import {useProfileShadow} from '#/state/cache/profile-shadow'
28import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
29import {useLanguagePrefs} from '#/state/preferences'
30import {type ThreadItem} from '#/state/queries/usePostThread/types'
31import {useSession} from '#/state/session'
32import {type OnPostSuccessData} from '#/state/shell/composer'
33import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
34import {type PostSource} from '#/state/unstable-post-source'
35import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
36import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
37import {
38 LINEAR_AVI_WIDTH,
39 OUTER_SPACE,
40 REPLY_LINE_WIDTH,
41} from '#/screens/PostThread/const'
42import {atoms as a, useTheme} from '#/alf'
43import {colors} from '#/components/Admonition'
44import {Button} from '#/components/Button'
45import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
46import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
47import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
48import {InlineLinkText, Link} from '#/components/Link'
49import {ContentHider} from '#/components/moderation/ContentHider'
50import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
51import {PostAlerts} from '#/components/moderation/PostAlerts'
52import {type AppModerationCause} from '#/components/Pills'
53import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
54import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
55import {useFormatPostStatCount} from '#/components/PostControls/util'
56import {ProfileHoverCard} from '#/components/ProfileHoverCard'
57import * as Prompt from '#/components/Prompt'
58import {RichText} from '#/components/RichText'
59import * as Skele from '#/components/Skeleton'
60import {Text} from '#/components/Typography'
61import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
62import {WhoCanReply} from '#/components/WhoCanReply'
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 {_} = 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 })
264
265 if (postSource) {
266 feedFeedback.sendInteraction({
267 item: post.uri,
268 event: 'app.bsky.feed.defs#interactionReply',
269 feedContext: postSource.post.feedContext,
270 reqId: postSource.post.reqId,
271 })
272 }
273 }, [
274 openComposer,
275 post,
276 record,
277 onPostSuccess,
278 moderation,
279 postSource,
280 feedFeedback,
281 ])
282
283 const onOpenAuthor = () => {
284 logger.metric('post:clickthroughAuthor', {
285 uri: post.uri,
286 authorDid: post.author.did,
287 logContext: 'PostThreadItem',
288 feedDescriptor: feedFeedback.feedDescriptor,
289 })
290 if (postSource) {
291 feedFeedback.sendInteraction({
292 item: post.uri,
293 event: 'app.bsky.feed.defs#clickthroughAuthor',
294 feedContext: postSource.post.feedContext,
295 reqId: postSource.post.reqId,
296 })
297 }
298 }
299
300 const onOpenEmbed = () => {
301 logger.metric('post:clickthroughEmbed', {
302 uri: post.uri,
303 authorDid: post.author.did,
304 logContext: 'PostThreadItem',
305 feedDescriptor: feedFeedback.feedDescriptor,
306 })
307 if (postSource) {
308 feedFeedback.sendInteraction({
309 item: post.uri,
310 event: 'app.bsky.feed.defs#clickthroughEmbed',
311 feedContext: postSource.post.feedContext,
312 reqId: postSource.post.reqId,
313 })
314 }
315 }
316
317 return (
318 <>
319 <ThreadItemAnchorParentReplyLine isRoot={isRoot} />
320
321 <View
322 testID={`postThreadItem-by-${post.author.handle}`}
323 style={[
324 {
325 paddingHorizontal: OUTER_SPACE,
326 },
327 isRoot && [a.pt_lg],
328 ]}>
329 <View style={[a.flex_row, a.gap_md, a.pb_md]}>
330 <View collapsable={false}>
331 <PreviewableUserAvatar
332 size={42}
333 profile={post.author}
334 moderation={moderation.ui('avatar')}
335 type={post.author.associated?.labeler ? 'labeler' : 'user'}
336 live={live}
337 onBeforePress={onOpenAuthor}
338 />
339 </View>
340 <Link
341 to={authorHref}
342 style={[a.flex_1]}
343 label={sanitizeDisplayName(
344 post.author.displayName || sanitizeHandle(post.author.handle),
345 moderation.ui('displayName'),
346 )}
347 onPress={onOpenAuthor}>
348 <View style={[a.flex_1, a.align_start]}>
349 <ProfileHoverCard did={post.author.did} style={[a.w_full]}>
350 <View style={[a.flex_row, a.align_center]}>
351 <Text
352 emoji
353 style={[
354 a.flex_shrink,
355 a.text_lg,
356 a.font_semi_bold,
357 a.leading_snug,
358 ]}
359 numberOfLines={1}>
360 {sanitizeDisplayName(
361 post.author.displayName ||
362 sanitizeHandle(post.author.handle),
363 moderation.ui('displayName'),
364 )}
365 </Text>
366
367 <View style={[a.pl_xs]}>
368 <VerificationCheckButton profile={authorShadow} size="md" />
369 </View>
370 </View>
371 <Text
372 style={[
373 a.text_md,
374 a.leading_snug,
375 t.atoms.text_contrast_medium,
376 ]}
377 numberOfLines={1}>
378 {sanitizeHandle(post.author.handle, '@')}
379 </Text>
380 </ProfileHoverCard>
381 </View>
382 </Link>
383 {showFollowButton && (
384 <View collapsable={false} style={[a.self_center]}>
385 <ThreadItemAnchorFollowButton did={post.author.did} />
386 </View>
387 )}
388 </View>
389 <View style={[a.pb_sm]}>
390 <LabelsOnMyPost post={post} style={[a.pb_sm]} />
391 <ContentHider
392 modui={moderation.ui('contentView')}
393 ignoreMute
394 childContainerStyle={[a.pt_sm]}>
395 <PostAlerts
396 modui={moderation.ui('contentView')}
397 size="lg"
398 includeMute
399 style={[a.pb_sm]}
400 additionalCauses={additionalPostAlerts}
401 />
402 {richText?.text ? (
403 <RichText
404 enableTags
405 selectable
406 value={richText}
407 style={[a.flex_1, a.text_lg]}
408 authorHandle={post.author.handle}
409 shouldProxyLinks={true}
410 />
411 ) : undefined}
412 {post.embed && (
413 <View style={[a.py_xs]}>
414 <Embed
415 embed={post.embed}
416 moderation={moderation}
417 viewContext={PostEmbedViewContext.ThreadHighlighted}
418 onOpen={onOpenEmbed}
419 />
420 </View>
421 )}
422 </ContentHider>
423 <ExpandedPostDetails
424 post={item.value.post}
425 isThreadAuthor={isThreadAuthor}
426 />
427 {post.repostCount !== 0 ||
428 post.likeCount !== 0 ||
429 post.quoteCount !== 0 ||
430 post.bookmarkCount !== 0 ? (
431 // Show this section unless we're *sure* it has no engagement.
432 <View
433 style={[
434 a.flex_row,
435 a.flex_wrap,
436 a.align_center,
437 {
438 rowGap: a.gap_sm.gap,
439 columnGap: a.gap_lg.gap,
440 },
441 a.border_t,
442 a.border_b,
443 a.mt_md,
444 a.py_md,
445 t.atoms.border_contrast_low,
446 ]}>
447 {post.repostCount != null && post.repostCount !== 0 ? (
448 <Link to={repostsHref} label={_(msg`Reposts of this post`)}>
449 <Text
450 testID="repostCount-expanded"
451 style={[a.text_md, t.atoms.text_contrast_medium]}>
452 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
453 {formatPostStatCount(post.repostCount)}
454 </Text>{' '}
455 <Plural
456 value={post.repostCount}
457 one="repost"
458 other="reposts"
459 />
460 </Text>
461 </Link>
462 ) : null}
463 {post.quoteCount != null &&
464 post.quoteCount !== 0 &&
465 !post.viewer?.embeddingDisabled ? (
466 <Link to={quotesHref} label={_(msg`Quotes of this post`)}>
467 <Text
468 testID="quoteCount-expanded"
469 style={[a.text_md, t.atoms.text_contrast_medium]}>
470 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
471 {formatPostStatCount(post.quoteCount)}
472 </Text>{' '}
473 <Plural
474 value={post.quoteCount}
475 one="quote"
476 other="quotes"
477 />
478 </Text>
479 </Link>
480 ) : null}
481 {post.likeCount != null && post.likeCount !== 0 ? (
482 <Link to={likesHref} label={_(msg`Likes on this post`)}>
483 <Text
484 testID="likeCount-expanded"
485 style={[a.text_md, t.atoms.text_contrast_medium]}>
486 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
487 {formatPostStatCount(post.likeCount)}
488 </Text>{' '}
489 <Plural value={post.likeCount} one="like" other="likes" />
490 </Text>
491 </Link>
492 ) : null}
493 {post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
494 <Text
495 testID="bookmarkCount-expanded"
496 style={[a.text_md, t.atoms.text_contrast_medium]}>
497 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
498 {formatPostStatCount(post.bookmarkCount)}
499 </Text>{' '}
500 <Plural value={post.bookmarkCount} one="save" other="saves" />
501 </Text>
502 ) : null}
503 </View>
504 ) : null}
505 <View
506 style={[
507 a.pt_sm,
508 a.pb_2xs,
509 {
510 marginLeft: -5,
511 },
512 ]}>
513 <FeedFeedbackProvider value={feedFeedback}>
514 <PostControls
515 big
516 post={postShadow}
517 record={record}
518 richText={richText}
519 onPressReply={onPressReply}
520 logContext="PostThreadItem"
521 threadgateRecord={threadgateRecord}
522 feedContext={postSource?.post?.feedContext}
523 reqId={postSource?.post?.reqId}
524 viaRepost={viaRepost}
525 />
526 </FeedFeedbackProvider>
527 </View>
528 <DebugFieldDisplay subject={post} />
529 </View>
530 </View>
531 </>
532 )
533})
534
535function ExpandedPostDetails({
536 post,
537 isThreadAuthor,
538}: {
539 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
540 isThreadAuthor: boolean
541}) {
542 const t = useTheme()
543 const {_, i18n} = useLingui()
544 const translate = useTranslate()
545 const isRootPost = !('reply' in post.record)
546 const langPrefs = useLanguagePrefs()
547
548 const needsTranslation = useMemo(
549 () =>
550 Boolean(
551 langPrefs.primaryLanguage &&
552 !isPostInLanguage(post, [langPrefs.primaryLanguage]),
553 ),
554 [post, langPrefs.primaryLanguage],
555 )
556
557 const onTranslatePress = useCallback(
558 (e: GestureResponderEvent) => {
559 e.preventDefault()
560 translate(post.record.text || '', langPrefs.primaryLanguage)
561
562 if (
563 bsky.dangerousIsType<AppBskyFeedPost.Record>(
564 post.record,
565 AppBskyFeedPost.isRecord,
566 )
567 ) {
568 logger.metric('translate', {
569 sourceLanguages: post.record.langs ?? [],
570 targetLanguage: langPrefs.primaryLanguage,
571 textLength: post.record.text.length,
572 })
573 }
574
575 return false
576 },
577 [translate, langPrefs, post],
578 )
579
580 return (
581 <View style={[a.gap_md, a.pt_md, a.align_start]}>
582 <BackdatedPostIndicator post={post} />
583 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
584 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
585 {niceDate(i18n, post.indexedAt, 'dot separated')}
586 </Text>
587 {isRootPost && (
588 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
589 )}
590 {needsTranslation && (
591 <>
592 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
593 ·
594 </Text>
595
596 <InlineLinkText
597 // overridden to open an intent on android, but keep
598 // as anchor tag for accessibility
599 to={getTranslatorLink(
600 post.record.text,
601 langPrefs.primaryLanguage,
602 )}
603 label={_(msg`Translate`)}
604 style={[a.text_sm]}
605 onPress={onTranslatePress}>
606 <Trans>Translate</Trans>
607 </InlineLinkText>
608 </>
609 )}
610 </View>
611 </View>
612 )
613}
614
615function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
616 const t = useTheme()
617 const {_, i18n} = useLingui()
618 const control = Prompt.usePromptControl()
619
620 const indexedAt = new Date(post.indexedAt)
621 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
622 post.record,
623 AppBskyFeedPost.isRecord,
624 )
625 ? new Date(post.record.createdAt)
626 : new Date(post.indexedAt)
627
628 // backdated if createdAt is 24 hours or more before indexedAt
629 const isBackdated =
630 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
631
632 if (!isBackdated) return null
633
634 const orange = colors.warning
635
636 return (
637 <>
638 <Button
639 label={_(msg`Archived post`)}
640 accessibilityHint={_(
641 msg`Shows information about when this post was created`,
642 )}
643 onPress={e => {
644 e.preventDefault()
645 e.stopPropagation()
646 control.open()
647 }}>
648 {({hovered, pressed}) => (
649 <View
650 style={[
651 a.flex_row,
652 a.align_center,
653 a.rounded_full,
654 t.atoms.bg_contrast_25,
655 (hovered || pressed) && t.atoms.bg_contrast_50,
656 {
657 gap: 3,
658 paddingHorizontal: 6,
659 paddingVertical: 3,
660 },
661 ]}>
662 <CalendarClockIcon fill={orange} size="sm" aria-hidden />
663 <Text
664 style={[
665 a.text_xs,
666 a.font_semi_bold,
667 a.leading_tight,
668 t.atoms.text_contrast_medium,
669 ]}>
670 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans>
671 </Text>
672 </View>
673 )}
674 </Button>
675
676 <Prompt.Outer control={control}>
677 <Prompt.TitleText>
678 <Trans>Archived post</Trans>
679 </Prompt.TitleText>
680 <Prompt.DescriptionText>
681 <Trans>
682 This post claims to have been created on{' '}
683 <RNText style={[a.font_semi_bold]}>
684 {niceDate(i18n, createdAt)}
685 </RNText>
686 , but was first seen by Bluesky on{' '}
687 <RNText style={[a.font_semi_bold]}>
688 {niceDate(i18n, indexedAt)}
689 </RNText>
690 .
691 </Trans>
692 </Prompt.DescriptionText>
693 <Text
694 style={[
695 a.text_md,
696 a.leading_snug,
697 t.atoms.text_contrast_high,
698 a.pb_xl,
699 ]}>
700 <Trans>
701 Bluesky cannot confirm the authenticity of the claimed date.
702 </Trans>
703 </Text>
704 <Prompt.Actions>
705 <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
706 </Prompt.Actions>
707 </Prompt.Outer>
708 </>
709 )
710}
711
712function getThreadAuthor(
713 post: AppBskyFeedDefs.PostView,
714 record: AppBskyFeedPost.Record,
715): string {
716 if (!record.reply) {
717 return post.author.did
718 }
719 try {
720 return new AtUri(record.reply.root.uri).host
721 } catch {
722 return ''
723 }
724}
725
726export function ThreadItemAnchorSkeleton() {
727 return (
728 <View style={[a.p_lg, a.gap_md]}>
729 <Skele.Row style={[a.align_center, a.gap_md]}>
730 <Skele.Circle size={42} />
731
732 <Skele.Col>
733 <Skele.Text style={[a.text_lg, {width: '20%'}]} />
734 <Skele.Text blend style={[a.text_md, {width: '40%'}]} />
735 </Skele.Col>
736 </Skele.Row>
737
738 <View>
739 <Skele.Text style={[a.text_xl, {width: '100%'}]} />
740 <Skele.Text style={[a.text_xl, {width: '60%'}]} />
741 </View>
742
743 <Skele.Text style={[a.text_sm, {width: '50%'}]} />
744
745 <PostControlsSkeleton big />
746 </View>
747 )
748}