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