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