diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx
index 9a0c7139fb..eab32dc274 100644
--- a/src/screens/PostThread/components/LikesStat.tsx
+++ b/src/screens/PostThread/components/LikesStat.tsx
@@ -1,15 +1,13 @@
import {View} from 'react-native'
import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api'
-import {plural} from '@lingui/core/macro'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {enforceLen} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useLikedBySampleQuery} from '#/state/queries/post-liked-by'
import {useSession} from '#/state/session'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
import {AvatarStack} from '#/components/AvatarStack'
import {InlineLinkText, Link} from '#/components/Link'
import {useFormatPostStatCount} from '#/components/PostControls/util'
@@ -18,27 +16,56 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
const AVI_SIZE = 20
-const MAX_NAME_LENGTH = 16
/**
- * The likes stat for the expanded anchor post. When the viewer follows some
- * of the post's recent likers, renders social proof - a face pile plus
- * "Liked by A, B, and N others" - in place of the plain "N likes" text,
- * which it falls back to otherwise.
- *
- * Known likers are sourced client-side from a single `getLikes` request (100
- * likes, the API max per page), so they are a sample of the most recent
- * likers, not an exhaustive list. Only the faces and names are affected by
- * sampling - the "N others" count is derived from the post's total like
- * count.
+ * The plain "N likes" stat for the expanded anchor post, linking to the likes
+ * list. Renders nothing when the post has no likes.
*/
export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
const t = useTheme()
- const {gtMobile} = useBreakpoints()
+ const {t: l} = useLingui()
+ const formatPostStatCount = useFormatPostStatCount()
+ 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')
+
+ return (
+ ax.metric('post:likedBy:click', {})}>
+
+
+
+ {formatPostStatCount(likeCount)}
+ {' '}
+
+
+
+
+ )
+}
+
+/**
+ * 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 sourced client-side from a single `getLikes` request (100
+ * likes, the API max per page), so they are a sample of the most recent
+ * likers, not an exhaustive list.
+ */
+export function KnownLikers({post}: {post: AppBskyFeedDefs.PostView}) {
+ const t = useTheme()
const {t: l} = useLingui()
const {hasSession, currentAccount} = useSession()
const moderationOpts = useModerationOpts()
- const formatPostStatCount = useFormatPostStatCount()
const ax = useAnalytics()
const likeCount = post.likeCount ?? 0
@@ -78,67 +105,34 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
knownLikersAndModeration.length > 0 &&
ax.features.enabled(ax.features.PostThreadKnownLikersEnable)
- if (!showKnownLikers) {
- return (
-
-
-
-
- {formatPostStatCount(likeCount)}
- {' '}
-
-
-
-
- )
- }
+ if (!showKnownLikers) return null
const aviStackProfiles = knownLikersAndModeration
.slice(0, 3)
.map(({actor}) => actor)
- const maxNames = gtMobile ? 2 : 1
const names = knownLikersAndModeration
- .slice(0, maxNames)
+ .slice(0, 2)
.map(({actor, moderation}) => {
return {
did: actor.did,
href: makeProfileLink(actor),
- displayName: enforceLen(
- sanitizeDisplayName(
- actor.displayName || actor.handle,
- moderation.ui('displayName'),
- ),
- MAX_NAME_LENGTH,
- true,
+ displayName: sanitizeDisplayName(
+ actor.displayName || actor.handle,
+ moderation.ui('displayName'),
),
}
})
- const others = likeCount - names.length
-
/*
* The row link's a11y label mirrors the visible sentence so screen readers
* announce the social proof.
*/
- const othersLabel = plural(others, {
- one: `${formatPostStatCount(others)} other`,
- other: `${formatPostStatCount(others)} others`,
- })
const rowLabel =
names.length >= 2
- ? others > 0
- ? l`${names[0].displayName}, ${names[1].displayName}, and ${othersLabel} like this`
- : l`${names[0].displayName} and ${names[1].displayName} like this`
- : others > 0
- ? l`${names[0].displayName} and ${othersLabel} like this`
- : l`${names[0].displayName} likes this`
+ ? l`Liked by ${names[0].displayName} and ${names[1].displayName}`
+ : l`Liked by ${names[0].displayName}`
- const textStyle = [a.text_md, t.atoms.text_contrast_medium]
- const nameStyle = [a.text_md, a.font_semi_bold, t.atoms.text]
+ 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
@@ -160,10 +154,8 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) {
return (
/*
- * The full-width wrapper keeps the social proof on its own line within
- * the wrapping stats row, rather than wrapping mid-row and orphaning
- * whichever count stat comes last. The link itself hugs its content so
- * the empty space to the right of the text is not pressable.
+ * The full-width wrapper forces the social proof onto its own line below
+ * the count stats within the wrapping stats row.
*/
-
+
{names.length >= 2 ? (
- others > 0 ? (
-
- {nameLink(names[0])}, {nameLink(names[1])}, and{' '}
- {' '}
- like this
-
- ) : (
-
- {nameLink(names[0])} and {nameLink(names[1])} like this
-
- )
- ) : others > 0 ? (
-
- {nameLink(names[0])} and{' '}
- {' '}
- like this
+
+ Liked by {nameLink(names[0])} and {nameLink(names[1])}
) : (
-
- {nameLink(names[0])} likes this
+
+ Liked by {nameLink(names[0])}
)}
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index f88ad000c1..7973bc1a8a 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -28,7 +28,7 @@ import {type OnPostSuccessData} from '#/state/shell/composer'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {type PostSource} from '#/state/unstable-post-source'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {LikesStat} from '#/screens/PostThread/components/LikesStat'
+import {KnownLikers, LikesStat} from '#/screens/PostThread/components/LikesStat'
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
import {
LINEAR_AVI_WIDTH,
@@ -440,7 +440,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
a.py_md,
t.atoms.border_contrast_low,
]}>
-
{post.repostCount != null && post.repostCount !== 0 ? (
) : null}
+
{post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
) : null}
+
) : null}