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

Configure Feed

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

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