fix(notifications): tapping a new-mail notification does not open that email #56

Closed
opened 2026-07-01 20:45:14 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-01 20:45:14 +00:00 (Migrated from github.com)

Bug

Tapping a new-mail notification brings LibreMail to the foreground wherever it last was — it never navigates to the email the notification announced.

Root cause

Two-sided:

  1. The notification carries no target. MailNotifier.contentIntent() builds a bare Intent(context, MainActivity::class.java) with no message id. Worse, every per-message notification and the group summary share that one PendingIntent (request code 0, filterEquals-identical intents).
  2. Nothing handles a target anyway. MainActivity only parses compose intents (IntentComposeParser); there is no "open message" path into the NavHost.

Fix plan

Deep-link the per-message notification into the existing reader route (Routes.reader(messageId)), mirroring the established pendingCompose intent-handoff pattern (MainActivity → LibreMailApp → LaunchedEffect → navController.navigate).

  • New notifications/NotificationIntents.kt — owns the open-message intent contract: ACTION_OPEN_MESSAGE + EXTRA_MESSAGE_ID + a per-message data URI (libremail://message/{id}). The data URI is load-bearing: PendingIntent identity ignores extras, so without distinct URIs all messages would collapse onto one FLAG_UPDATE_CURRENT PendingIntent and every notification would open the most-recently-notified message. No manifest change (explicit intent — no new exported surface).
  • MailNotifier.kt — per-message notifications get the deep-link content intent; the group summary keeps the plain open-the-app intent.
  • MainActivity.kt — parse the message id in onCreate (fresh launch only) and onNewIntent (warm tap), hold as pendingOpenMessageId state beside pendingCompose.
  • LibreMailApp.kt — LaunchedEffect navigates to Routes.reader(id) and consumes the pending id. Reader pushes onto the current stack, so back lands in the mailbox.
  • New NotificationIntentsTest.kt (androidTest) — round-trip parse, null for launcher/mailto intents, and filterEquals uniqueness across message ids.

This is safe because MailSyncer persists new messages before notifying (same NonCancellable block), so the tapped message row always exists locally; a message since deleted server-side hits the reader's existing error state.

Acceptance criteria

  • Warm tap (app backgrounded): reader opens the exact message announced; back returns to the mailbox.
  • Cold tap (app swiped from recents): app cold-starts straight into the reader for that message.
  • With 2+ notifications showing, tapping an older one opens that message, not the newest.
  • Launcher open, mailto: links, and share-to-LibreMail still open compose as before.
  • CI passed gate green (unit tests, lint, ktlint, detekt, E2E matrix).

Out of scope (follow-up ideas)

  • Group-summary tap deep-linking to the account-filtered mailbox (Routes.mailboxForAccount).
  • Auto-clearing per-message notifications when mail is read on another device.
## Bug Tapping a new-mail notification brings LibreMail to the foreground wherever it last was — it never navigates to the email the notification announced. ## Root cause Two-sided: 1. **The notification carries no target.** `MailNotifier.contentIntent()` builds a bare `Intent(context, MainActivity::class.java)` with no message id. Worse, every per-message notification and the group summary share that one `PendingIntent` (request code 0, `filterEquals`-identical intents). 2. **Nothing handles a target anyway.** `MainActivity` only parses compose intents (`IntentComposeParser`); there is no "open message" path into the NavHost. ## Fix plan Deep-link the per-message notification into the existing reader route (`Routes.reader(messageId)`), mirroring the established `pendingCompose` intent-handoff pattern (`MainActivity` → `LibreMailApp` → `LaunchedEffect` → `navController.navigate`). - **New `notifications/NotificationIntents.kt`** — owns the open-message intent contract: `ACTION_OPEN_MESSAGE` + `EXTRA_MESSAGE_ID` + a per-message `data` URI (`libremail://message/{id}`). The data URI is load-bearing: `PendingIntent` identity ignores extras, so without distinct URIs all messages would collapse onto one `FLAG_UPDATE_CURRENT` `PendingIntent` and every notification would open the most-recently-notified message. No manifest change (explicit intent — no new exported surface). - **`MailNotifier.kt`** — per-message notifications get the deep-link content intent; the group summary keeps the plain open-the-app intent. - **`MainActivity.kt`** — parse the message id in `onCreate` (fresh launch only) and `onNewIntent` (warm tap), hold as `pendingOpenMessageId` state beside `pendingCompose`. - **`LibreMailApp.kt`** — `LaunchedEffect` navigates to `Routes.reader(id)` and consumes the pending id. Reader pushes onto the current stack, so back lands in the mailbox. - **New `NotificationIntentsTest.kt` (androidTest)** — round-trip parse, null for launcher/mailto intents, and `filterEquals` uniqueness across message ids. This is safe because `MailSyncer` persists new messages before notifying (same `NonCancellable` block), so the tapped message row always exists locally; a message since deleted server-side hits the reader's existing error state. ## Acceptance criteria - Warm tap (app backgrounded): reader opens the exact message announced; back returns to the mailbox. - Cold tap (app swiped from recents): app cold-starts straight into the reader for that message. - With 2+ notifications showing, tapping an **older** one opens **that** message, not the newest. - Launcher open, `mailto:` links, and share-to-LibreMail still open compose as before. - `CI passed` gate green (unit tests, lint, ktlint, detekt, E2E matrix). ## Out of scope (follow-up ideas) - Group-summary tap deep-linking to the account-filtered mailbox (`Routes.mailboxForAccount`). - Auto-clearing per-message notifications when mail is read on another device.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#56