Skip to content

Accidental swipe in message viewer marks unread email as read #10747

@digitalby

Description

@digitalby

Accidental swipe in message viewer marks unread email as read

Description

When using the swipe left/right gesture to navigate between messages in the message viewer,
a fast but unintentional gesture - a wrist flick, an accidental pocket interaction, or a
quick motion while trying to scroll down - can silently trigger a full page navigation. The
newly-visited message is immediately marked as read, even if the user swipes back in the
same motion.

This is a significant issue for inbox-zero workflows and anyone managing a high-volume inbox,
because unread count and read state are meaningful signals that get corrupted by incidental
gestures.

Steps to reproduce

  1. Ensure "Mark messages as read when opened" is enabled (Settings → Account → Reading).
  2. Have at least two unread messages in a folder.
  3. Open the first message.
  4. Make a quick, fast flick gesture to the left (toward the next message) - not a slow, deliberate
    drag, but a short, fast wrist motion.
  5. Swipe back immediately to return to the original message.

Expected behavior

The adjacent message remains unread. Navigation should only occur - and mark-as-read should
only trigger - on a clearly deliberate, sustained swipe gesture.

Actual behavior

The adjacent message is marked as read. Even if the user returns to it immediately, the
SEEN flag has already been set (both locally and queued for IMAP sync). The unread indicator
disappears.

Root cause (technical)

The message viewer uses ViewPager2 for left/right navigation, with a custom
ItemTouchHelper in MessageViewContainerFragment to manage swipe sensitivity:

// MessageViewContainerFragment.kt
private const val VIEW_PAGER_SWIPE_THRESHOLD = 0.4f
private const val VIEW_PAGER_SWIPE_VELOCITY_THRESHOLD = 0.8f

The ItemTouchHelper.onSwiped callback fires on either condition:

  1. The swipe covers >= 40% of screen width, OR
  2. The swipe velocity exceeds 0.8x the ItemTouchHelper default

Condition (2) is the problem. At 0.8x of the default, a casual wrist flick consistently
triggers a page change even though the screen barely moves. onSwiped then calls
viewPager.currentItem += 1, which causes the adjacent MessageViewFragment to become
the active page and call markMessageAsOpened() in onResume().

Proposed fix - Phase 1 (minimal)

Raise the velocity threshold significantly so that only a forceful, deliberate flick completes
a swipe. The distance threshold (40%) is already reasonable and can stay.

private const val VIEW_PAGER_SWIPE_THRESHOLD = 0.4f
private const val VIEW_PAGER_SWIPE_VELOCITY_THRESHOLD = 3.0f  // was 0.8f

Proposed enhancement - Phase 2 (deferred)

Decouple mark-as-read from the moment the fragment is first resumed. Instead, mark as read
only after a meaningful reading signal:

  • The user has remained on the message for ≥ N seconds (e.g. 2s)
  • The user has scrolled within the email body
  • The user navigates back to the message list

This would be implemented with a Handler.postDelayed() in MessageViewFragment.onResume(),
cancelled in onPause() if the user leaves before the delay fires.

Proposed enhancement - Phase 3 (UX, deferred)

Add a visible read/unread state indicator to the message viewer toolbar, so users can
immediately see whether an email has been marked read during their session and act on it.

Currently, the toggle read/unread action exists but is buried in the overflow menu, or appearing as a rather confusing icon button. Surfacing the current state - and ideally a one-tap toggle - in the toolbar would make accidental
mark-as-read recoverable easily.

Environment

  • Reproducible on: Thunderbird for Android (current Gplay production app)
  • Affected setting: "Mark messages as read when opened" (enabled by default)
  • Not affected: users with that setting disabled

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    In Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions