Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 742 lines 25 kB view raw
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}