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