Add known likers to feed posts (#11595)
This commit is contained in:
@@ -13,13 +13,17 @@ export function AvatarStack({
|
||||
size = 26,
|
||||
numPending,
|
||||
backgroundColor,
|
||||
borderWidth = 1,
|
||||
overlap,
|
||||
}: {
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
size?: number
|
||||
numPending?: number
|
||||
backgroundColor?: string
|
||||
borderWidth?: number
|
||||
overlap?: number
|
||||
}) {
|
||||
const translation = size / 3 // overlap by 1/3
|
||||
const translation = overlap ?? size / 3
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
@@ -55,7 +59,7 @@ export function AvatarStack({
|
||||
width: size,
|
||||
height: size,
|
||||
left: i * -translation,
|
||||
borderWidth: 1,
|
||||
borderWidth,
|
||||
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
||||
borderRadius: 999,
|
||||
zIndex: 3 - i,
|
||||
@@ -63,7 +67,7 @@ export function AvatarStack({
|
||||
]}>
|
||||
{item.profile && (
|
||||
<UserAvatar
|
||||
size={size - 2}
|
||||
size={size - borderWidth * 2}
|
||||
avatar={item.profile.avatar}
|
||||
type={item.profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
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={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{gap: 3},
|
||||
(hovered || focused || pressed) && native({opacity: 0.5}),
|
||||
style,
|
||||
]}>
|
||||
@@ -122,16 +122,16 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
||||
color={
|
||||
isThreadAuthor ? t.palette.primary_500 : t.palette.contrast_400
|
||||
}
|
||||
width={16}
|
||||
width={12}
|
||||
settings={settings}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.text_xs,
|
||||
a.leading_tight,
|
||||
isThreadAuthor
|
||||
? {color: t.palette.primary_500}
|
||||
: t.atoms.text_contrast_medium,
|
||||
: t.atoms.text_contrast_high,
|
||||
(hovered || focused || pressed) && web(a.underline),
|
||||
]}>
|
||||
{description}
|
||||
|
||||
Reference in New Issue
Block a user