audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: don't render dead-end "+1" / "+2" tiles in avatar stacks (#1308)

Clicking "+1" to reveal a single extra avatar expanded a strip that
scrolled nowhere — pointless. Skip the "+N" tile when the overflow
would be smaller than minOverflow (default 3) AND we have enough
users loaded to show everyone inline.

- backend `get_top_likers` default limit: 3 -> 5. tracks with 4 or 5
total likes now ship everyone in the preview, so the frontend can
render them all inline without a dead-end tile. cost per EXPLAIN
ANALYZE is still sub-millisecond on production.
- frontend `AvatarStack` gains `minOverflow` prop (default 3). only
skips "+N" when the overflow is small AND users.length >= total
(i.e. we have everyone loaded), so partial-data surfaces fall back
safely to the regular "+N" affordance.

Behavior:
- total <= 5: shows everyone inline, no "+N"
- total >= 6: shows 3 + "+N>=3" (expansion reveals meaningfully more)

Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4 (1M context)
and committed by
GitHub
47bed2de 99e145fd

+35 -7
+6 -2
backend/src/backend/utilities/aggregations.py
··· 77 77 async def get_top_likers( 78 78 db: AsyncSession, 79 79 track_ids: list[int], 80 - limit: int = 3, 80 + limit: int = 5, 81 81 ) -> dict[int, list[LikerPreview]]: 82 82 """get the N most recent likers per track in a single batched query. 83 83 ··· 90 90 args: 91 91 db: database session 92 92 track_ids: list of track IDs to get likers for 93 - limit: max likers per track (default 3) 93 + limit: max likers per track. default 5 — the frontend displays 3 94 + inline but only shows a "+N" expand tile when the overflow is 95 + meaningful (3+). returning 5 means tracks with 4 or 5 total 96 + likes can render everyone inline without the dead-end "+1"/"+2" 97 + affordance. 94 98 95 99 returns: 96 100 dict mapping track_id -> list of LikerPreview, most recent first.
+10 -4
backend/tests/utilities/test_aggregations.py
··· 296 296 assert result == {} 297 297 298 298 299 - async def test_get_top_likers_limits_to_three_by_default( 299 + async def test_get_top_likers_default_limit( 300 300 db_session: AsyncSession, test_tracks_with_liker_artists: list[Track] 301 301 ): 302 - """default limit=3 returns at most 3 likers per track, most recent first.""" 302 + """default limit=5 returns at most 5 likers per track, most recent first. 303 + 304 + the frontend displays 3 inline but the backend returns 5 so tracks with 305 + 4 or 5 total likes can render everyone without a dead-end "+1"/"+2" tile. 306 + """ 303 307 tracks = test_tracks_with_liker_artists 304 308 result = await get_top_likers(db_session, [t.id for t in tracks]) 305 309 306 - # track 0 has 5 likes → returns 3 most recent (liker5, liker4, liker3) 307 - assert len(result[tracks[0].id]) == 3 310 + # track 0 has exactly 5 likes → returns all 5 most recent 311 + assert len(result[tracks[0].id]) == 5 308 312 assert [liker.handle for liker in result[tracks[0].id]] == [ 309 313 "liker5.bsky.social", 310 314 "liker4.bsky.social", 311 315 "liker3.bsky.social", 316 + "liker2.bsky.social", 317 + "liker1.bsky.social", 312 318 ] 313 319 314 320 # track 1 has 2 likes → returns both (liker2, liker1)
+19 -1
frontend/src/lib/components/AvatarStack.svelte
··· 8 8 total: number; 9 9 /** max avatars to render before overflowing to "+N". default 3. */ 10 10 maxVisible?: number; 11 + /** minimum overflow size that justifies a "+N" tile. if the overflow 12 + * would be smaller than this, just render everyone inline — clicking 13 + * "+1" to reveal a single extra avatar is ridiculous and expanding 14 + * scrolls nowhere. default 3. */ 15 + minOverflow?: number; 11 16 /** avatar diameter in pixels. default 24. */ 12 17 size?: number; 13 18 /** CSS color for the hairline ring separating stacked avatars. ··· 45 50 users, 46 51 total, 47 52 maxVisible = 3, 53 + minOverflow = 3, 48 54 size = 24, 49 55 borderColor = 'var(--bg-secondary)', 50 56 moreHref, ··· 64 70 return u.display_name || u.handle; 65 71 } 66 72 67 - let visible = $derived(users.slice(0, maxVisible)); 73 + // if the overflow would be tiny, show everyone — "+1" (or "+2") next to 74 + // three avatars is a dead-end affordance: clicking it scrolls nowhere. 75 + // only skip the +N tile when we actually have enough users loaded to 76 + // fill in the overflow; otherwise we'd render 3 and pretend that's all. 77 + let effectiveMaxVisible = $derived.by(() => { 78 + const wouldOverflow = Math.max(0, total - maxVisible); 79 + const haveEveryone = users.length >= total; 80 + if (wouldOverflow > 0 && wouldOverflow < minOverflow && haveEveryone) { 81 + return total; 82 + } 83 + return maxVisible; 84 + }); 85 + let visible = $derived(users.slice(0, effectiveMaxVisible)); 68 86 let overflow = $derived(Math.max(0, total - visible.length)); 69 87 // overlap grows with size so the visual density stays consistent. 70 88 let overlap = $derived(Math.round(size / 4));