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