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

Configure Feed

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

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