Skip to content

feat: scope notifications to deadcat-authored content + topshell polish - #106

Merged
dmnyc merged 7 commits into
masterfrom
feat/notifications-deadcat-scope
Apr 25, 2026
Merged

feat: scope notifications to deadcat-authored content + topshell polish#106
dmnyc merged 7 commits into
masterfrom
feat/notifications-deadcat-scope

Conversation

@dmnyc

@dmnyc dmnyc commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The notifications bell was filling up with the user's entire Nostr notifications stream — every reaction, zap, and reply across the network, most of it about non-Deadcat content the user posted from other clients. This PR scopes the bell to deadcat-authored events only via universal NIP-89 client tagging, plus a few topshell polish fixes that surfaced along the way.

Notifications scoping (the headline change)

  • Universal NIP-89 client tag on every event the app publishes. Previously only kind:1111 comments carried the tag; now reactions, deletions, follow lists, mute lists, profile updates, wallet backups, market announcements, attestations, pool announcements, limit orders, and order deletions all do too. Centralized via a shared client_tag() helper in the SDK so future builders can't forget it.
  • Notification subscription filters target events. The subscription bundles a second filter ({authors: [me]}) alongside the original notifications filter, post-filters incoming events for the deadcat client tag, and tracks the resulting event IDs in a persisted OwnEventsStore (sibling of notifications.json under <app_data_dir>/<network>/). Inbound notifications (kind:7 / 9735 / 1111) are dropped unless their target e tag is in that set.
  • Strict filter, no empty-set bypass. Brand-new users with no Deadcat-authored content see an empty bell — correct behaviour; the bell only ever shows reactions/zaps/replies on things they actually posted from this app.
  • Profile-level zaps drop too — pubkey-scoped zap receipts can come from any client, so they're indistinguishable from non-Deadcat zaps and stay out of the bell.
  • Legacy prune. A scheduled task (10s after subscription start) sweeps the persisted notifications file against the fully replayed own-events set, dropping anything that doesn't target a Deadcat-authored event. Cleans up records that leaked through during the bypass-era without requiring a manual "Mark all read."

Topshell polish

  • Two-edge category-bar fade — left + right gradients with hidden scrollbar, conditionally rendered via data-overflow-* attributes computed from scrollLeft / clientWidth / scrollWidth. ResizeObserver + scroll listener keep them in sync. Width 24px, fully opaque at edge — narrow enough to not bleed onto pills, dark enough to read as "more content this way."
  • Active pill auto-scrolls into view when activeCategory changes, so a sign-in (which adds "My Markets") or a deep link to a clipped category doesn't leave the new active state hidden.
  • Mutually exclusive popovers — opening the notifications bell now closes the user menu and vice versa. No more cross-popover overlap.
  • Notifications empty state — capped width + extra padding so the line wraps cleanly inside the popover instead of jamming the edges.

Commits (oldest → newest)

  • 4bf8da8 chore(ui): add right-edge fade + hidden scrollbar to category row
  • df73773 feat(notifications): scope to deadcat-authored content via NIP-89 client tag
  • b472301 fix(notifications): strict deadcat-only filter + persist + prune legacy
  • 0fa8567 fix(ui): two-edge category bar fade + tighter notifications empty state
  • 0e1975c tune(ui): soften category bar fades so active pills stay readable
  • 9e48ac7 tune(ui): narrow category bar fades to 24px so they don't bleed onto pills
  • b9350b2 fix(ui): close user menu when bell opens (and vice versa)

Out of scope

  • "Show all Nostr notifications" toggle for power users who want one inbox — easy follow-up if requested.
  • Mobile responsive overhaul (hamburger / bottom-nav). Tracked in memory; the desktop-narrow scroll fade is the interim affordance.

Test plan

  • Bring an existing Nostr identity into the app — bell stays empty until a comment is posted from Deadcat. Existing notifications get pruned within ~10s of session start.
  • Post a comment from Deadcat → reaction / reply / zap on it shows in the bell. Reactions on non-Deadcat content do not.
  • Profile-level zaps from outside Deadcat don't appear in the bell.
  • Category bar shows fades only when there's clipped content past the edge; no fades at full width.
  • Clicking a category that's currently clipped (e.g. on a narrow window) auto-scrolls it into view.
  • Opening the bell with the user menu open closes the user menu, and vice versa.
  • biome check, tsc --noEmit, cargo fmt, cargo clippy --all-targets -- -D warnings all pass.

dmnyc added 7 commits April 25, 2026 11:29
The category row already had \`overflow-x-auto whitespace-nowrap\`
so it scrolled horizontally on narrow viewports — but with no
visual cue that it was scrollable, "Ending Soon" looked like it
was simply being clipped. Two small additions:

- Hide the default scrollbar (\`[scrollbar-width:none]\` + the
  webkit-scrollbar pseudo-class). The OS scrollbar in this row is
  noise.
- Right-edge fade gradient via a \`::after\` pseudo-element on a
  new positioning wrapper. Communicates "more content this way"
  the moment the row overflows, without a hamburger commitment.

Pure progressive enhancement — at full width the fade sits over
empty space and is invisible. Hamburger / bottom-nav / mobile pass
is tracked in memory as its own future deep-dive once we actually
build for phones.
…ent tag

Users who bring an existing Nostr identity to Deadcat were seeing
their entire Nostr notifications stream in the bell — every reaction,
zap, and reply across the whole network, most of it about content
they posted from other clients. Two changes solve this:

1. **Universal NIP-89 client tag** on every event the app publishes.
   Previously only kind:1111 comments carried the tag; now reactions,
   deletions, follow lists, mute lists, profile updates, wallet
   backups, market announcements, attestations, pool announcements,
   limit orders, and order deletions all do too. Centralized via a
   shared `client_tag()` helper in the SDK so future builders can't
   forget it.

2. **Notifications subscription filters target events.** The
   subscription bundles a second filter (`{authors: [me]}`) alongside
   the original notifications filter, post-filters incoming events
   for the deadcat client tag, and tracks the resulting event IDs
   in an in-memory set. Inbound notifications (kind:7 / 9735 / 1111)
   are dropped unless their target `e` tag is in that set — i.e.,
   unless they target a deadcat-authored event. Profile-level zaps
   (kind:9735 with no `e` tag) still pass through since they target
   the user directly.

`parse_notification_event` takes a new `&HashSet<String>` parameter;
empty set opts out of filtering (used implicitly while the tracker
is hydrating on the first burst of historical events). Existing test
updated to seed the set with the parent comment id.

No new persistence — the tracker rebuilds on every startup from the
relay's historical replay. Persistence is a clean follow-up if the
cold-start race window becomes an issue in practice; today's mitigation
is that historical own-events typically arrive before historical
notifications targeting them.
The first cut of the deadcat-scoped notifications filter shipped with
an empty-set bypass: if the in-memory own-events tracker was empty
(cold-start window before historical replay completed, or a brand-new
user with an existing Nostr identity), every event-targeted
notification passed through unfiltered. Users who brought an existing
pubkey were still seeing their full Nostr notification history.

Three changes close the leak:

1. **Strict filter.** \`targets_deadcat_event\` now drops every
   event-targeted notification when the own-events set doesn't
   contain the target id, regardless of whether the set is empty.
   Brand-new users see an empty bell until they post from Deadcat —
   correct behaviour.

2. **Profile-level zaps drop too.** Kind:9735 receipts with no e-tag
   target a pubkey, not an event. They're indistinguishable from
   non-Deadcat zaps so they're dropped under the strict filter.

3. **Persistent own-events store.** New \`OwnEventsStore\` saves the
   set to \`<network>/own_deadcat_events.json\` so a cold start
   doesn't reset to empty before historical replay. Loaded as the
   subscription's hydrated baseline; appended on every new own-event.

4. **One-shot legacy prune.** A scheduled task (10s after subscription
   start) sweeps the persisted notifications file against the fully
   replayed own-events set, dropping anything that doesn't target a
   Deadcat-authored event. Cleans up records that leaked through
   during the bypass-era without requiring a manual "Mark all read"
   from the user. Delayed rather than per-event so historical replay
   has time to populate the full set before pruning runs.
Two narrow-viewport polish fixes that surfaced together:

- **Notifications empty state** was bleeding into the popover
  edges. Capped at 280px, added padding, leaning leading-relaxed
  so the line wraps inside its column instead of jamming against
  the borders.

- **Category bar** had a one-sided fade that only hinted at
  overflow on the right; users scrolling left would still see
  abrupt clipping ("ding" on the screenshot, where Trending was
  cut at the start). Two changes:

  - Added a mirror left fade so both edges show the gradient when
    there's content past them. Both fades widened from 32px to
    48px so the cue is harder to miss.
  - Each fade is gated by a data attribute (`data-overflow-left`
    / `data-overflow-right`) computed from `scrollLeft` /
    `scrollWidth` / `clientWidth`. ResizeObserver + scroll
    listener keep them in sync; transitions on opacity smooth
    the toggle so the gradients don't snap on/off.
  - Active pill auto-scrolls into view on activeCategory change,
    so a sign-in or deep-link that activates a clipped category
    no longer leaves the new state hidden behind the fade.

Hamburger / mobile-nav rework still tracked for the dedicated
mobile responsive pass.
Both popovers anchor to the top-shell action row and were happily
co-existing — clicking the bell with the avatar menu open left both
panels overlapping each other. Each toggle now closes the other so
the user only ever sees one floating panel from this row at a time.
@dmnyc
dmnyc merged commit 090d248 into master Apr 25, 2026
3 checks passed
@dmnyc
dmnyc deleted the feat/notifications-deadcat-scope branch April 25, 2026 18:59
tvolk131 pushed a commit that referenced this pull request May 6, 2026
…sh (#106)

* chore(ui): add right-edge fade + hidden scrollbar to category row

The category row already had \`overflow-x-auto whitespace-nowrap\`
so it scrolled horizontally on narrow viewports — but with no
visual cue that it was scrollable, "Ending Soon" looked like it
was simply being clipped. Two small additions:

- Hide the default scrollbar (\`[scrollbar-width:none]\` + the
  webkit-scrollbar pseudo-class). The OS scrollbar in this row is
  noise.
- Right-edge fade gradient via a \`::after\` pseudo-element on a
  new positioning wrapper. Communicates "more content this way"
  the moment the row overflows, without a hamburger commitment.

Pure progressive enhancement — at full width the fade sits over
empty space and is invisible. Hamburger / bottom-nav / mobile pass
is tracked in memory as its own future deep-dive once we actually
build for phones.

* feat(notifications): scope to deadcat-authored content via NIP-89 client tag

Users who bring an existing Nostr identity to Deadcat were seeing
their entire Nostr notifications stream in the bell — every reaction,
zap, and reply across the whole network, most of it about content
they posted from other clients. Two changes solve this:

1. **Universal NIP-89 client tag** on every event the app publishes.
   Previously only kind:1111 comments carried the tag; now reactions,
   deletions, follow lists, mute lists, profile updates, wallet
   backups, market announcements, attestations, pool announcements,
   limit orders, and order deletions all do too. Centralized via a
   shared `client_tag()` helper in the SDK so future builders can't
   forget it.

2. **Notifications subscription filters target events.** The
   subscription bundles a second filter (`{authors: [me]}`) alongside
   the original notifications filter, post-filters incoming events
   for the deadcat client tag, and tracks the resulting event IDs
   in an in-memory set. Inbound notifications (kind:7 / 9735 / 1111)
   are dropped unless their target `e` tag is in that set — i.e.,
   unless they target a deadcat-authored event. Profile-level zaps
   (kind:9735 with no `e` tag) still pass through since they target
   the user directly.

`parse_notification_event` takes a new `&HashSet<String>` parameter;
empty set opts out of filtering (used implicitly while the tracker
is hydrating on the first burst of historical events). Existing test
updated to seed the set with the parent comment id.

No new persistence — the tracker rebuilds on every startup from the
relay's historical replay. Persistence is a clean follow-up if the
cold-start race window becomes an issue in practice; today's mitigation
is that historical own-events typically arrive before historical
notifications targeting them.

* fix(notifications): strict deadcat-only filter + persist + prune legacy

The first cut of the deadcat-scoped notifications filter shipped with
an empty-set bypass: if the in-memory own-events tracker was empty
(cold-start window before historical replay completed, or a brand-new
user with an existing Nostr identity), every event-targeted
notification passed through unfiltered. Users who brought an existing
pubkey were still seeing their full Nostr notification history.

Three changes close the leak:

1. **Strict filter.** \`targets_deadcat_event\` now drops every
   event-targeted notification when the own-events set doesn't
   contain the target id, regardless of whether the set is empty.
   Brand-new users see an empty bell until they post from Deadcat —
   correct behaviour.

2. **Profile-level zaps drop too.** Kind:9735 receipts with no e-tag
   target a pubkey, not an event. They're indistinguishable from
   non-Deadcat zaps so they're dropped under the strict filter.

3. **Persistent own-events store.** New \`OwnEventsStore\` saves the
   set to \`<network>/own_deadcat_events.json\` so a cold start
   doesn't reset to empty before historical replay. Loaded as the
   subscription's hydrated baseline; appended on every new own-event.

4. **One-shot legacy prune.** A scheduled task (10s after subscription
   start) sweeps the persisted notifications file against the fully
   replayed own-events set, dropping anything that doesn't target a
   Deadcat-authored event. Cleans up records that leaked through
   during the bypass-era without requiring a manual "Mark all read"
   from the user. Delayed rather than per-event so historical replay
   has time to populate the full set before pruning runs.

* fix(ui): two-edge category bar fade + tighter notifications empty state

Two narrow-viewport polish fixes that surfaced together:

- **Notifications empty state** was bleeding into the popover
  edges. Capped at 280px, added padding, leaning leading-relaxed
  so the line wraps inside its column instead of jamming against
  the borders.

- **Category bar** had a one-sided fade that only hinted at
  overflow on the right; users scrolling left would still see
  abrupt clipping ("ding" on the screenshot, where Trending was
  cut at the start). Two changes:

  - Added a mirror left fade so both edges show the gradient when
    there's content past them. Both fades widened from 32px to
    48px so the cue is harder to miss.
  - Each fade is gated by a data attribute (`data-overflow-left`
    / `data-overflow-right`) computed from `scrollLeft` /
    `scrollWidth` / `clientWidth`. ResizeObserver + scroll
    listener keep them in sync; transitions on opacity smooth
    the toggle so the gradients don't snap on/off.
  - Active pill auto-scrolls into view on activeCategory change,
    so a sign-in or deep-link that activates a clipped category
    no longer leaves the new state hidden behind the fade.

Hamburger / mobile-nav rework still tracked for the dedicated
mobile responsive pass.

* tune(ui): soften category bar fades so active pills stay readable

* tune(ui): narrow category bar fades to 24px so they don't bleed onto pills

* fix(ui): close user menu when bell opens (and vice versa)

Both popovers anchor to the top-shell action row and were happily
co-existing — clicking the bell with the avatar menu open left both
panels overlapping each other. Each toggle now closes the other so
the user only ever sees one floating panel from this row at a time.

---------

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
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