Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 714 lines 24 kB view raw
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}