Skip to content

Prototype: Show known likers on post thread page#11080

Merged
estrattonbailey merged 7 commits into
mainfrom
post-page-social-proof
Jul 13, 2026
Merged

Prototype: Show known likers on post thread page#11080
estrattonbailey merged 7 commits into
mainfrom
post-page-social-proof

Conversation

@abenzer

@abenzer abenzer commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Note: this is a prototype. It's gated behind post_thread:known_likers:enable while we evaluate the UX. Not intended for a general rollout in its current form.

What this does

On the post thread page, the post's plain "N likes" stat is replaced with a row showing which people you follow liked the post: a few stacked avatars plus a sentence like "Jack, Amy, and 34 others like this." Names link to profiles, but the rest of the row links to the liked-by list. Viewers who don't follow any of the likers (or aren't signed in) see the existing "N likes" text unchanged.

Demo

Screenshot 2026-07-06 at 6 16 00 PM

How it works

  • Fetches a single page of app.bsky.feed.getLikes (limit: 100, the API max) via a new useLikedBySampleQuery, kept separate from the liked-by screen's paginated query.
  • Filters to likers the viewer follows, excluding self, muted, blocked, and blocking accounts. Display names and avatars go through the standard moderation pipeline.
  • Gated behind post_thread:known_likers:enable. The gate is evaluated only for signed-in viewers of posts with likes, and when it's off, no getLikes request fires.

Known limitations

  • Known likers are sampled from the 100 most recent likes, so a followed liker who liked a popular post early may not surface. Going deeper than 100 would need server-side support.
  • The row swaps in when the sample loads, which shifts layout slightly.
  • Viewing a post and then opening its likes list fetches likers twice (separate caches).

@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude finished @abenzer's task in 2m 29s —— View job


Review

  • Read repo conventions and review guidance
  • Review the diff (LikesStat, ThreadItemAnchor, query, analytics)
  • Post review

I read the four changed files against CLAUDE.md and the review guidance. This is a well-constructed prototype — hooks are unconditional (early returns come after them), the gate is short-circuited so getLikes never fires for ineligible viewers, moderation runs on both names and avatars, and self/muted/blocked/blocking likers are filtered out. Nice touch extending findAllProfilesInQueryData to walk the sample cache too, so profile-shadow follow/unfollow updates keep actor.viewer.following fresh in the social-proof row.

I found no blocking issues. A few non-blocking observations:

1. Avatars (3) vs. names (2) can make a recognized face read as an anonymous "other"LikesStat.tsx:101,153

others = likeCount - names.length uses the named count (max 2), but the face pile shows up to 3 (knownLikers.slice(0, 3)). Concrete scenario: you follow Jack, Amy, and Bob; all three liked a post with 3 total likes. The row shows all three avatars but reads "Jack, Amy, and 1 other like this" — Bob's recognizable avatar is displayed while the text lumps him into "1 other." Worth deciding whether the count should be relative to faces shown (likeCount - knownLikers shown) or whether the pile should cap at names.length. Non-blocking, but visible whenever ≥3 followed likers surface in the sample.

Fix this →

2. GrowthBook exposure logged on every eligible renderLikesStat.tsx:46-49

The comment explains the ordering intent (evaluate only for viewers who could see the treatment), which is right. Just flagging that ax.features.enabled(...) runs on each re-render of the anchor for eligible posts, so exposures fire repeatedly rather than once per view. If GrowthBook already dedupes exposures this is a non-issue; worth confirming so the experiment's exposure denominator isn't inflated. Non-blocking.

3. PAGE_SIZE = 30 for the paginated liked-by query is unchangedpost-liked-by.ts:14

Just confirming the new SAMPLE_SIZE = 100 path is fully separate from the existing liked-by screen pagination (it is — separate key root and hook), so the screen's paging behavior is untouched. No action needed.

I checked the three-platform surface (nothing web/native-specific here), Lingui coverage (all strings wrapped; both the visible <Trans> and the a11y rowLabel handle the 1-name / 2-name × others-present/absent cases), the negative-others edge (stale likeCount < sampled followers renders the "A and B like this" path, no crash), and moderation (avatars and display names both go through moderateProfile; muted/blocked filtered). The nested-link "deepest link claims the press" pattern matches the existing NotificationFeedItem approach.
post-page-social-proof

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
Old size New size Diff
7.75 MB 7.76 MB 4.91 KB (0.06%)

@estrattonbailey

Copy link
Copy Markdown
Member

Let's discuss in Slack. Just concerned about the feasibility of actually shipping this to production for all posts. Huge +1 on the feature though 👍

Fetch the likers sample for everyone signed in, then check the feature gate only if the viewer follows one of the likers. This keeps people who'd see no difference out of the test.

Note: this means app.bsky.feed.getLikes will be called on every thread page view for signed-in users for posts with at least 1 like
@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

APP-2563

# Conflicts:
#	src/analytics/features/types.ts
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @abenzer's task in 2m 32s —— View job


Review

  • Read repo conventions and review guidance (.github/claude-review-prompt.md)
  • Review the diff (LikesStat, ThreadItemAnchor, post-liked-by, analytics types)
  • Post review

I re-reviewed against origin/main, focusing on what changed since the last pass — chiefly d072291, which reorders the gate so getLikes fires for every signed-in viewer and the GrowthBook exposure is evaluated only once a followed liker actually surfaces (LikesStat.tsx:64-66). That's a clean fix to the earlier exposure-inflation concern: exposures now fire only when there's a treatment to show. Hooks remain unconditional (early returns follow them), moderation runs on both names and avatars, and self/muted/blocked/blocking likers are filtered.

One finding survives, and it's the same one from the first pass — still present verbatim after the merge:

1. Face pile shows up to 3 avatars, but the sentence only names 2 and counts the rest as "others" — a recognizable face gets described as anonymousLikesStat.tsx:96,148

names is capped at 2 (knownLikers.slice(0, 2)) and others = likeCount - names.length, but AvatarStack renders knownLikers.slice(0, 3). When ≥3 followed likers surface, the 3rd person's avatar is shown while the text folds them into "N others."

Concrete case: you follow Jack, Amy, and Bob; all three liked a post whose total likeCount is 3. The row shows three avatars but reads "Jack, Amy, and 1 other like this" — Bob's face is right there, described as "1 other." With a stale-low likeCount it gets worse: likeCount 2 + three surfaced followers → others = 0"Jack and Amy like this" under three avatars.

Non-blocking (prototype, gated), but visible whenever ≥3 followed likers appear in the sample. Reconcile the two: either cap the pile at names.length, or base others on the number of faces actually shown (likeCount - knownLikers shown).

Fix this →

Non-blocking notes (no change requested):

  • getLikes now fires on every thread view for signed-in users on any post with ≥1 like (LikesStat.tsx:42-44). This is the intended tradeoff of d072291 (fetch-first, gate-later), and the author + @estrattonbailey have already flagged the production-feasibility question. Flagging only to confirm it's a deliberate cost, not an oversight — the 5-min staleTime and retry: 1 keep it bounded per view.
  • Double-fetch of likers when a viewer opens the likes list after viewing the thread (separate liked-by vs liked-by-sample caches). Already documented as a known limitation; the separate key root is the right call to avoid perturbing the liked-by screen's pagination.
  • findAllProfilesInQueryData correctly walks both the paginated and sample caches, so profile-shadow follow/unfollow updates keep actor.viewer.following fresh in the row.

I also checked Lingui coverage (all four sentence variants and the a11y rowLabel are wrapped), the names[0] access (guarded by knownLikers.length > 0), the three-platform surface (nothing platform-specific here), and the removed dead likesHref/useMemo in ThreadItemAnchor (its remaining Plural/formatPostStatCount uses are still live). No blocking issues.
post-page-social-proof

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @abenzer's task in 2m 36s —— View job


Review

  • Read repo conventions and review guidance (.github/claude-review-prompt.md)
  • Review the diff (LikesStat, ThreadItemAnchor, post-liked-by, analytics types)
  • Post review

I re-reviewed against origin/main, focusing on what's new since the last pass — the only substantive change is e37e25c, which adds a second gate post_thread:known_likers:fetch:enable in front of the getLikes sample request (LikesStat.tsx:46-51). That's a clean, sensible killswitch: with it off, useLikedBySampleQuery gets uri: undefined and never fires, independent of the display gate. Hooks stay unconditional, moderation still runs on both names and avatars, and self/muted/blocked/blocking likers are filtered.

One finding survives from the prior passes — still present verbatim:

1. Face pile shows up to 3 avatars, but the sentence names 2 and counts the rest as "others" — a recognizable face gets described as anonymousLikesStat.tsx:92,103,155 (non-blocking)

names is capped at 2 (knownLikers.slice(0, 2)) and others = likeCount - names.length, but AvatarStack renders knownLikers.slice(0, 3). When ≥3 followed likers surface, the 3rd person's avatar is shown while the text folds them into "N others."

Concrete case: you follow Jack, Amy, and Bob; all three liked a post whose total likeCount is 3. The row shows three avatars but reads "Jack, Amy, and 1 other like this" — Bob's face is right there, described as "1 other." With a stale-low likeCount (2) plus three surfaced followers, others = 0"Jack and Amy like this" under three avatars.

Reconcile the two: cap the pile at names.length, or base others on the number of faces actually shown.

Fix this →

New note on e37e25c (non-blocking):

  • The fetch killswitch is evaluated on every eligible renderLikesStat.tsx:46. ax.features.enabled(...) is wired to feats.isOn (analytics/index.tsx:267), which fires the feature:viewed usage callback. So PostThreadKnownLikersFetchEnable now logs an exposure on every render of the anchor for any signed-in viewer of a post with ≥1 like — i.e. much more broadly than the display gate, which d072291 deliberately deferred until a known liker actually surfaces. For a killswitch this is probably fine (you likely don't care about its exposure denominator), but it's worth confirming you don't want the fetch flag evaluated the same lazy way, since it now dominates the volume of feature:viewed events for this feature.

Still-standing non-blocking notes (no change requested):

  • getLikes fires on every thread view for signed-in users on any post with ≥1 like (now additionally gated by the fetch killswitch). Deliberate fetch-first/gate-later tradeoff; the production-feasibility question is already being discussed in Slack per @estrattonbailey. The 5-min staleTime and retry: 1 keep it bounded per view.
  • Double-fetch of likers when opening the likes list after viewing the thread (separate liked-by vs liked-by-sample caches). Documented limitation; the separate key root correctly avoids perturbing the liked-by screen's pagination.
  • findAllProfilesInQueryData walks both the paginated and sample caches, so profile-shadow follow/unfollow keeps actor.viewer.following fresh in the row.

I also re-checked Lingui coverage (all four sentence variants plus the a11y rowLabel are wrapped; Plural used correctly), the names[0] access (guarded by knownLikers.length > 0), moderation on avatars + display names, the three-platform surface (nothing platform-specific here), and the removed likesHref/useMemo in ThreadItemAnchor (its remaining Plural/formatPostStatCount uses are still live). No blocking issues.
post-page-social-proof

@arcalinea arcalinea temporarily deployed to post-page-social-proof - social-app PR #11080 July 13, 2026 19:44 — with Render Destroyed
@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown

Claude finished @estrattonbailey's task in 1m 57s —— View job


Review

  • Read repo conventions and review guidance (.github/claude-review-prompt.md)
  • Review the diff (LikesStat, ThreadItemAnchor, post-liked-by, analytics types)
  • Post review

I re-reviewed focusing on what's new since the last pass — the substantive change is 65589cb, which replaces the hand-rolled muted/blocking/blockedBy filter with moderateProfile(actor, moderationOpts).ui('profileList').filter (LikesStat.tsx:59-72).

That's a good change, and it's correct. It matches the canonical moderation-filter pattern used across the app (ProfileCard.tsx:622, state/queries/actor-autocomplete.ts:118, components/Autocomplete/useAutocomplete/index.ts:134), and it's strictly more thorough than the code it replaces: ui('profileList').filter still filters muted/blocking/blocked-by relationships, and additionally applies label- and list-based moderation that the old explicit checks missed. It also adds the post:likedBy:click metric on both the fallback and social-proof links. Hooks stay unconditional (early returns follow them), the two gates (...FetchEnable kill switch → sample request, ...KnownLikersEnable → display) are intact, and findAllProfilesInQueryData still walks both caches.

One finding survives from the prior passes — still present verbatim:

1. Face pile shows up to 3 avatars, but the sentence names 2 and counts the rest as "others" — a recognizable face gets described as anonymousLikesStat.tsx:98-99, 101-102, 113 (non-blocking)

aviStackProfiles is knownLikersAndModeration.slice(0, 3) (up to 3 faces), but names is slice(0, 2) and others = likeCount - names.length. When ≥3 followed likers surface, the 3rd person's avatar renders while the text folds them into "N others."

Concrete case: you follow Jack, Amy, and Bob; all three liked a post whose total likeCount is 3. The row shows three avatars but reads "Jack, Amy, and 1 other like this" — Bob's face is right there, described as "1 other." With a stale-low likeCount (2) plus three surfaced followers, others = 0"Jack and Amy like this" under three avatars.

Reconcile the two: cap the pile at names.length, or base others on the number of faces actually shown.

Fix this →

Still-standing non-blocking notes (no change requested):

  • getLikes fires on every thread view for signed-in viewers of any post with ≥1 like (gated by the ...FetchEnable kill switch). Deliberate fetch-first/gate-later tradeoff; the production-feasibility question is already in Slack per @estrattonbailey. The 5-min staleTime + retry: 1 keep it bounded per view.
  • Double-fetch of likers when opening the likes list after viewing the thread (separate liked-by vs liked-by-sample caches). Documented limitation; the separate key root correctly avoids perturbing the liked-by screen's pagination.
  • ...FetchEnable exposure fires on every eligible render (LikesStat.tsx:46), more broadly than the display gate. Fine for a kill switch, but worth confirming you don't want its feature:viewed volume distinct from the display experiment's.

I also re-checked Lingui coverage (all four sentence variants + the a11y rowLabel wrapped; Plural used correctly), the names[0] access (guarded by knownLikersAndModeration.length > 0), moderation now running via moderateProfile on both display names and the filter, the three-platform surface (nothing platform-specific), and the onPressLikedBy metric wiring. No blocking issues.
post-page-social-proof

@estrattonbailey estrattonbailey merged commit 04ebd02 into main Jul 13, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants