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, 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 ·
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}