forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
1import {memo, useMemo} from 'react'
2import {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 {Plural, Trans, useLingui} from '@lingui/react/macro'
11
12import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
13import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
14import {makeProfileLink} from '#/lib/routes/links'
15import {sanitizeDisplayName} from '#/lib/strings/display-names'
16import {sanitizeHandle} from '#/lib/strings/handles'
17import {niceDate} from '#/lib/strings/time'
18import {
19 POST_TOMBSTONE,
20 type Shadow,
21 usePostShadow,
22} from '#/state/cache/post-shadow'
23import {useProfileShadow} from '#/state/cache/profile-shadow'
24import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
25import {useDisableLikesMetrics} from '#/state/preferences/disable-likes-metrics'
26import {useDisableQuotesMetrics} from '#/state/preferences/disable-quotes-metrics'
27import {useDisableRepostsMetrics} from '#/state/preferences/disable-reposts-metrics'
28import {useDisableSavesMetrics} from '#/state/preferences/disable-saves-metrics'
29import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
30import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
31import {useHideScaryFollowButtons} from '#/state/preferences/hide-scary-follow-buttons'
32import {useShowViaClient} from '#/state/preferences/show-via-client'
33import {type ThreadItem} from '#/state/queries/usePostThread/types'
34import {useSession} from '#/state/session'
35import {type OnPostSuccessData} from '#/state/shell/composer'
36import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
37import {type PostSource} from '#/state/unstable-post-source'
38import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
39import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
40import {
41 LINEAR_AVI_WIDTH,
42 OUTER_SPACE,
43 REPLY_LINE_WIDTH,
44} from '#/screens/PostThread/const'
45import {atoms as a, useTheme} from '#/alf'
46import {Button} from '#/components/Button'
47import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
48import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
49import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
50import {GalleryBleed} from '#/components/images/Gallery'
51import {Link} from '#/components/Link'
52import {ContentHider} from '#/components/moderation/ContentHider'
53import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
54import {PostAlerts} from '#/components/moderation/PostAlerts'
55import {type AppModerationCause} from '#/components/Pills'
56import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
57import {TranslatedPost} from '#/components/Post/Translated'
58import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
59import {useFormatPostStatCount} from '#/components/PostControls/util'
60import {ProfileBadges} from '#/components/ProfileBadges'
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 {WhoCanReply} from '#/components/WhoCanReply'
67import {useAnalytics} from '#/analytics'
68import {useActorStatus} from '#/features/liveNow'
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
107 return (
108 <>
109 <ThreadItemAnchorParentReplyLine isRoot={isRoot} />
110
111 <View
112 style={[
113 {
114 paddingHorizontal: OUTER_SPACE,
115 paddingBottom: OUTER_SPACE,
116 },
117 isRoot && [a.pt_lg],
118 ]}>
119 <View
120 style={[
121 a.flex_row,
122 a.align_center,
123 a.py_md,
124 a.rounded_sm,
125 t.atoms.bg_contrast_25,
126 ]}>
127 <View
128 style={[
129 a.flex_row,
130 a.align_center,
131 a.justify_center,
132 {
133 width: LINEAR_AVI_WIDTH,
134 },
135 ]}>
136 <TrashIcon style={[t.atoms.text_contrast_medium]} />
137 </View>
138 <Text
139 style={[a.text_md, a.font_semi_bold, t.atoms.text_contrast_medium]}>
140 <Trans>Post has been deleted</Trans>
141 </Text>
142 </View>
143 </View>
144 </>
145 )
146}
147
148function ThreadItemAnchorParentReplyLine({isRoot}: {isRoot: boolean}) {
149 const t = useTheme()
150
151 return !isRoot ? (
152 <View style={[a.pl_lg, a.flex_row, a.pb_xs, {height: a.pt_lg.paddingTop}]}>
153 <View style={{width: 42}}>
154 <View
155 style={[
156 {
157 width: REPLY_LINE_WIDTH,
158 marginLeft: 'auto',
159 marginRight: 'auto',
160 flexGrow: 1,
161 backgroundColor: t.atoms.border_contrast_low.borderColor,
162 },
163 ]}
164 />
165 </View>
166 </View>
167 ) : null
168}
169
170const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
171 item,
172 isRoot,
173 postShadow,
174 onPostSuccess,
175 threadgateRecord,
176 postSource,
177}: {
178 item: Extract<ThreadItem, {type: 'threadPost'}>
179 isRoot: boolean
180 postShadow: Shadow<AppBskyFeedDefs.PostView>
181 onPostSuccess?: (data: OnPostSuccessData) => void
182 threadgateRecord?: AppBskyFeedThreadgate.Record
183 postSource?: PostSource
184}) {
185 const t = useTheme()
186 const ax = useAnalytics()
187 const {t: l} = useLingui()
188 const {openComposer} = useOpenComposer()
189 const {currentAccount, hasSession} = useSession()
190 const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
191 const formatPostStatCount = useFormatPostStatCount()
192
193 const post = postShadow
194 const record = item.value.post.record
195 const moderation = item.moderation
196 const authorShadow = useProfileShadow(post.author)
197 const {isActive: live} = useActorStatus(post.author)
198 const richText = useMemo(
199 () =>
200 new RichTextAPI({
201 text: record.text,
202 facets: record.facets,
203 }),
204 [record],
205 )
206
207 const threadRootUri = record.reply?.root?.uri || post.uri
208 const authorHref = makeProfileLink(post.author)
209 const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
210
211 // disable metrics
212 const disableLikesMetrics = useDisableLikesMetrics()
213 const disableRepostsMetrics = useDisableRepostsMetrics()
214 const disableQuotesMetrics = useDisableQuotesMetrics()
215 const disableSavesMetrics = useDisableSavesMetrics()
216
217 const likesHref = useMemo(() => {
218 const urip = new AtUri(post.uri)
219 return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
220 }, [post.uri, post.author])
221 const repostsHref = useMemo(() => {
222 const urip = new AtUri(post.uri)
223 return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
224 }, [post.uri, post.author])
225 const quotesHref = useMemo(() => {
226 const urip = new AtUri(post.uri)
227 return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
228 }, [post.uri, post.author])
229
230 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
231 threadgateRecord,
232 })
233 const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
234 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
235 const isControlledByViewer =
236 new AtUri(threadRootUri).host === currentAccount?.did
237 return isControlledByViewer && isPostHiddenByThreadgate
238 ? [
239 {
240 type: 'reply-hidden',
241 source: {type: 'user', did: currentAccount?.did},
242 priority: 6,
243 },
244 ]
245 : []
246 }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri])
247 const onlyFollowersCanReply = !!threadgateRecord?.allow?.find(
248 rule => rule.$type === 'app.bsky.feed.threadgate#followerRule',
249 )
250 const hideScaryFollowButtons = useHideScaryFollowButtons()
251 const showFollowButton =
252 currentAccount?.did !== post.author.did &&
253 !onlyFollowersCanReply &&
254 !hideScaryFollowButtons
255
256 const viaRepost = useMemo(() => {
257 const reason = postSource?.post.reason
258
259 if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
260 return {
261 uri: reason.uri,
262 cid: reason.cid,
263 }
264 }
265 }, [postSource])
266
267 const onPressReply = useNonReactiveCallback(() => {
268 openComposer({
269 replyTo: {
270 uri: post.uri,
271 cid: post.cid,
272 text: record.text,
273 author: post.author,
274 embed: post.embed,
275 moderation,
276 langs: record.langs,
277 },
278 onPostSuccess: onPostSuccess,
279 logContext: 'PostReply',
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
292 const onOpenAuthor = () => {
293 ax.metric('post:clickthroughAuthor', {
294 uri: post.uri,
295 authorDid: post.author.did,
296 logContext: 'PostThreadItem',
297 feedDescriptor: feedFeedback.feedDescriptor,
298 })
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 ax.metric('post:clickthroughEmbed', {
311 uri: post.uri,
312 authorDid: post.author.did,
313 logContext: 'PostThreadItem',
314 feedDescriptor: feedFeedback.feedDescriptor,
315 })
316 if (postSource) {
317 feedFeedback.sendInteraction({
318 item: post.uri,
319 event: 'app.bsky.feed.defs#clickthroughEmbed',
320 feedContext: postSource.post.feedContext,
321 reqId: postSource.post.reqId,
322 })
323 }
324 }
325
326 return (
327 <>
328 <ThreadItemAnchorParentReplyLine isRoot={isRoot} />
329 <GalleryBleed>
330 <View
331 testID={`postThreadItem-by-${post.author.handle}`}
332 style={[
333 {
334 paddingHorizontal: OUTER_SPACE,
335 },
336 isRoot && [a.pt_lg],
337 ]}>
338 <View style={[a.flex_row, a.gap_md, a.pb_md]}>
339 <View collapsable={false}>
340 <PreviewableUserAvatar
341 size={42}
342 profile={post.author}
343 moderation={moderation.ui('avatar')}
344 type={post.author.associated?.labeler ? 'labeler' : 'user'}
345 live={live}
346 onBeforePress={onOpenAuthor}
347 />
348 </View>
349 <Link
350 to={authorHref}
351 style={[a.flex_1]}
352 label={sanitizeDisplayName(
353 post.author.displayName || sanitizeHandle(post.author.handle),
354 moderation.ui('displayName'),
355 )}
356 onPress={onOpenAuthor}>
357 <View style={[a.flex_1, a.align_start]}>
358 <ProfileHoverCard did={post.author.did} style={[a.w_full]}>
359 <View style={[a.flex_row, a.align_center]}>
360 <Text
361 emoji
362 style={[
363 a.flex_shrink,
364 a.text_lg,
365 a.font_semi_bold,
366 a.leading_snug,
367 ]}
368 numberOfLines={1}>
369 {sanitizeDisplayName(
370 post.author.displayName ||
371 sanitizeHandle(post.author.handle),
372 moderation.ui('displayName'),
373 )}
374 </Text>
375
376 <View
377 style={[a.pl_xs, a.flex_row, a.gap_2xs, a.align_center]}>
378 <ProfileBadges
379 profile={authorShadow}
380 size="md"
381 interactive
382 />
383 </View>
384 </View>
385 <Text
386 style={[
387 a.text_md,
388 a.leading_snug,
389 t.atoms.text_contrast_medium,
390 ]}
391 numberOfLines={1}>
392 {sanitizeHandle(post.author.handle, '@')}
393 </Text>
394 </ProfileHoverCard>
395 </View>
396 </Link>
397 <View collapsable={false} style={[a.self_center]}>
398 <ThreadItemAnchorFollowButton
399 did={post.author.did}
400 enabled={showFollowButton}
401 />
402 </View>
403 </View>
404 <View style={[a.pb_sm]}>
405 <LabelsOnMyPost post={post} style={[a.pb_sm]} />
406 <ContentHider
407 modui={moderation.ui('contentView')}
408 ignoreMute
409 childContainerStyle={[a.pt_sm]}>
410 <PostAlerts
411 modui={moderation.ui('contentView')}
412 size="lg"
413 includeMute
414 style={[a.pb_sm]}
415 additionalCauses={additionalPostAlerts}
416 />
417 {richText?.text ? (
418 <RichText
419 enableTags
420 selectable
421 value={richText}
422 style={[a.flex_1, a.text_lg]}
423 authorHandle={post.author.handle}
424 shouldProxyLinks={true}
425 />
426 ) : undefined}
427 <TranslatedPost post={post} postTextStyle={[a.text_lg]} />
428 {post.embed && (
429 <View style={[richText?.text ? a.py_xs : []]}>
430 <Embed
431 embed={post.embed}
432 moderation={moderation}
433 viewContext={PostEmbedViewContext.ThreadHighlighted}
434 onOpen={onOpenEmbed}
435 />
436 </View>
437 )}
438 </ContentHider>
439 <ExpandedPostDetails
440 post={item.value.post}
441 isThreadAuthor={isThreadAuthor}
442 />
443 {(post.repostCount !== 0 && !disableRepostsMetrics) ||
444 (post.likeCount !== 0 && !disableLikesMetrics) ||
445 (post.quoteCount !== 0 && !disableQuotesMetrics) ||
446 (post.bookmarkCount !== 0 && !disableSavesMetrics) ? (
447 // Show this section unless we're *sure* it has no engagement.
448 <View
449 style={[
450 a.flex_row,
451 a.flex_wrap,
452 a.align_center,
453 {
454 rowGap: a.gap_sm.gap,
455 columnGap: a.gap_lg.gap,
456 },
457 a.border_t,
458 a.border_b,
459 a.mt_md,
460 a.py_md,
461 t.atoms.border_contrast_low,
462 ]}>
463 {post.repostCount != null &&
464 post.repostCount !== 0 &&
465 !disableRepostsMetrics ? (
466 <Link to={repostsHref} label={l`Reposts of this post`}>
467 <Text
468 testID="repostCount-expanded"
469 style={[a.text_md, t.atoms.text_contrast_medium]}>
470 <Trans comment="Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)">
471 <Text
472 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="repost"
478 other="reposts"
479 />
480 </Trans>
481 </Text>
482 </Link>
483 ) : null}
484 {post.quoteCount != null &&
485 post.quoteCount !== 0 &&
486 !post.viewer?.embeddingDisabled &&
487 !disableQuotesMetrics ? (
488 <Link to={quotesHref} label={l`Quotes of this post`}>
489 <Text
490 testID="quoteCount-expanded"
491 style={[a.text_md, t.atoms.text_contrast_medium]}>
492 <Trans comment="Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)">
493 <Text
494 style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
495 {formatPostStatCount(post.quoteCount)}
496 </Text>{' '}
497 <Plural
498 value={post.quoteCount}
499 one="quote"
500 other="quotes"
501 />
502 </Trans>
503 </Text>
504 </Link>
505 ) : null}
506 {post.likeCount != null &&
507 post.likeCount !== 0 &&
508 !disableLikesMetrics ? (
509 <Link to={likesHref} label={l`Likes on this post`}>
510 <Text
511 testID="likeCount-expanded"
512 style={[a.text_md, t.atoms.text_contrast_medium]}>
513 <Trans comment="Like count display, the <0> tags enclose the number of likes in bold (will never be 0)">
514 <Text
515 style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
516 {formatPostStatCount(post.likeCount)}
517 </Text>{' '}
518 <Plural
519 value={post.likeCount}
520 one="like"
521 other="likes"
522 />
523 </Trans>
524 </Text>
525 </Link>
526 ) : null}
527 {post.bookmarkCount != null &&
528 post.bookmarkCount !== 0 &&
529 !disableSavesMetrics ? (
530 <Text
531 testID="bookmarkCount-expanded"
532 style={[a.text_md, t.atoms.text_contrast_medium]}>
533 <Trans comment="Save count display, the <0> tags enclose the number of saves in bold (will never be 0)">
534 <Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
535 {formatPostStatCount(post.bookmarkCount)}
536 </Text>{' '}
537 <Plural
538 value={post.bookmarkCount}
539 one="save"
540 other="saves"
541 />
542 </Trans>
543 </Text>
544 ) : null}
545 </View>
546 ) : null}
547 <View
548 style={[
549 a.pt_sm,
550 a.pb_2xs,
551 {
552 marginLeft: -5,
553 },
554 ]}>
555 <FeedFeedbackProvider value={feedFeedback}>
556 <PostControls
557 big
558 post={postShadow}
559 record={record}
560 richText={richText}
561 onPressReply={onPressReply}
562 logContext="PostThreadItem"
563 threadgateRecord={threadgateRecord}
564 feedContext={postSource?.post?.feedContext}
565 reqId={postSource?.post?.reqId}
566 viaRepost={viaRepost}
567 />
568 </FeedFeedbackProvider>
569 </View>
570 <DebugFieldDisplay subject={post} />
571 </View>
572 </View>
573 </GalleryBleed>
574 </>
575 )
576})
577
578function ExpandedPostDetails({
579 post,
580 isThreadAuthor,
581}: {
582 post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
583 isThreadAuthor: boolean
584}) {
585 const t = useTheme()
586 const {i18n} = useLingui()
587 const showViaClient = useShowViaClient()
588 const isRootPost = !('reply' in post.record)
589 const via = post.record.via as string | undefined
590
591 return (
592 <View style={[a.gap_md, a.pt_md, a.align_start]}>
593 <BackdatedPostIndicator post={post} />
594 <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
595 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
596 {niceDate(i18n, post.indexedAt, 'dot separated')}
597 {showViaClient && via ? ` · ${truncateVia(via)}` : null}
598 </Text>
599 {isRootPost && (
600 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
601 )}
602 </View>
603 </View>
604 )
605}
606
607function truncateVia(via: string) {
608 return via.length > 24 ? `${via.slice(0, 23)}…` : via
609}
610
611function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
612 const t = useTheme()
613 const {t: l, i18n} = useLingui()
614 const control = Prompt.usePromptControl()
615 const enableSquareButtons = useEnableSquareButtons()
616
617 const indexedAt = new Date(post.indexedAt)
618 const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
619 post.record,
620 AppBskyFeedPost.isRecord,
621 )
622 ? new Date(post.record.createdAt)
623 : new Date(post.indexedAt)
624
625 // backdated if createdAt is 24 hours or more before indexedAt
626 const isBackdated =
627 indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
628
629 if (!isBackdated) return null
630
631 return (
632 <>
633 <Button
634 label={l`Archived post`}
635 accessibilityHint={l`Shows information about when this post was created`}
636 onPress={e => {
637 e.preventDefault()
638 e.stopPropagation()
639 control.open()
640 }}>
641 {({hovered, pressed}) => (
642 <View
643 style={[
644 a.flex_row,
645 a.align_center,
646 enableSquareButtons ? a.rounded_sm : a.rounded_full,
647 t.atoms.bg_contrast_25,
648 (hovered || pressed) && t.atoms.bg_contrast_50,
649 {
650 gap: 3,
651 paddingHorizontal: 6,
652 paddingVertical: 3,
653 },
654 ]}>
655 <CalendarClockIcon fill={t.palette.yellow} size="sm" aria-hidden />
656 <Text
657 style={[
658 a.text_xs,
659 a.font_semi_bold,
660 a.leading_tight,
661 t.atoms.text_contrast_medium,
662 ]}>
663 <Trans>Archived from {niceDate(i18n, createdAt, 'medium')}</Trans>
664 </Text>
665 </View>
666 )}
667 </Button>
668
669 <Prompt.Outer control={control}>
670 <Prompt.Content>
671 <Prompt.TitleText>
672 <Trans>Archived post</Trans>
673 </Prompt.TitleText>
674 <Prompt.DescriptionText>
675 <Trans>
676 This post claims to have been created on{' '}
677 <RNText style={[a.font_semi_bold]}>
678 {niceDate(i18n, createdAt)}
679 </RNText>
680 , but was first seen by Bluesky on{' '}
681 <RNText style={[a.font_semi_bold]}>
682 {niceDate(i18n, indexedAt)}
683 </RNText>
684 .
685 </Trans>
686 </Prompt.DescriptionText>
687 <Prompt.DescriptionText>
688 <Trans>
689 Bluesky cannot confirm the authenticity of the claimed date.
690 </Trans>
691 </Prompt.DescriptionText>
692 </Prompt.Content>
693 <Prompt.Actions>
694 <Prompt.Action cta={l`Okay`} onPress={() => {}} />
695 </Prompt.Actions>
696 </Prompt.Outer>
697 </>
698 )
699}
700
701function getThreadAuthor(
702 post: AppBskyFeedDefs.PostView,
703 record: AppBskyFeedPost.Record,
704): string {
705 if (!record.reply) {
706 return post.author.did
707 }
708 try {
709 return new AtUri(record.reply.root.uri).host
710 } catch {
711 return ''
712 }
713}
714
715export function ThreadItemAnchorSkeleton() {
716 const enableSquareAvatars = useEnableSquareAvatars()
717
718 return (
719 <View style={[a.p_lg, a.gap_md]}>
720 <Skele.Row style={[a.align_center, a.gap_md]}>
721 <Skele.Circle
722 size={42}
723 style={enableSquareAvatars && {borderRadius: 8}}
724 />
725
726 <Skele.Col>
727 <Skele.Text style={[a.text_lg, {width: '20%'}]} />
728 <Skele.Text blend style={[a.text_md, {width: '40%'}]} />
729 </Skele.Col>
730 </Skele.Row>
731
732 <View>
733 <Skele.Text style={[a.text_xl, {width: '100%'}]} />
734 <Skele.Text style={[a.text_xl, {width: '60%'}]} />
735 </View>
736
737 <Skele.Text style={[a.text_sm, {width: '50%'}]} />
738
739 <PostControlsSkeleton big />
740 </View>
741 )
742}