Add known likers to feed posts (#11595)
This commit is contained in:
@@ -77,6 +77,22 @@ export function getFeatures() {
|
|||||||
|
|
||||||
export function getFeatureDescription(feature: Features, i18n: I18n) {
|
export function getFeatureDescription(feature: Features, i18n: I18n) {
|
||||||
switch (feature) {
|
switch (feature) {
|
||||||
|
case Features.PostFeedKnownLikersEnable:
|
||||||
|
return {
|
||||||
|
key: feature,
|
||||||
|
name: i18n._(
|
||||||
|
msg({
|
||||||
|
message: 'Social proofing on posts',
|
||||||
|
comment: 'Name for a feature flag',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
description: i18n._(
|
||||||
|
msg({
|
||||||
|
message: 'Spot posts your friends and follows have liked.',
|
||||||
|
comment: 'Description of a feature flag (Social proofing on posts)',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
case Features.CanonicalPostNumberingEnable:
|
case Features.CanonicalPostNumberingEnable:
|
||||||
return {
|
return {
|
||||||
key: feature,
|
key: feature,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export enum Features {
|
|||||||
GroupChatsDisable = 'group_chats:disable',
|
GroupChatsDisable = 'group_chats:disable',
|
||||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||||
|
PostFeedKnownLikersEnable = 'post_feed:known_likers:enable',
|
||||||
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
||||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||||
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ export function AvatarStack({
|
|||||||
size = 26,
|
size = 26,
|
||||||
numPending,
|
numPending,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
borderWidth = 1,
|
||||||
|
overlap,
|
||||||
}: {
|
}: {
|
||||||
profiles: bsky.profile.AnyProfileView[]
|
profiles: bsky.profile.AnyProfileView[]
|
||||||
size?: number
|
size?: number
|
||||||
numPending?: number
|
numPending?: number
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
|
borderWidth?: number
|
||||||
|
overlap?: number
|
||||||
}) {
|
}) {
|
||||||
const translation = size / 3 // overlap by 1/3
|
const translation = overlap ?? size / 3
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
@@ -55,7 +59,7 @@ export function AvatarStack({
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
left: i * -translation,
|
left: i * -translation,
|
||||||
borderWidth: 1,
|
borderWidth,
|
||||||
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
zIndex: 3 - i,
|
zIndex: 3 - i,
|
||||||
@@ -63,7 +67,7 @@ export function AvatarStack({
|
|||||||
]}>
|
]}>
|
||||||
{item.profile && (
|
{item.profile && (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size={size - 2}
|
size={size - borderWidth * 2}
|
||||||
avatar={item.profile.avatar}
|
avatar={item.profile.avatar}
|
||||||
type={item.profile.associated?.labeler ? 'labeler' : 'user'}
|
type={item.profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
moderation={item.moderation.ui('avatar')}
|
moderation={item.moderation.ui('avatar')}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {AtUri} from '@atproto/syntax'
|
||||||
|
import {moderateProfile} from '@bsky/sdk/moderation'
|
||||||
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {AvatarStack} from '#/components/AvatarStack'
|
||||||
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {type Features, useAnalytics} from '#/analytics'
|
||||||
|
import {type app} from '#/lexicons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social proof for a post. When the viewer follows some of the post's recent
|
||||||
|
* likers, renders a face pile plus "Liked by A and B". Renders nothing when
|
||||||
|
* the feature is disabled or no visible known likers are available.
|
||||||
|
*/
|
||||||
|
export function KnownLikers({
|
||||||
|
post,
|
||||||
|
feature,
|
||||||
|
variant = 'thread',
|
||||||
|
}: {
|
||||||
|
post: app.bsky.feed.defs.PostView
|
||||||
|
feature: Features
|
||||||
|
variant?: 'feed' | 'thread'
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
|
const knownLikersAndModeration = moderationOpts
|
||||||
|
? (post.viewer?.knownLikers?.actors ?? [])
|
||||||
|
.map(actor => ({
|
||||||
|
actor,
|
||||||
|
moderation: moderateProfile(actor, moderationOpts),
|
||||||
|
}))
|
||||||
|
.filter(({moderation}) => !moderation.ui('profileList').filter)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (knownLikersAndModeration.length === 0 || !ax.features.enabled(feature)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const urip = new AtUri(post.uri)
|
||||||
|
const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
|
||||||
|
const aviStackProfiles = knownLikersAndModeration
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(({actor}) => actor)
|
||||||
|
const names = knownLikersAndModeration
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(({actor, moderation}) => ({
|
||||||
|
did: actor.did,
|
||||||
|
href: makeProfileLink(actor),
|
||||||
|
displayName: sanitizeDisplayName(
|
||||||
|
actor.displayName || actor.handle,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
const isFeed = variant === 'feed'
|
||||||
|
const rowLabel =
|
||||||
|
names.length >= 2
|
||||||
|
? l`Liked by ${names[0].displayName} and ${names[1].displayName}`
|
||||||
|
: l`Liked by ${names[0].displayName}`
|
||||||
|
const textStyle = [a.text_sm, t.atoms.text_contrast_medium]
|
||||||
|
const nameStyle = [a.text_sm, a.font_medium, t.atoms.text_contrast_medium]
|
||||||
|
|
||||||
|
const nameLink = (name: (typeof names)[number]) => (
|
||||||
|
<ProfileHoverCard key={name.did} did={name.did} inline>
|
||||||
|
<InlineLinkText
|
||||||
|
to={name.href}
|
||||||
|
label={l`Go to ${name.displayName}'s profile`}
|
||||||
|
disableMismatchWarning
|
||||||
|
emoji
|
||||||
|
style={nameStyle}>
|
||||||
|
{name.displayName}
|
||||||
|
</InlineLinkText>
|
||||||
|
</ProfileHoverCard>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full, a.flex_row, isFeed && a.mt_sm]}>
|
||||||
|
<Link
|
||||||
|
to={likesHref}
|
||||||
|
label={rowLabel}
|
||||||
|
style={[a.flex_row, a.align_center, a.gap_xs, a.flex_shrink]}
|
||||||
|
onPress={() => ax.metric('post:likedBy:click', {})}>
|
||||||
|
<AvatarStack
|
||||||
|
profiles={aviStackProfiles}
|
||||||
|
size={16}
|
||||||
|
overlap={4}
|
||||||
|
borderWidth={0.5}
|
||||||
|
backgroundColor={t.atoms.bg_contrast_25.backgroundColor}
|
||||||
|
/>
|
||||||
|
<Text testID="knownLikersStat" style={[a.flex_shrink, textStyle]}>
|
||||||
|
{names.length >= 2 ? (
|
||||||
|
<Trans comment="Social proof below a post; the bolded names are people the viewer follows who liked the post">
|
||||||
|
Liked by {nameLink(names[0])} and {nameLink(names[1])}
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans comment="Social proof below a post; the bolded name is a person the viewer follows who liked the post">
|
||||||
|
Liked by {nameLink(names[0])}
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -114,7 +114,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_xs,
|
{gap: 3},
|
||||||
(hovered || focused || pressed) && native({opacity: 0.5}),
|
(hovered || focused || pressed) && native({opacity: 0.5}),
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
@@ -122,16 +122,16 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
color={
|
color={
|
||||||
isThreadAuthor ? t.palette.primary_500 : t.palette.contrast_400
|
isThreadAuthor ? t.palette.primary_500 : t.palette.contrast_400
|
||||||
}
|
}
|
||||||
width={16}
|
width={12}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_xs,
|
||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
isThreadAuthor
|
isThreadAuthor
|
||||||
? {color: t.palette.primary_500}
|
? {color: t.palette.primary_500}
|
||||||
: t.atoms.text_contrast_medium,
|
: t.atoms.text_contrast_high,
|
||||||
(hovered || focused || pressed) && web(a.underline),
|
(hovered || focused || pressed) && web(a.underline),
|
||||||
]}>
|
]}>
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import {View} from 'react-native'
|
|
||||||
import {AtUri} from '@atproto/syntax'
|
import {AtUri} from '@atproto/syntax'
|
||||||
import {moderateProfile} from '@bsky/sdk/moderation'
|
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {AvatarStack} from '#/components/AvatarStack'
|
import {Link} from '#/components/Link'
|
||||||
import {InlineLinkText, Link} from '#/components/Link'
|
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {type app} from '#/lexicons'
|
import {type app} from '#/lexicons'
|
||||||
|
|
||||||
const AVI_SIZE = 20
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The plain "N likes" stat for the expanded anchor post, linking to the likes
|
* The plain "N likes" stat for the expanded anchor post, linking to the likes
|
||||||
* list. Renders nothing when the post has no likes.
|
* list. Renders nothing when the post has no likes.
|
||||||
@@ -40,9 +32,9 @@ export function LikesStat({post}: {post: app.bsky.feed.defs.PostView}) {
|
|||||||
onPress={() => ax.metric('post:likedBy:click', {})}>
|
onPress={() => ax.metric('post:likedBy:click', {})}>
|
||||||
<Text
|
<Text
|
||||||
testID="likeCount-expanded"
|
testID="likeCount-expanded"
|
||||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||||
<Trans comment="Like count display, the <0> tags enclose the number of likes in bold (will never be 0)">
|
<Trans comment="Like count display, the <0> tags enclose the number of likes in bold (will never be 0)">
|
||||||
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||||
{formatPostStatCount(likeCount)}
|
{formatPostStatCount(likeCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural value={likeCount} one="like" other="likes" />
|
<Plural value={likeCount} one="like" other="likes" />
|
||||||
@@ -51,115 +43,3 @@ export function LikesStat({post}: {post: app.bsky.feed.defs.PostView}) {
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Social proof for the expanded anchor post. When the viewer follows some of
|
|
||||||
* the post's recent likers, renders a face pile plus "Liked by A and B" on
|
|
||||||
* its own row below the interaction stats line. Renders nothing otherwise.
|
|
||||||
*
|
|
||||||
* Known likers are included in the post viewer state so this can render with
|
|
||||||
* the rest of the thread, without a separate request or layout shift.
|
|
||||||
*/
|
|
||||||
export function KnownLikers({post}: {post: app.bsky.feed.defs.PostView}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
const ax = useAnalytics()
|
|
||||||
|
|
||||||
const likeCount = post.likeCount ?? 0
|
|
||||||
if (likeCount === 0) return null
|
|
||||||
|
|
||||||
const urip = new AtUri(post.uri)
|
|
||||||
const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
|
|
||||||
const onPressLikedBy = () => ax.metric('post:likedBy:click', {})
|
|
||||||
|
|
||||||
const knownLikersAndModeration = moderationOpts
|
|
||||||
? (post.viewer?.knownLikers?.actors ?? [])
|
|
||||||
.map(actor => ({
|
|
||||||
actor,
|
|
||||||
moderation: moderateProfile(actor, moderationOpts),
|
|
||||||
}))
|
|
||||||
.filter(({moderation}) => {
|
|
||||||
const modui = moderation.ui('profileList')
|
|
||||||
return !modui.filter
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
|
|
||||||
const showKnownLikers =
|
|
||||||
knownLikersAndModeration.length > 0 &&
|
|
||||||
ax.features.enabled(ax.features.PostThreadKnownLikersEnable)
|
|
||||||
|
|
||||||
if (!showKnownLikers) return null
|
|
||||||
|
|
||||||
const aviStackProfiles = knownLikersAndModeration
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(({actor}) => actor)
|
|
||||||
const names = knownLikersAndModeration
|
|
||||||
.slice(0, 2)
|
|
||||||
.map(({actor, moderation}) => {
|
|
||||||
return {
|
|
||||||
did: actor.did,
|
|
||||||
href: makeProfileLink(actor),
|
|
||||||
displayName: sanitizeDisplayName(
|
|
||||||
actor.displayName || actor.handle,
|
|
||||||
moderation.ui('displayName'),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
* The row link's a11y label mirrors the visible sentence so screen readers
|
|
||||||
* announce the social proof.
|
|
||||||
*/
|
|
||||||
const rowLabel =
|
|
||||||
names.length >= 2
|
|
||||||
? l`Liked by ${names[0].displayName} and ${names[1].displayName}`
|
|
||||||
: l`Liked by ${names[0].displayName}`
|
|
||||||
|
|
||||||
const textStyle = [a.text_sm, t.atoms.text_contrast_medium]
|
|
||||||
const nameStyle = [a.text_sm, a.font_semi_bold, t.atoms.text]
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Nested inside the row link, but the deepest link claims the press, so
|
|
||||||
* tapping a name goes to that profile while the rest of the row goes to
|
|
||||||
* the likes list. Same pattern as NotificationFeedItem's author links.
|
|
||||||
*/
|
|
||||||
const nameLink = (name: (typeof names)[number]) => (
|
|
||||||
<ProfileHoverCard key={name.did} did={name.did} inline>
|
|
||||||
<InlineLinkText
|
|
||||||
to={name.href}
|
|
||||||
label={l`Go to ${name.displayName}'s profile`}
|
|
||||||
disableMismatchWarning
|
|
||||||
emoji
|
|
||||||
style={nameStyle}>
|
|
||||||
{name.displayName}
|
|
||||||
</InlineLinkText>
|
|
||||||
</ProfileHoverCard>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
/*
|
|
||||||
* The full-width wrapper forces the social proof onto its own line below
|
|
||||||
* the count stats within the wrapping stats row.
|
|
||||||
*/
|
|
||||||
<View style={[a.w_full, a.flex_row]}>
|
|
||||||
<Link
|
|
||||||
to={likesHref}
|
|
||||||
label={rowLabel}
|
|
||||||
style={[a.flex_row, a.align_center, a.gap_sm, a.flex_shrink]}
|
|
||||||
onPress={onPressLikedBy}>
|
|
||||||
<AvatarStack profiles={aviStackProfiles} size={AVI_SIZE} />
|
|
||||||
<Text testID="knownLikersStat" style={[a.flex_shrink, textStyle]}>
|
|
||||||
{names.length >= 2 ? (
|
|
||||||
<Trans comment="Social proof below the post stats; the bolded names are people the viewer follows who liked the post">
|
|
||||||
Liked by {nameLink(names[0])} and {nameLink(names[1])}
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans comment="Social proof below the post stats; the bolded name is a person the viewer follows who liked the post">
|
|
||||||
Liked by {nameLink(names[0])}
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {type OnPostSuccessData} from '#/state/shell/composer'
|
|||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {type PostSource} from '#/state/unstable-post-source'
|
import {type PostSource} from '#/state/unstable-post-source'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {KnownLikers, LikesStat} from '#/screens/PostThread/components/LikesStat'
|
import {LikesStat} from '#/screens/PostThread/components/LikesStat'
|
||||||
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
|
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
|
||||||
import {
|
import {
|
||||||
POST_NUMBER_INLINE_OFFSET,
|
POST_NUMBER_INLINE_OFFSET,
|
||||||
@@ -47,6 +47,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
|
|||||||
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
|
import {KnownLikers} from '#/components/Post/KnownLikers'
|
||||||
import {TranslatedPost} from '#/components/Post/Translated'
|
import {TranslatedPost} from '#/components/Post/Translated'
|
||||||
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
|
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
@@ -57,7 +58,7 @@ import {RichText} from '#/components/RichText'
|
|||||||
import * as Skele from '#/components/Skeleton'
|
import * as Skele from '#/components/Skeleton'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {WhoCanReply} from '#/components/WhoCanReply'
|
import {WhoCanReply} from '#/components/WhoCanReply'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {Features, useAnalytics} from '#/analytics'
|
||||||
import {useActorStatus} from '#/features/liveNow'
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {app} from '#/lexicons'
|
import {app} from '#/lexicons'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
@@ -451,7 +452,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
columnGap: a.gap_lg.gap,
|
columnGap: a.gap_lg.gap,
|
||||||
},
|
},
|
||||||
a.border_t,
|
a.border_t,
|
||||||
a.border_b,
|
|
||||||
a.mt_md,
|
a.mt_md,
|
||||||
a.py_md,
|
a.py_md,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
@@ -460,10 +460,10 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
<Link to={repostsHref} label={l`Reposts of this post`}>
|
<Link to={repostsHref} label={l`Reposts of this post`}>
|
||||||
<Text
|
<Text
|
||||||
testID="repostCount-expanded"
|
testID="repostCount-expanded"
|
||||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||||
<Trans comment="Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)">
|
<Trans comment="Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0)">
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||||
{formatPostStatCount(post.repostCount)}
|
{formatPostStatCount(post.repostCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
@@ -481,10 +481,10 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
<Link to={quotesHref} label={l`Quotes of this post`}>
|
<Link to={quotesHref} label={l`Quotes of this post`}>
|
||||||
<Text
|
<Text
|
||||||
testID="quoteCount-expanded"
|
testID="quoteCount-expanded"
|
||||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||||
<Trans comment="Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)">
|
<Trans comment="Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0)">
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||||
{formatPostStatCount(post.quoteCount)}
|
{formatPostStatCount(post.quoteCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
@@ -500,9 +500,9 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
{post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
|
{post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
|
||||||
<Text
|
<Text
|
||||||
testID="bookmarkCount-expanded"
|
testID="bookmarkCount-expanded"
|
||||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||||
<Trans comment="Save count display, the <0> tags enclose the number of saves in bold (will never be 0)">
|
<Trans comment="Save count display, the <0> tags enclose the number of saves in bold (will never be 0)">
|
||||||
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||||
{formatPostStatCount(post.bookmarkCount)}
|
{formatPostStatCount(post.bookmarkCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
@@ -513,9 +513,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<KnownLikers post={post} />
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
<KnownLikers
|
||||||
|
post={post}
|
||||||
|
feature={Features.PostThreadKnownLikersEnable}
|
||||||
|
/>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.pt_sm,
|
a.pt_sm,
|
||||||
@@ -562,7 +565,7 @@ function ExpandedPostDetails({
|
|||||||
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
||||||
<BackdatedPostIndicator post={post} />
|
<BackdatedPostIndicator post={post} />
|
||||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
|
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_xs, t.atoms.text_contrast_high]}>
|
||||||
{niceDate(i18n, post.indexedAt, 'dot separated')}
|
{niceDate(i18n, post.indexedAt, 'dot separated')}
|
||||||
</Text>
|
</Text>
|
||||||
{isRootPost && (
|
{isRootPost && (
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies
|
|||||||
import {useBreakpoints} from '#/alf'
|
import {useBreakpoints} from '#/alf'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import {app} from '#/lexicons'
|
import {app} from '#/lexicons'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export * from '#/state/queries/usePostThread/context'
|
export * from '#/state/queries/usePostThread/context'
|
||||||
export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
|
export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
|
||||||
@@ -73,6 +74,7 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
|||||||
enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
|
enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
|
||||||
queryKey: postThreadQueryKey,
|
queryKey: postThreadQueryKey,
|
||||||
async queryFn(ctx) {
|
async queryFn(ctx) {
|
||||||
|
const placeholder = getThreadPlaceholder(qc, anchor!)
|
||||||
const data = await client.call(app.bsky.unspecced.getPostThreadV2, {
|
const data = await client.call(app.bsky.unspecced.getPostThreadV2, {
|
||||||
anchor: anchor! as AtUriString,
|
anchor: anchor! as AtUriString,
|
||||||
branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF,
|
branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF,
|
||||||
@@ -80,6 +82,28 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
|||||||
sort: sort,
|
sort: sort,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const cachedKnownLikers =
|
||||||
|
placeholder &&
|
||||||
|
bsky.isType(app.bsky.unspecced.defs.threadItemPost, placeholder.value)
|
||||||
|
? placeholder.value.post.viewer?.knownLikers
|
||||||
|
: undefined
|
||||||
|
if (cachedKnownLikers?.actors.length) {
|
||||||
|
const anchorItem = data.thread?.find(item => item.uri === anchor)
|
||||||
|
if (
|
||||||
|
anchorItem &&
|
||||||
|
bsky.isType(
|
||||||
|
app.bsky.unspecced.defs.threadItemPost,
|
||||||
|
anchorItem.value,
|
||||||
|
) &&
|
||||||
|
!anchorItem.value.post.viewer?.knownLikers?.actors.length
|
||||||
|
) {
|
||||||
|
anchorItem.value.post.viewer = {
|
||||||
|
...anchorItem.value.post.viewer,
|
||||||
|
knownLikers: cachedKnownLikers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Initialize `ctx.meta` to track if we know we have additional replies
|
* Initialize `ctx.meta` to track if we know we have additional replies
|
||||||
* we could fetch once we hit the end.
|
* we could fetch once we hit the end.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import * as ReportDialogMetadataContext from '#/components/moderation/ReportDial
|
|||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Embed} from '#/components/Post/Embed'
|
import {Embed} from '#/components/Post/Embed'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
|
import {KnownLikers} from '#/components/Post/KnownLikers'
|
||||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||||
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
||||||
import {TranslatedPost} from '#/components/Post/Translated'
|
import {TranslatedPost} from '#/components/Post/Translated'
|
||||||
@@ -51,7 +52,7 @@ import {PostControls} from '#/components/PostControls'
|
|||||||
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {Features, useAnalytics} from '#/analytics'
|
||||||
import {useActorStatus} from '#/features/liveNow'
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {app} from '#/lexicons'
|
import {app} from '#/lexicons'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
@@ -456,6 +457,11 @@ let FeedItemInner = ({
|
|||||||
onShowLess={onShowLess}
|
onShowLess={onShowLess}
|
||||||
viaRepost={viaRepost}
|
viaRepost={viaRepost}
|
||||||
/>
|
/>
|
||||||
|
<KnownLikers
|
||||||
|
post={post}
|
||||||
|
feature={Features.PostFeedKnownLikersEnable}
|
||||||
|
variant="feed"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DiscoverDebug feedContext={feedContext} />
|
<DiscoverDebug feedContext={feedContext} />
|
||||||
|
|||||||
Reference in New Issue
Block a user