Polish known likers in feeds and posts

This commit is contained in:
vineyardbovines
2026-08-28 14:18:28 -04:00
parent ca000e7865
commit 662441af85
7 changed files with 67 additions and 63 deletions
+7 -3
View File
@@ -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')}
+12 -35
View File
@@ -1,7 +1,7 @@
import {View} from 'react-native' import {View} from 'react-native'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {moderateProfile} from '@bsky/sdk/moderation' import {moderateProfile} from '@bsky/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {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 {sanitizeDisplayName} from '#/lib/strings/display-names'
@@ -33,9 +33,6 @@ export function KnownLikers({
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const ax = useAnalytics() const ax = useAnalytics()
const likeCount = post.likeCount ?? 0
if (likeCount === 0) return null
const knownLikersAndModeration = moderationOpts const knownLikersAndModeration = moderationOpts
? (post.viewer?.knownLikers?.actors ?? []) ? (post.viewer?.knownLikers?.actors ?? [])
.map(actor => ({ .map(actor => ({
@@ -64,19 +61,13 @@ export function KnownLikers({
moderation.ui('displayName'), moderation.ui('displayName'),
), ),
})) }))
const isFeed = variant === 'feed'
const rowLabel = const rowLabel =
variant === 'feed' names.length >= 2
? likeCount > 1
? l`${names[0].displayName} and ${likeCount - 1} others liked this`
: l`${names[0].displayName} liked this`
: names.length >= 2
? l`Liked by ${names[0].displayName} and ${names[1].displayName}` ? l`Liked by ${names[0].displayName} and ${names[1].displayName}`
: l`Liked by ${names[0].displayName}` : l`Liked by ${names[0].displayName}`
const textStyle = [ const textStyle = [a.text_sm, t.atoms.text_contrast_medium]
variant === 'feed' ? a.text_xs : a.text_sm, const nameStyle = [a.text_sm, a.font_medium, t.atoms.text_contrast_medium]
t.atoms.text_contrast_medium,
]
const nameStyle = [a.text_sm, a.font_semi_bold, t.atoms.text]
const nameLink = (name: (typeof names)[number]) => ( const nameLink = (name: (typeof names)[number]) => (
<ProfileHoverCard key={name.did} did={name.did} inline> <ProfileHoverCard key={name.did} did={name.did} inline>
@@ -92,35 +83,21 @@ export function KnownLikers({
) )
return ( return (
<View style={[a.w_full, a.flex_row]}> <View style={[a.w_full, a.flex_row, isFeed && a.mt_sm]}>
<Link <Link
to={likesHref} to={likesHref}
label={rowLabel} label={rowLabel}
style={[ style={[a.flex_row, a.align_center, a.gap_xs, a.flex_shrink]}
a.flex_row,
a.align_center,
variant === 'feed' ? a.gap_xs : a.gap_sm,
a.flex_shrink,
]}
onPress={() => ax.metric('post:likedBy:click', {})}> onPress={() => ax.metric('post:likedBy:click', {})}>
<AvatarStack <AvatarStack
profiles={aviStackProfiles} profiles={aviStackProfiles}
size={variant === 'feed' ? 16 : 20} size={16}
overlap={4}
borderWidth={0.5}
backgroundColor={t.atoms.bg_contrast_25.backgroundColor}
/> />
<Text testID="knownLikersStat" style={[a.flex_shrink, textStyle]}> <Text testID="knownLikersStat" style={[a.flex_shrink, textStyle]}>
{variant === 'feed' ? ( {names.length >= 2 ? (
likeCount > 1 ? (
<Trans comment="Social proof below a feed post; a person the viewer follows and the number of other people who liked the post">
{names[0].displayName} and{' '}
<Plural value={likeCount - 1} one="1 other" other="# others" />{' '}
liked this
</Trans>
) : (
<Trans comment="Social proof below a feed post; a person the viewer follows who liked the post">
{names[0].displayName} liked this
</Trans>
)
) : names.length >= 2 ? (
<Trans comment="Social proof below a post; the bolded names are people the viewer follows who liked the post"> <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])} Liked by {nameLink(names[0])} and {nameLink(names[1])}
</Trans> </Trans>
+4 -4
View File
@@ -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}
@@ -32,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" />
@@ -452,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,
@@ -461,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
@@ -482,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
@@ -501,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
@@ -514,12 +513,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</Trans> </Trans>
</Text> </Text>
) : null} ) : null}
</View>
) : null}
<KnownLikers <KnownLikers
post={post} post={post}
feature={Features.PostThreadKnownLikersEnable} feature={Features.PostThreadKnownLikersEnable}
/> />
</View>
) : null}
<View <View
style={[ style={[
a.pt_sm, a.pt_sm,
@@ -566,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 && (
+24
View File
@@ -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.
+5 -5
View File
@@ -445,11 +445,6 @@ let FeedItemInner = ({
additionalPostAlerts={additionalPostAlerts} additionalPostAlerts={additionalPostAlerts}
feedDescriptor={feedDescriptor} feedDescriptor={feedDescriptor}
/> />
<KnownLikers
post={rootPost}
feature={Features.PostFeedKnownLikersEnable}
variant="feed"
/>
<PostControls <PostControls
post={post} post={post}
record={record} record={record}
@@ -462,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} />