Skip to content

feat(ui): show new-message notifications via the gateway shell bridge#409

Merged
sanity merged 2 commits into
mainfrom
fix/notifications-shell-bridge
Jul 18, 2026
Merged

feat(ui): show new-message notifications via the gateway shell bridge#409
sanity merged 2 commits into
mainfrom
fix/notifications-shell-bridge

Conversation

@sanity

@sanity sanity commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Browser notifications for new messages silently stopped working when River moved into the gateway's sandboxed iframe (freenet-core#3254, Feb 2026): the iframe has an opaque (null) origin, and browsers block the Web Notifications API from opaque-origin iframes, so permission can never reach granted and notifications never fire (#408). They worked before that, as a top-level page (Dec 2025 → Feb 2026). Reported by Ivvor.

Approach

Route notifications through the shell page (same-origin with the node, a real origin) over the existing __freenet_shell__ postMessage bridge — the same channel that already carries title/favicon updates. Paired with freenet-core#4801, which adds the shell-side handlers that actually show the notification.

  • Detect the iframe context (window.parent !== window). Served directly (dev / no-sync, a real origin), the existing direct Notifications API path is used unchanged.
  • In the iframe, request_permission_on_first_message asks the shell to offer notifications (the shell owns the real-origin permission prompt / affordance), and show_notification postMessages the notification instead of calling the API directly.
  • The room's owner key is encoded as the notification tag; the shell posts a notification_click reply carrying it back, and a window message listener (installed once from App, with the signal write deferred per the Dioxus signal-safety rules) routes CURRENT_ROOM to the clicked room.

Deployment / ordering

This is a no-op until a gateway serving the freenet-core#4801 shell is deployed, and neither side's deployment breaks the other (an old shell ignores the unknown message types; old River still calls the API directly, which no-ops in the iframe as it does today). The try.freenet.org gateway is a manual rebuild, so the feature lights up there once #4801 merges and that node is rebuilt.

Testing

Refs #408

🤖 Generated with Claude Code

sanity and others added 2 commits July 15, 2026 09:14
Browser notifications silently stopped working when River moved into the
gateway's sandboxed iframe (freenet-core#3254): the iframe has an opaque
(null) origin, and browsers block the Notifications API there, so
permission can never reach "granted" and notifications never fire
(#408). They worked before that as a top-level page.

Route notifications through the shell page (real origin) over the
existing `__freenet_shell__` postMessage bridge — the same channel that
already carries title/favicon. Paired with freenet-core#4801, which adds
the shell-side handlers.

- Detect the iframe context (`window.parent !== window`). When served
  directly (dev / `no-sync`, a real origin) the existing direct
  Notifications API path is used unchanged.
- In the iframe, `request_permission_on_first_message` asks the shell to
  offer notifications (it owns the real-origin permission prompt), and
  `show_notification` postMessages the notification to the shell instead
  of calling the API directly.
- Encode the room's owner key as the notification `tag`; the shell posts
  a `notification_click` reply carrying it back, and a window `message`
  listener (installed once from App, deferred signal write per the
  Dioxus signal-safety rules) routes CURRENT_ROOM to the clicked room.

Tests: tag round-trip + malformed-tag rejection (the click-routing
correctness). Native + wasm compile clean.

Refs #408

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyYK79ygyquHkDqnnBQ9Xe
…k, feature)

Multi-model review (Codex + a signal-safety lens) of #409 found no
signal-safety or correctness P1/P2 (the deferred CURRENT_ROOM write is
correct and required). Addressed:

- Codex P2: a notification click set CURRENT_ROOM but not MOBILE_VIEW, so
  on mobile the user stayed on the Rooms/Members panel instead of seeing
  the clicked conversation (and the room could be marked read without
  being shown). Set MOBILE_VIEW = Chat in the deferred callback, mirroring
  normal room selection.
- Lens P3: declare `MessageEvent` explicitly in ui/Cargo.toml's web-sys
  features — it was only enabled transitively via dioxus-web/devtools,
  which is fragile (a dep bump or --no-default-features would drop it).
- Lens P3: add an `event.source() == window.parent()` check to the
  notification_click listener (defense-in-depth against a stray
  postMessage forcing a room switch), and correct the two comments that
  overstated the guarantee / the iframe-detection specificity.

Native (431) + wasm compile clean.

Refs #408

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyYK79ygyquHkDqnnBQ9Xe
@sanity

sanity commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Multi-model review — Codex + a signal-safety/correctness lens

Verdict: no P1/P2 signal-safety or correctness bugs. The lens confirmed the deferred CURRENT_ROOM write is correct and required (raw JS message closure → no Dioxus scope → a synchronous write would hit the empty-scope panic), the iframe detection is sound (no cross-origin SecurityError), the listener is installed exactly once, and the tag round-trip / non-object-payload paths are panic-free. The dev/top-level path is unchanged.

Fixed in the latest commit

Finding Reviewer Fix
Notification click set CURRENT_ROOM but not MOBILE_VIEW → on mobile the user stays on the Rooms/Members panel instead of seeing the conversation (and it may be marked read without being shown) Codex P2 Set MOBILE_VIEW = Chat in the deferred callback, mirroring normal room selection
web_sys::MessageEvent only enabled transitively (via dioxus-web/devtools) — fragile lens P3 Declared explicitly in ui/Cargo.toml web-sys features
notification_click listener had no sender check lens P3 Added event.source() == window.parent() gate + corrected the two overclaiming comments

Documented, not changed (justified)

  • Message preview posted to parent with target origin "*" (lens P3, inherent): in the intended deployment the parent is the same-origin gateway shell — inside the user's trust boundary and it needs the text to render the notification. "*" is unavoidable because the opaque-origin iframe can't name the parent's real origin, and this matches the existing title/favicon bridge (document_title.rs), which posts to parent the same way. Acceptable.
  • The dev-only direct (top-level) path has the same pre-existing MOBILE_VIEW gap and a non-deferred CURRENT_ROOM write; left as-is to keep this PR scoped to the production (iframe) path, since that path only runs when River is served directly (dev / no-sync).

Aside (separate cleanup, not this PR)

The lens noted ui/src/components/app/tab_detection.rs appears to be orphaned dead code (no mod tab_detection; anywhere). Pre-existing and unrelated; worth a follow-up.

Paired with freenet-core#4801 (the shell-side handlers); this half is a harmless no-op until that ships and the gateway is rebuilt.

[AI-assisted - Claude]

@sanity
sanity merged commit 77c8649 into main Jul 18, 2026
6 checks passed
sanity added a commit that referenced this pull request Jul 18, 2026
UI-only republish of #409 (browser notifications via gateway shell bridge).
No WASM/migration change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B49wBfvR8EjpfYTw5muNV9
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