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