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