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