feat: scope notifications to deadcat-authored content + topshell polish - #106
Merged
Conversation
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
client_tag()helper in the SDK so future builders can't forget it.{authors: [me]}) alongside the original notifications filter, post-filters incoming events for the deadcat client tag, and tracks the resulting event IDs in a persistedOwnEventsStore(sibling ofnotifications.jsonunder<app_data_dir>/<network>/). Inbound notifications (kind:7 / 9735 / 1111) are dropped unless their targetetag is in that set.Topshell polish
data-overflow-*attributes computed fromscrollLeft/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."activeCategorychanges, so a sign-in (which adds "My Markets") or a deep link to a clipped category doesn't leave the new active state hidden.Commits (oldest → newest)
4bf8da8chore(ui): add right-edge fade + hidden scrollbar to category rowdf73773feat(notifications): scope to deadcat-authored content via NIP-89 client tagb472301fix(notifications): strict deadcat-only filter + persist + prune legacy0fa8567fix(ui): two-edge category bar fade + tighter notifications empty state0e1975ctune(ui): soften category bar fades so active pills stay readable9e48ac7tune(ui): narrow category bar fades to 24px so they don't bleed onto pillsb9350b2fix(ui): close user menu when bell opens (and vice versa)Out of scope
Test plan