Skip to content

Add Gmail-style vertical swipe-to-select for thread multi-selection - #2928

Draft
JorisBodin with Copilot wants to merge 2 commits into
mainfrom
copilot/add-swipe-to-select-gesture
Draft

Add Gmail-style vertical swipe-to-select for thread multi-selection#2928
JorisBodin with Copilot wants to merge 2 commits into
mainfrom
copilot/add-swipe-to-select-gesture

Conversation

Copilot AI commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

This change adds drag-to-select behavior to the thread list while multi-selection mode is already active. Users can now vertically swipe across items to select or deselect multiple threads in one gesture, without interfering with horizontal swipe actions.

  • New touch listener for drag selection

    • Added SwipeToSelectTouchListener (RecyclerView.OnItemTouchListener) in ui/main/folder.
    • Activates only when:
      • multi-select is enabled, and
      • gesture is clearly vertical (dy > touchSlop && dy > dx).
    • Captures gesture intent from the first touched thread:
      • first thread unselected → select all crossed threads
      • first thread selected → deselect all crossed threads
    • Applies intent per crossed item, publishes selection updates, sends SELECTED_STATE payload updates, and triggers haptic feedback when crossing item boundaries.
    • Resets listener state on UP, CANCEL, or disallow-intercept.
  • Adapter API for position-to-thread lookup

    • Added ThreadListAdapter.getThreadAt(position: Int): Thread? to expose content-thread access safely from adapter positions.
  • Fragment wiring

    • Registered SwipeToSelectTouchListener in ThreadListFragment.setupAdapter().
    • Bound it to MainViewModel multi-selection state via a MultiSelectionListener<Thread> adapter.
if (dy > touchSlop && dy > dx) {
    val startThread = (rv.adapter as? ThreadListAdapter)?.getThreadAt(startPosition)
    selectionIntent = startThread != null && !multiSelection.selectedItems.contains(startThread)
}
Original prompt

Goal

Add a "swipe-to-select" gesture to the email thread list so that, when multi-selection mode is already active, the user can drag their finger vertically across list items to select (or deselect) multiple threads in one motion — similar to Gmail on Android.


Context

Relevant files (all in app/src/main/java/com/infomaniak/mail/ui/main/folder/):

  • ThreadListAdapter.kt — extends DragDropSwipeAdapter. Multi-select is toggled via toggleMultiSelectedThread(). Exposes multiSelection: MultiSelectionListener<Thread> (private). dataSet comes from the parent DragDropSwipeAdapter.
  • ThreadListFragment.kt — sets up the RecyclerView in setupAdapter(). Attaches the swipeListener (horizontal actions). Calls unlockSwipeActionsIfSet() which disables horizontal swipe directions when isMultiSelectOn == true.
  • MultiSelectionListener.kt — interface with isEnabled, selectedItems: MutableSet<Thread>, publishSelectedItems: () -> Unit.
  • ThreadListMultiSelection.kt — handles multi-select actions (archive, delete, etc.).
  • ThreadListItem.kt — sealed class; ThreadListItem.Content(val thread: Thread) is the item type for emails.

What to implement

1. New file: SwipeToSelectTouchListener.kt

Create app/src/main/java/com/infomaniak/mail/ui/main/folder/SwipeToSelectTouchListener.kt.

This class implements RecyclerView.OnItemTouchListener and provides Gmail-style drag-to-select behaviour:

  • onInterceptTouchEvent:

    • On ACTION_DOWN: record the start coordinates and the item position under the finger. Do NOT intercept yet.
    • On ACTION_MOVE: if multiSelection.isEnabled is true AND the vertical displacement exceeds scaledTouchSlop AND the vertical displacement is greater than the horizontal displacement (so it's clearly a vertical drag, not a horizontal swipe), set isActive = true and return true to steal the event stream. When activating, record the selection state of the item at the start position (was it already selected?) — this determines the intent for the whole gesture: if the first item was unselected → the gesture selects all touched items; if it was selected → the gesture deselects all touched items.
    • Otherwise return isActive.
  • onTouchEvent:

    • On ACTION_MOVE: use rv.findChildViewUnder(e.x, e.y) to find the item currently under the finger. If it is a new position (not the last one processed), and it is a ThreadListItem.Content, apply the intent (add or remove from selectedItems), call publishSelectedItems(), and call rv.adapter?.notifyItemChanged(pos, ThreadListAdapter.NotificationType.SELECTED_STATE). Also perform haptic feedback (HapticFeedbackConstants.VIRTUAL_KEY) when the finger crosses from one item to the next.
    • On ACTION_UP / ACTION_CANCEL: reset isActive, lastSelectedPosition, startY, startX, selectionIntent.
  • onRequestDisallowInterceptTouchEvent: if disallowIntercept is true, reset isActive.

package com.infomaniak.mail.ui.main.folder

import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.recyclerview.widget.RecyclerView
import com.infomaniak.mail.data.models.thread.Thread
import kotlin.math.abs

class SwipeToSelectTouchListener(
    recyclerView: RecyclerView,
    private val multiSelection: MultiSelectionListener<Thread>,
) : RecyclerView.OnItemTouchListener {

    private val touchSlop = ViewConfiguration.get(recyclerView.context).scaledTouchSlop
    private var isActive = false
    private var startX = 0f
    private var startY = 0f
    private var lastSelectedPosition = RecyclerView.NO_POSITION
    private var selectionIntent: Boolean? = null // true = select, false = deselect

    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
        when (e.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                startX = e.x
                startY = e.y
                lastSelectedPosition = RecyclerView.NO_POSITION
                selectionIntent = null
                isActive = false
            }
            MotionEvent.ACTION_MOVE -> {
                if (!isActive && multiSelection.isEnabled) {
                    val dy = abs(e.y - startY)
                    val dx = abs(e.x - startX)
                    if (dy > touchSlop && dy > dx) {
                        // Determine intent from the item at the start position
                        val startView = rv.findChildViewUnder(startX, startY)
                        if (startView != null) {
                            val startPos = rv.getChildAdapterPosition(startView)
                            if (startPos != RecyclerView.NO_POSITION) {
                                val startItem = (rv.adapter as? ThreadListAdapter)?.getThreadAt(startPos)
                                selectionIntent = startItem != null && !multiSelection.selectedItems.contai...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI changed the title [WIP] Add swipe-to-select gesture for email thread list Add Gmail-style vertical swipe-to-select for thread multi-selection Jun 2, 2026
Copilot AI requested a review from JorisBodin June 2, 2026 15:30
@sonarqubecloud

sonarqubecloud Bot commented Jun 2, 2026

Copy link
Copy Markdown

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.

2 participants