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

Configure Feed

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

at 017120b3e8bff4881d577ea0d8a2ee455e555096 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 { 22 POST_TOMBSTONE, 23 type Shadow, 24 usePostShadow, 25} from '#/state/cache/post-shadow' 26import {useProfileShadow} from '#/state/cache/profile-shadow' 27import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 28import {useLanguagePrefs} from '#/state/preferences' 29import {type ThreadItem} from '#/state/queries/usePostThread/types' 30import {useSession} from '#/state/session' 31import {type OnPostSuccessData} from '#/state/shell/composer' 32import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 33import {type PostSource} from '#/state/unstable-post-source' 34import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 35import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton' 36import { 37 LINEAR_AVI_WIDTH, 38 OUTER_SPACE, 39 REPLY_LINE_WIDTH, 40} from '#/screens/PostThread/const' 41import {atoms as a, useTheme} from '#/alf' 42import {colors} from '#/components/Admonition' 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 {InlineLinkText, 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 {PostControls, PostControlsSkeleton} from '#/components/PostControls' 54import {useFormatPostStatCount} from '#/components/PostControls/util' 55import {ProfileHoverCard} from '#/components/ProfileHoverCard' 56import * as Prompt from '#/components/Prompt' 57import {RichText} from '#/components/RichText' 58import * as Skele from '#/components/Skeleton' 59import {Text} from '#/components/Typography' 60import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 61import {WhoCanReply} from '#/components/WhoCanReply' 62import {useAnalytics} from '#/analytics' 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 ax = useAnalytics() 181 const {_} = useLingui() 182 const {openComposer} = useOpenComposer() 183 const {currentAccount, hasSession} = useSession() 184 const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession) 185 const formatPostStatCount = useFormatPostStatCount() 186 187 const post = postShadow 188 const record = item.value.post.record 189 const moderation = item.moderation 190 const authorShadow = useProfileShadow(post.author) 191 const {isActive: live} = useActorStatus(post.author) 192 const richText = useMemo( 193 () => 194 new RichTextAPI({ 195 text: record.text, 196 facets: record.facets, 197 }), 198 [record], 199 ) 200 201 const threadRootUri = record.reply?.root?.uri || post.uri 202 const authorHref = makeProfileLink(post.author) 203 const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did 204 205 const likesHref = useMemo(() => { 206 const urip = new AtUri(post.uri) 207 return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') 208 }, [post.uri, post.author]) 209 const repostsHref = useMemo(() => { 210 const urip = new AtUri(post.uri) 211 return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') 212 }, [post.uri, post.author]) 213 const quotesHref = useMemo(() => { 214 const urip = new AtUri(post.uri) 215 return makeProfileLink(post.author, 'post', urip.rkey, 'quotes') 216 }, [post.uri, post.author]) 217 218 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ 219 threadgateRecord, 220 }) 221 const additionalPostAlerts: AppModerationCause[] = useMemo(() => { 222 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) 223 const isControlledByViewer = 224 new AtUri(threadRootUri).host === currentAccount?.did 225 return isControlledByViewer && isPostHiddenByThreadgate 226 ? [ 227 { 228 type: 'reply-hidden', 229 source: {type: 'user', did: currentAccount?.did}, 230 priority: 6, 231 }, 232 ] 233 : [] 234 }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) 235 const onlyFollowersCanReply = !!threadgateRecord?.allow?.find( 236 rule => rule.$type === 'app.bsky.feed.threadgate#followerRule', 237 ) 238 const showFollowButton = 239 currentAccount?.did !== post.author.did && !onlyFollowersCanReply 240 241 const viaRepost = useMemo(() => { 242 const reason = postSource?.post.reason 243 244 if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { 245 return { 246 uri: reason.uri, 247 cid: reason.cid, 248 } 249 } 250 }, [postSource]) 251 252 const onPressReply = useCallback(() => { 253 openComposer({ 254 replyTo: { 255 uri: post.uri, 256 cid: post.cid, 257 text: record.text, 258 author: post.author, 259 embed: post.embed, 260 moderation, 261 langs: record.langs, 262 }, 263 onPostSuccess: onPostSuccess, 264 logContext: 'PostReply', 265 }) 266 267 if (postSource) { 268 feedFeedback.sendInteraction({ 269 item: post.uri, 270 event: 'app.bsky.feed.defs#interactionReply', 271 feedContext: postSource.post.feedContext, 272 reqId: postSource.post.reqId, 273 }) 274 } 275 }, [ 276 openComposer, 277 post, 278 record, 279 onPostSuccess, 280 moderation, 281 postSource, 282 feedFeedback, 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 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 style={[a.pl_xs]}> 370 <VerificationCheckButton profile={authorShadow} size="md" /> 371 </View> 372 </View> 373 <Text 374 style={[ 375 a.text_md, 376 a.leading_snug, 377 t.atoms.text_contrast_medium, 378 ]} 379 numberOfLines={1}> 380 {sanitizeHandle(post.author.handle, '@')} 381 </Text> 382 </ProfileHoverCard> 383 </View> 384 </Link> 385 <View collapsable={false} style={[a.self_center]}> 386 <ThreadItemAnchorFollowButton 387 did={post.author.did} 388 enabled={showFollowButton} 389 /> 390 </View> 391 </View> 392 <View style={[a.pb_sm]}> 393 <LabelsOnMyPost post={post} style={[a.pb_sm]} /> 394 <ContentHider 395 modui={moderation.ui('contentView')} 396 ignoreMute 397 childContainerStyle={[a.pt_sm]}> 398 <PostAlerts 399 modui={moderation.ui('contentView')} 400 size="lg" 401 includeMute 402 style={[a.pb_sm]} 403 additionalCauses={additionalPostAlerts} 404 /> 405 {richText?.text ? ( 406 <RichText 407 enableTags 408 selectable 409 value={richText} 410 style={[a.flex_1, a.text_lg]} 411 authorHandle={post.author.handle} 412 shouldProxyLinks={true} 413 /> 414 ) : undefined} 415 {post.embed && ( 416 <View style={[a.py_xs]}> 417 <Embed 418 embed={post.embed} 419 moderation={moderation} 420 viewContext={PostEmbedViewContext.ThreadHighlighted} 421 onOpen={onOpenEmbed} 422 /> 423 </View> 424 )} 425 </ContentHider> 426 <ExpandedPostDetails 427 post={item.value.post} 428 isThreadAuthor={isThreadAuthor} 429 /> 430 {post.repostCount !== 0 || 431 post.likeCount !== 0 || 432 post.quoteCount !== 0 || 433 post.bookmarkCount !== 0 ? ( 434 // Show this section unless we're *sure* it has no engagement. 435 <View 436 style={[ 437 a.flex_row, 438 a.flex_wrap, 439 a.align_center, 440 { 441 rowGap: a.gap_sm.gap, 442 columnGap: a.gap_lg.gap, 443 }, 444 a.border_t, 445 a.border_b, 446 a.mt_md, 447 a.py_md, 448 t.atoms.border_contrast_low, 449 ]}> 450 {post.repostCount != null && post.repostCount !== 0 ? ( 451 <Link to={repostsHref} label={_(msg`Reposts of this post`)}> 452 <Text 453 testID="repostCount-expanded" 454 style={[a.text_md, t.atoms.text_contrast_medium]}> 455 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 456 {formatPostStatCount(post.repostCount)} 457 </Text>{' '} 458 <Plural 459 value={post.repostCount} 460 one="repost" 461 other="reposts" 462 /> 463 </Text> 464 </Link> 465 ) : null} 466 {post.quoteCount != null && 467 post.quoteCount !== 0 && 468 !post.viewer?.embeddingDisabled ? ( 469 <Link to={quotesHref} label={_(msg`Quotes of this post`)}> 470 <Text 471 testID="quoteCount-expanded" 472 style={[a.text_md, t.atoms.text_contrast_medium]}> 473 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 474 {formatPostStatCount(post.quoteCount)} 475 </Text>{' '} 476 <Plural 477 value={post.quoteCount} 478 one="quote" 479 other="quotes" 480 /> 481 </Text> 482 </Link> 483 ) : null} 484 {post.likeCount != null && post.likeCount !== 0 ? ( 485 <Link to={likesHref} label={_(msg`Likes on this post`)}> 486 <Text 487 testID="likeCount-expanded" 488 style={[a.text_md, t.atoms.text_contrast_medium]}> 489 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 490 {formatPostStatCount(post.likeCount)} 491 </Text>{' '} 492 <Plural value={post.likeCount} one="like" other="likes" /> 493 </Text> 494 </Link> 495 ) : null} 496 {post.bookmarkCount != null && post.bookmarkCount !== 0 ? ( 497 <Text 498 testID="bookmarkCount-expanded" 499 style={[a.text_md, t.atoms.text_contrast_medium]}> 500 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 501 {formatPostStatCount(post.bookmarkCount)} 502 </Text>{' '} 503 <Plural value={post.bookmarkCount} one="save" other="saves" /> 504 </Text> 505 ) : null} 506 </View> 507 ) : null} 508 <View 509 style={[ 510 a.pt_sm, 511 a.pb_2xs, 512 { 513 marginLeft: -5, 514 }, 515 ]}> 516 <FeedFeedbackProvider value={feedFeedback}> 517 <PostControls 518 big 519 post={postShadow} 520 record={record} 521 richText={richText} 522 onPressReply={onPressReply} 523 logContext="PostThreadItem" 524 threadgateRecord={threadgateRecord} 525 feedContext={postSource?.post?.feedContext} 526 reqId={postSource?.post?.reqId} 527 viaRepost={viaRepost} 528 /> 529 </FeedFeedbackProvider> 530 </View> 531 <DebugFieldDisplay subject={post} /> 532 </View> 533 </View> 534 </> 535 ) 536}) 537 538function ExpandedPostDetails({ 539 post, 540 isThreadAuthor, 541}: { 542 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post'] 543 isThreadAuthor: boolean 544}) { 545 const t = useTheme() 546 const ax = useAnalytics() 547 const {_, i18n} = useLingui() 548 const translate = useTranslate() 549 const isRootPost = !('reply' in post.record) 550 const langPrefs = useLanguagePrefs() 551 552 const needsTranslation = useMemo( 553 () => 554 Boolean( 555 langPrefs.primaryLanguage && 556 !isPostInLanguage(post, [langPrefs.primaryLanguage]), 557 ), 558 [post, langPrefs.primaryLanguage], 559 ) 560 561 const onTranslatePress = useCallback( 562 (e: GestureResponderEvent) => { 563 e.preventDefault() 564 translate(post.record.text || '', langPrefs.primaryLanguage) 565 566 if ( 567 bsky.dangerousIsType<AppBskyFeedPost.Record>( 568 post.record, 569 AppBskyFeedPost.isRecord, 570 ) 571 ) { 572 ax.metric('translate', { 573 sourceLanguages: post.record.langs ?? [], 574 targetLanguage: langPrefs.primaryLanguage, 575 textLength: post.record.text.length, 576 }) 577 } 578 579 return false 580 }, 581 [ax, translate, langPrefs, post], 582 ) 583 584 return ( 585 <View style={[a.gap_md, a.pt_md, a.align_start]}> 586 <BackdatedPostIndicator post={post} /> 587 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}> 588 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 589 {niceDate(i18n, post.indexedAt, 'dot separated')} 590 </Text> 591 {isRootPost && ( 592 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 593 )} 594 {needsTranslation && ( 595 <> 596 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 597 &middot; 598 </Text> 599 600 <InlineLinkText 601 // overridden to open an intent on android, but keep 602 // as anchor tag for accessibility 603 to={getTranslatorLink( 604 post.record.text, 605 langPrefs.primaryLanguage, 606 )} 607 label={_(msg`Translate`)} 608 style={[a.text_sm]} 609 onPress={onTranslatePress}> 610 <Trans>Translate</Trans> 611 </InlineLinkText> 612 </> 613 )} 614 </View> 615 </View> 616 ) 617} 618 619function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { 620 const t = useTheme() 621 const {_, i18n} = useLingui() 622 const control = Prompt.usePromptControl() 623 624 const indexedAt = new Date(post.indexedAt) 625 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>( 626 post.record, 627 AppBskyFeedPost.isRecord, 628 ) 629 ? new Date(post.record.createdAt) 630 : new Date(post.indexedAt) 631 632 // backdated if createdAt is 24 hours or more before indexedAt 633 const isBackdated = 634 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000 635 636 if (!isBackdated) return null 637 638 const orange = colors.warning 639 640 return ( 641 <> 642 <Button 643 label={_(msg`Archived post`)} 644 accessibilityHint={_( 645 msg`Shows information about when this post was created`, 646 )} 647 onPress={e => { 648 e.preventDefault() 649 e.stopPropagation() 650 control.open() 651 }}> 652 {({hovered, pressed}) => ( 653 <View 654 style={[ 655 a.flex_row, 656 a.align_center, 657 a.rounded_full, 658 t.atoms.bg_contrast_25, 659 (hovered || pressed) && t.atoms.bg_contrast_50, 660 { 661 gap: 3, 662 paddingHorizontal: 6, 663 paddingVertical: 3, 664 }, 665 ]}> 666 <CalendarClockIcon fill={orange} size="sm" aria-hidden /> 667 <Text 668 style={[ 669 a.text_xs, 670 a.font_semi_bold, 671 a.leading_tight, 672 t.atoms.text_contrast_medium, 673 ]}> 674 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans> 675 </Text> 676 </View> 677 )} 678 </Button> 679 680 <Prompt.Outer control={control}> 681 <Prompt.Content> 682 <Prompt.TitleText> 683 <Trans>Archived post</Trans> 684 </Prompt.TitleText> 685 <Prompt.DescriptionText> 686 <Trans> 687 This post claims to have been created on{' '} 688 <RNText style={[a.font_semi_bold]}> 689 {niceDate(i18n, createdAt)} 690 </RNText> 691 , but was first seen by Bluesky on{' '} 692 <RNText style={[a.font_semi_bold]}> 693 {niceDate(i18n, indexedAt)} 694 </RNText> 695 . 696 </Trans> 697 </Prompt.DescriptionText> 698 <Prompt.DescriptionText> 699 <Trans> 700 Bluesky cannot confirm the authenticity of the claimed date. 701 </Trans> 702 </Prompt.DescriptionText> 703 </Prompt.Content> 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}