this repo has no description
0
fork

Configure Feed

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

at main 716 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 {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}