Skip to content

fix(desktop): stop DMs from firing duplicate desktop notifications#2105

Merged
tlongwell-block merged 2 commits into
mainfrom
eva/dm-double-notification
Jul 19, 2026
Merged

fix(desktop): stop DMs from firing duplicate desktop notifications#2105
tlongwell-block merged 2 commits into
mainfrom
eva/dm-double-notification

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Problem

Tyler: "DMs send two notifications." Every DM now carries a recipient p tag (added to let bots auto-reply in DMs), so an incoming DM matches both the live WebSocket DM subscription and the home-feed mention query — two toasts per DM.

Fix (two commits, two layered bugs)

Commit 1 — the eligibility seam. The intended DM-exclusion guard in eligibleFeedNotificationItems filtered on item.channelType !== "dm", but the feed emits no resolved channel type and enrichment ran only after eligibility. Fix: enrich mention items from the loaded channel list before the DM filter. Failover invariant: a DM in a channel not yet loaded stays feed-eligible (the WS path also bails on unknown channels), so no notification is lost.

Commit 2 — the null/undefined boundary (caught by live-testing the native binary; the relay-mode e2e alone missed it). Native get_feed serializes FeedItemInfo.channel_type (Option<String>) as null — the key is always present — while the enrichment guard tests channelType === undefined. null !== undefined, so DM items were never enriched and null !== "dm" sailed through. The e2e bridge omitted the key entirely (undefined), which is why Playwright went green while the real app stayed red. Fix at one seam: RawFeedItem.channel_type is honestly string | null and fromRawFeedItem canonicalizes null → undefined, restoring FeedItem's declared contract for all downstream consumers. The bridge now emits channel_type: null exactly like native serde, so the e2e exercises the native wire shape.

Verification

  • Unit: new guard cases + null/present channel_type canonicalization; full desktop suite 3171/3171 green.
  • Playwright dm-double-notification.spec.ts (relay mode, real signed events through relay ingest): red (2 toasts) at the pre-fix baseline; red again when canonicalization is bypassed at the new bridge shape (bypass requires a type cast — the honest string | null makes the naive regression a compile error); green (exactly 1 toast) with the fix.
  • Live native test: tauri dev app on this branch, 3 plain-DM probes from alice → exactly 3 toasts (all WS-path, title alice-tyler), zero feed-path leaks across multiple 30s feed polls; confirmed both via in-app instrumentation (not committed) and usernoted delivery records scoped to the app's notification source.

Related (not fixed here)

Native feed emits category: "mentions" (plural) while the frontend compares === "mention" (singular) everywhere — mention items always fall through to fallback titles/labels ("Needs Action in #…" toast title, inbox category labels). Broader contract bug, filed separately.

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 2 commits July 18, 2026 20:29
An incoming DM notified twice: once via the live WebSocket path
(useUnreadChannels -> handleDmNotification) and once via the home-feed
polling path (useFeedDesktopNotifications). The intended DM-exclusion
guard in eligibleFeedNotificationItems filtered on
item.channelType !== "dm", but the backend feed emits
channel_type: None and channel_name: "" (feed_item_from_event), so
every DM passed the guard as undefined !== "dm". Channel enrichment
ran only after eligibility, too late to rescue the filter.

Fix at the eligibility seam:
- enrichFeedItemChannel now fills channelName and channelType
  independently instead of early-returning whenever a name is present.
- eligibleFeedNotificationItems takes the loaded channel list and
  enriches mention and needs-action items BEFORE the DM filter.
- useFeedDesktopNotifications passes channels through and drops the
  now-redundant post-hoc enrichment in the notify loop.

Failover invariant preserved: a known DM notifies via WS only; a DM in
a channel not yet in the channel list stays feed-eligible (the WS path
also bails on unknown channels), so no notification is lost.

Adds unit coverage for the three guard cases and a relay-mode
Playwright spec proving exactly one notification per incoming DM
(red=2 before the fix, green=1 after, 3/3 repeats).

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Live testing the DM dedupe fix against the native Tauri binary still
produced two toasts for a plain DM. Root cause: native get_feed
serializes FeedItemInfo.channel_type (Option<String>) as null — the key
is always present — while the enrichment guard in enrichFeedItemChannel
tests channelType === undefined. null !== undefined, so DM feed items
were never enriched and null !== "dm" sailed through the exclusion
filter. The relay-mode e2e bridge omitted the key entirely (undefined),
which is why the Playwright spec went green while the real binary
stayed red.

Fix at one seam: RawFeedItem.channel_type is now honestly
string | null, and fromRawFeedItem canonicalizes null to undefined so
FeedItem's declared optional contract holds at runtime everywhere
downstream. The e2e bridge feed now emits channel_type: null exactly
like native serde, so the existing dm-double-notification spec
exercises the native wire shape: with canonicalization removed (via a
cast — the null-honest type makes the naive regression a compile
error), the spec fails with a duplicate toast; with it, green.

Adds unit coverage for the null and present channel_type shapes.

The leaked toast titled 'Needs Action in #alice-tyler' was a mention
item, not a needs_action item: notificationTitle compares
category === "mention" but native emits "mentions", so mention items
fall through to the fallback title. That plural/singular category
contract bug is broader than this fix and is filed separately.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block
tlongwell-block requested a review from a team as a code owner July 19, 2026 02:19
@tlongwell-block
tlongwell-block merged commit da2e259 into main Jul 19, 2026
25 checks passed
@tlongwell-block
tlongwell-block deleted the eva/dm-double-notification branch July 19, 2026 03:16
morgmart added a commit that referenced this pull request Jul 19, 2026
* origin/main:
  fix(desktop): stop DMs from firing duplicate desktop notifications (#2105)
  release(helm): buzz chart 0.1.6 (#2109)
kalvinnchau pushed a commit that referenced this pull request Jul 19, 2026
…y-membership-check

* origin/main:
  fix(desktop): stop DMs from firing duplicate desktop notifications (#2105)
  release(helm): buzz chart 0.1.6 (#2109)
  Route lag-tolerant reads to an optional Postgres read replica (#2084)

Co-authored-by: npub1ntr8jjcqq6gt06q5avvqttfjgpwshmra22pcmcagdnukw4ja4nqqsa9g54 <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1ntr8jjcqq6gt06q5avvqttfjgpwshmra22pcmcagdnukw4ja4nqqsa9g54 <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@sprout-oss.stage.blox.sqprod.co>
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