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