Skip to content

fix(zaps): collapse same-actor zap spam in notifications and post drawer#557

Open
dmnyc wants to merge 2 commits into
barrydeen:mainfrom
dmnyc:fix/collapse-zap-spam
Open

fix(zaps): collapse same-actor zap spam in notifications and post drawer#557
dmnyc wants to merge 2 commits into
barrydeen:mainfrom
dmnyc:fix/collapse-zap-spam

Conversation

@dmnyc
Copy link
Copy Markdown
Contributor

@dmnyc dmnyc commented May 22, 2026

Summary

Android counterpart to iOS PR barrydeen/wisp-ios#161.

A sender spamming many small zaps against the same note used to push everything else off-screen — both in the Notifications tab (one row per zap receipt) and in the in-post engagement drawer (one row per individual zapper). This change folds same-actor + same-note zaps into a single row in both surfaces.

Notifications tab

  • FlatNotificationItem gains mergedZaps: List<FlatNotificationItem> (defaulted empty so the ingest path is unchanged) and a totalZapSats computed property summing the primary + every merged duplicate.
  • NotificationsViewModel.collapseSameActorZapSpam runs after the existing filter pass. Walks the newest-first list, dedupes NotificationType.ZAP entries by (actorPubkey, referencedEventId), and folds duplicates into the primary's mergedZaps. DM_ZAP / PROFILE_ZAP don't participate — they have no referencedEventId to dedupe against.
  • NotificationTypeIcon bolt-icon label uses totalZapSats so the surfaced amount is the combined contribution from this actor on this note.
  • ZenNotificationRow shows a +N more pill next to the actor name when duplicates were folded.
  • New MergedZapsBreakdown composable renders below the embedded referenced note (matching the iOS NotificationRowView caption order): N zaps · total sats header, then one line per individual zap with amount + comment.

Post-engagement drawer

  • ReactionDetailsSection now groups [ZapDetail] by pubkey before rendering. Each group renders one ZapRow with the combined sat total and a (×N) count suffix on the label when N > 1. The first non-empty zap message becomes the row label (falls back to display name / shortened npub same as the per-zap default).
  • Distinct zappers still get their own rows; sort stays "highest combined sats first."
  • Long-press inspect on a collapsed row routes to the first individual receipt in the group, preserving the existing single-receipt affordance.

Test plan

  • On a note that's been zapped repeatedly by the same actor, the Notifications tab shows ONE row for that actor with the +N more pill; expanding the row shows the embedded note followed by the per-zap breakdown (N zaps · total sats + one line per zap with amount and comment).
  • The notification's bolt-icon label shows the combined sat total, not just the first zap's amount.
  • DM_ZAP and PROFILE_ZAP notifications (no referencedEventId) still render as individual rows.
  • In the post-engagement drawer, the same actor's repeat zaps collapse into one row with (×N) after the label and the combined sat total on the right.
  • Different zappers on the same note continue to render as separate rows, sorted by combined sats descending.
  • Long-press on a collapsed row in the drawer opens the inspect modal for the first individual receipt (existing behavior preserved).

dmnyc added 2 commits May 21, 2026 23:12
Mirrors iOS PR barrydeen/wisp-ios#161 on the Android side. A sender
spamming many small zaps against the same note used to push everything
else off the Notifications tab — one row per zap receipt. This change
folds same-actor + same-note zaps into a single row.

- `FlatNotificationItem` gains a `mergedZaps: List<FlatNotificationItem>`
  field (defaulted to empty so the repo-side ingest path is unchanged)
  and a `totalZapSats` computed property summing primary + merged.
- `NotificationsViewModel.collapseSameActorZapSpam` runs after the
  existing filter pass: walks newest-first, dedupes
  `NotificationType.ZAP` items by `(actorPubkey, referencedEventId)`,
  folds duplicates into the primary's `mergedZaps`. DM_ZAP and
  PROFILE_ZAP don't participate — they have no `referencedEventId` to
  dedupe against.
- `NotificationTypeIcon`'s bolt label now reads `totalZapSats` so the
  surfaced amount reflects the combined contribution.
- `ZenNotificationRow` shows a `+N more` pill next to the actor name
  when duplicates were folded.
- New `MergedZapsBreakdown` composable renders below the embedded
  referenced note (matching the iOS NotificationRowView caption flow):
  shows `N zaps · total sats` then one line per individual zap with
  amount + comment.
Mirrors iOS PR barrydeen/wisp-ios#161 on the Android side. The
in-post engagement drawer (`ReactionDetailsSection`) rendered one
`ZapRow` per individual `ZapDetail`, so an actor spamming N small
zaps against a single note took up N rows and pushed legitimate
zappers off the screen.

Groups `[ZapDetail]` by `pubkey` before rendering: each group shows
the combined sat total and a `(×N)` count suffix on the label when
N > 1. The first non-empty zap message becomes the row label (falls
back to display name / shortened npub when no message exists, same
as the per-zap default). Distinct zappers still get their own rows;
sort order stays "highest combined sats first." Long-press inspect
on a collapsed row routes to the first individual receipt in the
group.
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.

1 participant