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