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