diff --git a/.cursor/rules/pr-descriptions.mdc b/.cursor/rules/pr-descriptions.mdc
new file mode 100644
index 0000000000..c1199b2a13
--- /dev/null
+++ b/.cursor/rules/pr-descriptions.mdc
@@ -0,0 +1,13 @@
+---
+description: Keep pull request descriptions limited to a concise summary of the changes
+alwaysApply: true
+---
+
+# Pull Request Descriptions
+
+When creating or editing a pull request description:
+
+- Include only a concise summary of the changes.
+- Do not mention Linear tickets or issue identifiers.
+- Do not include validation, testing, or verification steps.
+- Do not add boilerplate sections that duplicate information available elsewhere.
diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts
index 4d3cf5e197..c059eca8b6 100644
--- a/src/analytics/features/types.ts
+++ b/src/analytics/features/types.ts
@@ -16,6 +16,7 @@ export enum Features {
GroupChatsDisable = 'group_chats:disable',
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
+ PostFeedKnownLikersEnable = 'post_feed:known_likers:enable',
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
CustomLogoJapanEnable = 'custom_logo:japan:enable',
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
diff --git a/src/components/Post/KnownLikers.tsx b/src/components/Post/KnownLikers.tsx
new file mode 100644
index 0000000000..7a10e91d19
--- /dev/null
+++ b/src/components/Post/KnownLikers.tsx
@@ -0,0 +1,109 @@
+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'
+
+const AVI_SIZE = 20
+
+/**
+ * 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,
+}: {
+ post: app.bsky.feed.defs.PostView
+ feature: Features
+}) {
+ const t = useTheme()
+ const {t: l} = useLingui()
+ const moderationOpts = useModerationOpts()
+ const ax = useAnalytics()
+
+ const likeCount = post.likeCount ?? 0
+ if (likeCount === 0) return null
+
+ 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 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]
+
+ const nameLink = (name: (typeof names)[number]) => (
+
+
+ {name.displayName}
+
+
+ )
+
+ return (
+
+ ax.metric('post:likedBy:click', {})}>
+
+
+ {names.length >= 2 ? (
+
+ Liked by {nameLink(names[0])} and {nameLink(names[1])}
+
+ ) : (
+
+ Liked by {nameLink(names[0])}
+
+ )}
+
+
+
+ )
+}
diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx
index 15cbeba3ac..0b2fa30b53 100644
--- a/src/screens/PostThread/components/LikesStat.tsx
+++ b/src/screens/PostThread/components/LikesStat.tsx
@@ -1,22 +1,14 @@
-import {View} from 'react-native'
import {AtUri} from '@atproto/syntax'
-import {moderateProfile} from '@bsky/sdk/moderation'
import {Plural, 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 {Link} from '#/components/Link'
import {useFormatPostStatCount} from '#/components/PostControls/util'
-import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
-const AVI_SIZE = 20
-
/**
* The plain "N likes" stat for the expanded anchor post, linking to the likes
* list. Renders nothing when the post has no likes.
@@ -51,115 +43,3 @@ export function LikesStat({post}: {post: app.bsky.feed.defs.PostView}) {
)
}
-
-/**
- * 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]) => (
-
-
- {name.displayName}
-
-
- )
-
- return (
- /*
- * The full-width wrapper forces the social proof onto its own line below
- * the count stats within the wrapping stats row.
- */
-
-
-
-
- {names.length >= 2 ? (
-
- Liked by {nameLink(names[0])} and {nameLink(names[1])}
-
- ) : (
-
- Liked by {nameLink(names[0])}
-
- )}
-
-
-
- )
-}
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index 6a1ee4bb55..8a4f714157 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -23,7 +23,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 {KnownLikers, LikesStat} from '#/screens/PostThread/components/LikesStat'
+import {LikesStat} from '#/screens/PostThread/components/LikesStat'
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
import {
POST_NUMBER_INLINE_OFFSET,
@@ -47,6 +47,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import {type AppModerationCause} from '#/components/Pills'
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {KnownLikers} from '#/components/Post/KnownLikers'
import {TranslatedPost} from '#/components/Post/Translated'
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
import {useFormatPostStatCount} from '#/components/PostControls/util'
@@ -57,7 +58,7 @@ import {RichText} from '#/components/RichText'
import * as Skele from '#/components/Skeleton'
import {Text} from '#/components/Typography'
import {WhoCanReply} from '#/components/WhoCanReply'
-import {useAnalytics} from '#/analytics'
+import {Features, useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
@@ -513,7 +514,10 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
) : null}
-
+
) : null}
+