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