Bluesky app fork with some witchin' additions 馃挮
at main 719 lines 24 kB view raw
1import {memo, useCallback, 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 {useOpenComposer} from '#/lib/hooks/useOpenComposer' 13import {makeProfileLink} from '#/lib/routes/links' 14import {sanitizeDisplayName} from '#/lib/strings/display-names' 15import {sanitizeHandle} from '#/lib/strings/handles' 16import {niceDate} from '#/lib/strings/time' 17import { 18 POST_TOMBSTONE, 19 type Shadow, 20 usePostShadow, 21} from '#/state/cache/post-shadow' 22import {useProfileShadow} from '#/state/cache/profile-shadow' 23import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 24import {useDisableLikesMetrics} from '#/state/preferences/disable-likes-metrics' 25import {useDisableQuotesMetrics} from '#/state/preferences/disable-quotes-metrics' 26import {useDisableRepostsMetrics} from '#/state/preferences/disable-reposts-metrics' 27import {useDisableSavesMetrics} from '#/state/preferences/disable-saves-metrics' 28import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 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 {Button} from '#/components/Button' 43import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 44import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 45import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 46import {Link} from '#/components/Link' 47import {ContentHider} from '#/components/moderation/ContentHider' 48import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 49import {PostAlerts} from '#/components/moderation/PostAlerts' 50import {PdsBadge} from '#/components/PdsBadge' 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 {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 {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 = useCallback(() => { 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 openComposer, 285 post, 286 record, 287 onPostSuccess, 288 moderation, 289 postSource, 290 feedFeedback, 291 ]) 292 293 const onOpenAuthor = () => { 294 ax.metric('post:clickthroughAuthor', { 295 uri: post.uri, 296 authorDid: post.author.did, 297 logContext: 'PostThreadItem', 298 feedDescriptor: feedFeedback.feedDescriptor, 299 }) 300 if (postSource) { 301 feedFeedback.sendInteraction({ 302 item: post.uri, 303 event: 'app.bsky.feed.defs#clickthroughAuthor', 304 feedContext: postSource.post.feedContext, 305 reqId: postSource.post.reqId, 306 }) 307 } 308 } 309 310 const onOpenEmbed = () => { 311 ax.metric('post:clickthroughEmbed', { 312 uri: post.uri, 313 authorDid: post.author.did, 314 logContext: 'PostThreadItem', 315 feedDescriptor: feedFeedback.feedDescriptor, 316 }) 317 if (postSource) { 318 feedFeedback.sendInteraction({ 319 item: post.uri, 320 event: 'app.bsky.feed.defs#clickthroughEmbed', 321 feedContext: postSource.post.feedContext, 322 reqId: postSource.post.reqId, 323 }) 324 } 325 } 326 327 return ( 328 <> 329 <ThreadItemAnchorParentReplyLine isRoot={isRoot} /> 330 <View 331 testID={`postThreadItem-by-${post.author.handle}`} 332 style={[ 333 { 334 paddingHorizontal: OUTER_SPACE, 335 }, 336 isRoot && [a.pt_lg], 337 ]}> 338 <View style={[a.flex_row, a.gap_md, a.pb_md]}> 339 <View collapsable={false}> 340 <PreviewableUserAvatar 341 size={42} 342 profile={post.author} 343 moderation={moderation.ui('avatar')} 344 type={post.author.associated?.labeler ? 'labeler' : 'user'} 345 live={live} 346 onBeforePress={onOpenAuthor} 347 /> 348 </View> 349 <Link 350 to={authorHref} 351 style={[a.flex_1]} 352 label={sanitizeDisplayName( 353 post.author.displayName || sanitizeHandle(post.author.handle), 354 moderation.ui('displayName'), 355 )} 356 onPress={onOpenAuthor}> 357 <View style={[a.flex_1, a.align_start]}> 358 <ProfileHoverCard did={post.author.did} style={[a.w_full]}> 359 <View style={[a.flex_row, a.align_center]}> 360 <Text 361 emoji 362 style={[ 363 a.flex_shrink, 364 a.text_lg, 365 a.font_semi_bold, 366 a.leading_snug, 367 ]} 368 numberOfLines={1}> 369 {sanitizeDisplayName( 370 post.author.displayName || 371 sanitizeHandle(post.author.handle), 372 moderation.ui('displayName'), 373 )} 374 </Text> 375 376 <View 377 style={[a.pl_xs, a.flex_row, a.gap_2xs, a.align_center]}> 378 <PdsBadge did={post.author.did} size="md" /> 379 <VerificationCheckButton profile={authorShadow} size="md" /> 380 </View> 381 </View> 382 <Text 383 style={[ 384 a.text_md, 385 a.leading_snug, 386 t.atoms.text_contrast_medium, 387 ]} 388 numberOfLines={1}> 389 {sanitizeHandle(post.author.handle, '@')} 390 </Text> 391 </ProfileHoverCard> 392 </View> 393 </Link> 394 <View collapsable={false} style={[a.self_center]}> 395 <ThreadItemAnchorFollowButton 396 did={post.author.did} 397 enabled={showFollowButton} 398 /> 399 </View> 400 </View> 401 <View style={[a.pb_sm]}> 402 <LabelsOnMyPost post={post} style={[a.pb_sm]} /> 403 <ContentHider 404 modui={moderation.ui('contentView')} 405 ignoreMute 406 childContainerStyle={[a.pt_sm]}> 407 <PostAlerts 408 modui={moderation.ui('contentView')} 409 size="lg" 410 includeMute 411 style={[a.pb_sm]} 412 additionalCauses={additionalPostAlerts} 413 /> 414 {richText?.text ? ( 415 <RichText 416 enableTags 417 selectable 418 value={richText} 419 style={[a.flex_1, a.text_lg]} 420 authorHandle={post.author.handle} 421 shouldProxyLinks={true} 422 /> 423 ) : undefined} 424 <TranslatedPost post={post} postText={record.text} /> 425 {post.embed && ( 426 <View style={[a.py_xs]}> 427 <Embed 428 embed={post.embed} 429 moderation={moderation} 430 viewContext={PostEmbedViewContext.ThreadHighlighted} 431 onOpen={onOpenEmbed} 432 /> 433 </View> 434 )} 435 </ContentHider> 436 <ExpandedPostDetails 437 post={item.value.post} 438 isThreadAuthor={isThreadAuthor} 439 /> 440 {(post.repostCount !== 0 && !disableRepostsMetrics) || 441 (post.likeCount !== 0 && !disableLikesMetrics) || 442 (post.quoteCount !== 0 && !disableQuotesMetrics) || 443 (post.bookmarkCount !== 0 && !disableSavesMetrics) ? ( 444 // Show this section unless we're *sure* it has no engagement. 445 <View 446 style={[ 447 a.flex_row, 448 a.flex_wrap, 449 a.align_center, 450 { 451 rowGap: a.gap_sm.gap, 452 columnGap: a.gap_lg.gap, 453 }, 454 a.border_t, 455 a.border_b, 456 a.mt_md, 457 a.py_md, 458 t.atoms.border_contrast_low, 459 ]}> 460 {post.repostCount != null && 461 post.repostCount !== 0 && 462 !disableRepostsMetrics ? ( 463 <Link to={repostsHref} label={l`Reposts of this post`}> 464 <Text 465 testID="repostCount-expanded" 466 style={[a.text_md, t.atoms.text_contrast_medium]}> 467 <Trans comment="Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)"> 468 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 469 {formatPostStatCount(post.repostCount)} 470 </Text>{' '} 471 <Plural 472 value={post.repostCount} 473 one="repost" 474 other="reposts" 475 /> 476 </Trans> 477 </Text> 478 </Link> 479 ) : null} 480 {post.quoteCount != null && 481 post.quoteCount !== 0 && 482 !post.viewer?.embeddingDisabled && 483 !disableQuotesMetrics ? ( 484 <Link to={quotesHref} label={l`Quotes of this post`}> 485 <Text 486 testID="quoteCount-expanded" 487 style={[a.text_md, t.atoms.text_contrast_medium]}> 488 <Trans comment="Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)"> 489 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 490 {formatPostStatCount(post.quoteCount)} 491 </Text>{' '} 492 <Plural 493 value={post.quoteCount} 494 one="quote" 495 other="quotes" 496 /> 497 </Trans> 498 </Text> 499 </Link> 500 ) : null} 501 {post.likeCount != null && 502 post.likeCount !== 0 && 503 !disableLikesMetrics ? ( 504 <Link to={likesHref} label={l`Likes on this post`}> 505 <Text 506 testID="likeCount-expanded" 507 style={[a.text_md, t.atoms.text_contrast_medium]}> 508 <Trans comment="Like count display, the <0> tags enclose the number of likes in bold (will never be 0)"> 509 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 510 {formatPostStatCount(post.likeCount)} 511 </Text>{' '} 512 <Plural value={post.likeCount} one="like" other="likes" /> 513 </Trans> 514 </Text> 515 </Link> 516 ) : null} 517 {post.bookmarkCount != null && 518 post.bookmarkCount !== 0 && 519 !disableSavesMetrics ? ( 520 <Text 521 testID="bookmarkCount-expanded" 522 style={[a.text_md, t.atoms.text_contrast_medium]}> 523 <Trans comment="Save count display, the <0> tags enclose the number of saves in bold (will never be 0)"> 524 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}> 525 {formatPostStatCount(post.bookmarkCount)} 526 </Text>{' '} 527 <Plural 528 value={post.bookmarkCount} 529 one="save" 530 other="saves" 531 /> 532 </Trans> 533 </Text> 534 ) : null} 535 </View> 536 ) : null} 537 <View 538 style={[ 539 a.pt_sm, 540 a.pb_2xs, 541 { 542 marginLeft: -5, 543 }, 544 ]}> 545 <FeedFeedbackProvider value={feedFeedback}> 546 <PostControls 547 big 548 post={postShadow} 549 record={record} 550 richText={richText} 551 onPressReply={onPressReply} 552 logContext="PostThreadItem" 553 threadgateRecord={threadgateRecord} 554 feedContext={postSource?.post?.feedContext} 555 reqId={postSource?.post?.reqId} 556 viaRepost={viaRepost} 557 /> 558 </FeedFeedbackProvider> 559 </View> 560 <DebugFieldDisplay subject={post} /> 561 </View> 562 </View> 563 </> 564 ) 565}) 566 567function ExpandedPostDetails({ 568 post, 569 isThreadAuthor, 570}: { 571 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post'] 572 isThreadAuthor: boolean 573}) { 574 const t = useTheme() 575 const {i18n} = useLingui() 576 const isRootPost = !('reply' in post.record) 577 578 return ( 579 <View style={[a.gap_md, a.pt_md, a.align_start]}> 580 <BackdatedPostIndicator post={post} /> 581 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}> 582 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 583 {niceDate(i18n, post.indexedAt, 'dot separated')} 584 </Text> 585 {isRootPost && ( 586 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 587 )} 588 </View> 589 </View> 590 ) 591} 592 593function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { 594 const t = useTheme() 595 const {t: l, i18n} = useLingui() 596 const control = Prompt.usePromptControl() 597 const enableSquareButtons = useEnableSquareButtons() 598 599 const indexedAt = new Date(post.indexedAt) 600 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>( 601 post.record, 602 AppBskyFeedPost.isRecord, 603 ) 604 ? new Date(post.record.createdAt) 605 : new Date(post.indexedAt) 606 607 // backdated if createdAt is 24 hours or more before indexedAt 608 const isBackdated = 609 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000 610 611 if (!isBackdated) return null 612 613 return ( 614 <> 615 <Button 616 label={l`Archived post`} 617 accessibilityHint={l`Shows information about when this post was created`} 618 onPress={e => { 619 e.preventDefault() 620 e.stopPropagation() 621 control.open() 622 }}> 623 {({hovered, pressed}) => ( 624 <View 625 style={[ 626 a.flex_row, 627 a.align_center, 628 enableSquareButtons ? a.rounded_sm : a.rounded_full, 629 t.atoms.bg_contrast_25, 630 (hovered || pressed) && t.atoms.bg_contrast_50, 631 { 632 gap: 3, 633 paddingHorizontal: 6, 634 paddingVertical: 3, 635 }, 636 ]}> 637 <CalendarClockIcon fill={t.palette.yellow} size="sm" aria-hidden /> 638 <Text 639 style={[ 640 a.text_xs, 641 a.font_semi_bold, 642 a.leading_tight, 643 t.atoms.text_contrast_medium, 644 ]}> 645 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans> 646 </Text> 647 </View> 648 )} 649 </Button> 650 651 <Prompt.Outer control={control}> 652 <Prompt.Content> 653 <Prompt.TitleText> 654 <Trans>Archived post</Trans> 655 </Prompt.TitleText> 656 <Prompt.DescriptionText> 657 <Trans> 658 This post claims to have been created on{' '} 659 <RNText style={[a.font_semi_bold]}> 660 {niceDate(i18n, createdAt)} 661 </RNText> 662 , but was first seen by Bluesky on{' '} 663 <RNText style={[a.font_semi_bold]}> 664 {niceDate(i18n, indexedAt)} 665 </RNText> 666 . 667 </Trans> 668 </Prompt.DescriptionText> 669 <Prompt.DescriptionText> 670 <Trans> 671 Bluesky cannot confirm the authenticity of the claimed date. 672 </Trans> 673 </Prompt.DescriptionText> 674 </Prompt.Content> 675 <Prompt.Actions> 676 <Prompt.Action cta={l`Okay`} onPress={() => {}} /> 677 </Prompt.Actions> 678 </Prompt.Outer> 679 </> 680 ) 681} 682 683function getThreadAuthor( 684 post: AppBskyFeedDefs.PostView, 685 record: AppBskyFeedPost.Record, 686): string { 687 if (!record.reply) { 688 return post.author.did 689 } 690 try { 691 return new AtUri(record.reply.root.uri).host 692 } catch { 693 return '' 694 } 695} 696 697export function ThreadItemAnchorSkeleton() { 698 return ( 699 <View style={[a.p_lg, a.gap_md]}> 700 <Skele.Row style={[a.align_center, a.gap_md]}> 701 <Skele.Circle size={42} /> 702 703 <Skele.Col> 704 <Skele.Text style={[a.text_lg, {width: '20%'}]} /> 705 <Skele.Text blend style={[a.text_md, {width: '40%'}]} /> 706 </Skele.Col> 707 </Skele.Row> 708 709 <View> 710 <Skele.Text style={[a.text_xl, {width: '100%'}]} /> 711 <Skele.Text style={[a.text_xl, {width: '60%'}]} /> 712 </View> 713 714 <Skele.Text style={[a.text_sm, {width: '50%'}]} /> 715 716 <PostControlsSkeleton big /> 717 </View> 718 ) 719}