Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5856862
feat: Refactor of actions to use messages instead of threads
solrubado Jan 30, 2026
70fadc0
refactor: Remove unnused variables, add parameters name
solrubado Feb 2, 2026
3fac308
refactor: Separate emoji functionality
solrubado Feb 17, 2026
054f2af
fix: FolderRolesActions to be able to delete properly schedule messag…
solrubado May 7, 2026
0e5b288
refactor: Remove messages related functions from SharedUtils
solrubado Feb 20, 2026
c8da378
feat: Add multiselection interface
solrubado Jan 21, 2026
c89e724
feat: Multiselect ui in search
solrubado Jan 26, 2026
a59940c
refactor: Create MultiselectionViewModel
solrubado May 13, 2026
aca1a7a
feat: Actions multiselect in search
solrubado May 20, 2026
ce8869f
feat: Add swipe, fix share email action and refresh move
solrubado May 22, 2026
c2a1f32
fix: FolderRoles for deleting dialog confirmation
solrubado May 27, 2026
a86d0b3
fix: Add isFromSearch in SearchFragment directions
solrubado May 27, 2026
9c29bb7
refactor: Add argument names and use action argument instead of actio…
solrubado May 27, 2026
1d055cb
refactor: Don't call searchResults first twice
solrubado May 27, 2026
1d602b9
fix: Select current folder as default in search
solrubado May 27, 2026
99f3400
fix: Don't use host as lifecycleowner
solrubado May 27, 2026
037de6b
fix: update select all label
solrubado May 27, 2026
57e2991
fix: Fix copilot recommendations
solrubado May 27, 2026
2870433
fix: Fix rebase
solrubado May 28, 2026
644e880
fix: Refresh after actions in SearchFragment and make thread folder t…
solrubado May 28, 2026
85bacd9
fix: Use folderId instead of folder for archive and delete since it's…
solrubado May 28, 2026
182c5c9
refactor: Make variable private
solrubado May 28, 2026
7a6338f
fix: Don't use current folder since from search the source folder can…
solrubado May 28, 2026
9be43e9
refactor: Remove unused viewmodel
solrubado May 28, 2026
25d68d1
fix: Make isFromArchive work with multiselect dans la search
solrubado May 28, 2026
8921038
refactor: Use same swipe functions for search and threadlist
solrubado May 28, 2026
77c4460
fix: Use correct folderId
solrubado May 28, 2026
28198aa
fix: Fix copilot suggestions
solrubado May 28, 2026
4179d20
refactor: Improve duplicated code
solrubado May 29, 2026
48dfa43
fix: Make sure there is space for banners to show, let the recycler v…
solrubado May 29, 2026
443dfba
refactor: Improve setupMultiselectActions
solrubado May 29, 2026
5cb94b5
refactor: Improve code
solrubado May 29, 2026
24055e6
refactor: Add missing commas
solrubado May 29, 2026
941b822
refactor: Create extensions for duplicated code in swipe actions
solrubado May 29, 2026
31aa29f
fix: Is from search directions
solrubado May 29, 2026
5b50fdf
fix: Change channel for search refresh to confluated
solrubado May 29, 2026
31b632a
feat: Add ThreadSwipeListenerFactory to remove duplicated objects
solrubado May 29, 2026
119df98
refactor: Improve details
solrubado May 29, 2026
0103d98
fix: Make multiselectionviewmodel null safe
solrubado May 29, 2026
136dbc2
fix: Show block multiple users dialog in search
solrubado May 29, 2026
5b313f6
fix: Remove unnecessary mainviewmodel
solrubado May 29, 2026
d3f9e91
fix: Use action passed by parameters
solrubado May 29, 2026
93feeab
fix: Fix share email in search
solrubado May 29, 2026
ff6d9f5
refactor: Remove unused folder id and import
solrubado May 29, 2026
23ddf93
fix: Change threadlist binding type
solrubado May 29, 2026
d024746
fix: Notify search refresh even if the call didn't work, since the th…
solrubado May 29, 2026
7940974
fix: Archive action when they come from different folders
solrubado May 29, 2026
d6a5b3d
fix: Refresh search with contacts when multiselection is closed
solrubado May 29, 2026
6547a52
fix: Fix showing refresh laying after coming back to search
solrubado May 29, 2026
ee37200
fix: Use correct folderRole when swiping and change files names
solrubado May 29, 2026
8655953
refactor: Rename multiselection toolbar so it doesn't conflict with t…
solrubado May 29, 2026
1d36fd1
fix: Fix crash on search because binding was null
solrubado Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerFragment
import com.infomaniak.mail.ui.main.onboarding.PermissionsOnboardingPagerFragment
import com.infomaniak.mail.ui.main.search.SearchFragmentArgs
import com.infomaniak.mail.ui.main.thread.actions.ActionsViewModel
import com.infomaniak.mail.ui.main.thread.actions.multiselection.MultiselectionViewModel
import com.infomaniak.mail.ui.newMessage.NewMessageActivity
import com.infomaniak.mail.ui.sync.SyncAutoConfigActivity
import com.infomaniak.mail.ui.sync.discovery.SyncDiscoveryManager
Expand Down Expand Up @@ -115,6 +116,7 @@ class MainActivity : BaseActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val mainViewModel: MainViewModel by viewModels()
private val actionsViewModel: ActionsViewModel by viewModels()
private val multiselectionViewModel: MultiselectionViewModel by viewModels()

private val navigationArgs: MainActivityArgs? by lazy { intent?.extras?.let(MainActivityArgs::fromBundle) }

Expand Down Expand Up @@ -467,7 +469,7 @@ class MainActivity : BaseActivity() {
}

fun closeMultiSelect() {
mainViewModel.isMultiSelectOn = false
multiselectionViewModel.isMultiSelectOn = false
}

fun popBack() {
Expand All @@ -481,7 +483,7 @@ class MainActivity : BaseActivity() {
onBackPressedDispatcher.addCallback(this@MainActivity) {
when {
drawerLayout.isOpen -> closeDrawer()
mainViewModel.isMultiSelectOn -> closeMultiSelect()
multiselectionViewModel.isMultiSelectOn -> closeMultiSelect()
else -> popBack()
}
}
Expand Down
31 changes: 0 additions & 31 deletions app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ import com.infomaniak.core.network.networking.ManualAuthorizationRequired
import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError
import com.infomaniak.core.sentry.SentryLog
import com.infomaniak.core.ui.showToast
import com.infomaniak.mail.MatomoMail.MatomoName
import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.api.ApiRepository
Expand Down Expand Up @@ -81,7 +79,6 @@ import com.infomaniak.mail.utils.SharedUtils
import com.infomaniak.mail.utils.SharedUtils.Companion.updateSignatures
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.Utils.EML_CONTENT_TYPE
import com.infomaniak.mail.utils.Utils.runCatchingRealm
import com.infomaniak.mail.utils.coroutineContext
import com.infomaniak.mail.utils.extensions.MergedContactDictionary
import com.infomaniak.mail.utils.extensions.allFailed
Expand Down Expand Up @@ -173,19 +170,7 @@ class MainViewModel @Inject constructor(
val mailboxesLive = mailboxController.getMailboxesAsync(AccountUtils.currentUserId).asLiveData(ioCoroutineContext)

//region Multi selection
val isMultiSelectOnLiveData = MutableLiveData(false)
inline var isMultiSelectOn
get() = isMultiSelectOnLiveData.value!!
set(value) {
isMultiSelectOnLiveData.value = value
}

val selectedThreadsLiveData = MutableLiveData(mutableSetOf<Thread>())
inline val selectedThreads
get() = selectedThreadsLiveData.value!!

val isEverythingSelected
get() = runCatchingRealm { selectedThreads.count() == currentThreadsLive.value?.list?.count() }.getOrDefault(false)
//endregion

//region Current Mailbox
Expand Down Expand Up @@ -819,22 +804,6 @@ class MainViewModel @Inject constructor(

suspend fun getMessage(messageUid: String): Message? = messageController.getMessage(messageUid)

fun selectOrUnselectAll() {
if (isEverythingSelected) {
trackMultiSelectionEvent(MatomoName.None)
selectedThreads.clear()
} else {
trackMultiSelectionEvent(MatomoName.All)
currentThreadsLive.value?.list?.forEach { thread -> selectedThreads.add(thread) }
}

publishSelectedItems()
}

fun publishSelectedItems() {
selectedThreadsLiveData.value = selectedThreads
}

fun refreshDraftFolderWhenDraftArrives(scheduledMessageEtop: Long) = viewModelScope.launch(ioCoroutineContext) {
val folder = folderController.getFolder(FolderRole.DRAFT)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,201 +20,203 @@ package com.infomaniak.mail.ui.main.folder
import androidx.navigation.fragment.findNavController
import com.infomaniak.core.fragmentnavigation.safelyNavigate
import com.infomaniak.core.matomo.Matomo.TrackerAction
import com.infomaniak.core.sentry.SentryLog
import com.infomaniak.mail.MatomoMail.MatomoCategory
import com.infomaniak.mail.MatomoMail.trackEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.SwipeAction
import com.infomaniak.mail.data.models.isSnoozed
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
import com.infomaniak.mail.ui.main.folderPicker.FolderPickerAction
import com.infomaniak.mail.ui.main.settings.appearance.swipe.SwipeActionsSettingsFragment
import com.infomaniak.mail.ui.main.thread.ThreadViewModel.SnoozeScheduleType
import com.infomaniak.mail.utils.extensions.animatedNavigation
import com.infomaniak.mail.utils.extensions.archiveWithConfirmationPopup
import com.infomaniak.mail.utils.extensions.deleteWithConfirmationPopup
import com.infomaniak.mail.utils.extensions.getAnimatedNavOptions
import com.infomaniak.mail.utils.extensions.moveWithConfirmationPopup
import com.infomaniak.core.common.R as RCore

object PerformSwipeActionManager {

/**
* The boolean return value is used to know if we should keep the Thread in
* the RecyclerView (true), or remove it when the swipe is done (false).
*/
fun ThreadListFragment.performSwipeAction(
fun performSwipeAction(
host: SwipeActionHost,
swipeAction: SwipeAction,
thread: Thread,
position: Int,
isPermanentDeleteFolder: Boolean,
currentMailbox: Mailbox,
): Boolean {
val folderRole = thread.folder.role
if (!swipeAction.canDisplay(folderRole, mainViewModel.featureFlagsLive.value, localSettings)) {
snackbarManager.setValue(getString(R.string.snackbarSwipeActionIncompatible))
if (!swipeAction.canDisplay(folderRole, host.mainViewModel.featureFlagsLive.value, host.localSettings)) {
host.showSwipeActionIncompatible()
return true
}

trackEvent(MatomoCategory.SwipeActions, swipeAction.matomoName, TrackerAction.DRAG)

val shouldKeepItemBecauseOfAction = performSwipeAction(
val shouldKeepItemBecauseOfAction = performSwipeActionInternal(
host = host,
swipeAction = swipeAction,
folderRole = folderRole,
thread = thread,
position = position,
isPermanentDeleteFolder = isPermanentDeleteFolder,
currentMailbox = currentMailbox
)

val shouldKeepItemBecauseOfNoConnection = !mainViewModel.hasNetwork
val shouldKeepItemBecauseOfNoConnection = !host.mainViewModel.hasNetwork
return shouldKeepItemBecauseOfAction || shouldKeepItemBecauseOfNoConnection
}

private fun ThreadListFragment.performSwipeAction(
private fun performSwipeActionInternal(
host: SwipeActionHost,
swipeAction: SwipeAction,
folderRole: FolderRole?,
thread: Thread,
position: Int,
isPermanentDeleteFolder: Boolean
isPermanentDeleteFolder: Boolean,
currentMailbox: Mailbox,
): Boolean {
val currentMailbox = mainViewModel.currentMailbox.value ?: run {
snackbarManager.setValue(getString(RCore.string.anErrorHasOccurred))
SentryLog.e("PerformSwipeActionManager", getString(R.string.sentryErrorMailboxIsNull)) { scope ->
scope.setTag("context", "PerformSwipeActionManager.performSwipeAction")
}
return true
}

return when (swipeAction) {
SwipeAction.TUTORIAL -> {
localSettings.setDefaultSwipeActions()
safelyNavigate(ThreadListFragmentDirections.actionThreadListFragmentToSettingsFragment())
findNavController().navigate(R.id.swipeActionsSettingsFragment, args = null, getAnimatedNavOptions())
host.localSettings.setDefaultSwipeActions()
host.fragment.findNavController()
.navigate(R.id.swipeActionsSettingsFragment, args = null, getAnimatedNavOptions())
true
}

SwipeAction.ARCHIVE -> {
handleArchiveSwipe(thread, position, folderRole, currentMailbox)
handleArchiveSwipe(host, thread, position, folderRole, currentMailbox)
}

SwipeAction.DELETE -> {
handleDeleteSwipe(thread, position, isPermanentDeleteFolder, currentMailbox)
handleDeleteSwipe(host, thread, position, isPermanentDeleteFolder, currentMailbox)
}

SwipeAction.FAVORITE -> {
actionsViewModel.toggleThreadsFavoriteStatus(threadsUids = listOf(thread.uid), mailbox = currentMailbox)
host.actionsViewModel.toggleThreadsFavoriteStatus(
threadsUids = listOf(thread.uid),
shouldFavorite = !thread.isFavorite,
mailbox = currentMailbox
)
true
}

SwipeAction.MOVE -> {
val navController = findNavController()
descriptionDialog.moveWithConfirmationPopup(folderRole, count = 1) {
val navController = host.fragment.findNavController()
host.descriptionDialog.moveWithConfirmationPopup(folderRole, count = 1) {
navController.animatedNavigation(
directions = ThreadListFragmentDirections.actionThreadListFragmentToFolderPickerFragment(
threadsUids = arrayOf(thread.uid),
action = FolderPickerAction.MOVE,
sourceFolderId = mainViewModel.currentFolderId ?: Folder.DUMMY_FOLDER_ID
),
directions = host.directionsToMove(
threadUid = thread.uid,
sourceFolderId = thread.folderId,
)
)
}
true
}

SwipeAction.QUICKACTIONS_MENU -> {
safelyNavigate(
ThreadListFragmentDirections.actionThreadListFragmentToThreadActionsBottomSheetDialog(
threadUid = thread.uid,
shouldLoadDistantResources = false,
)
)
host.fragment.safelyNavigate(host.directionsToQuickActions(thread.uid))
true
}

SwipeAction.READ_UNREAD -> {
actionsViewModel.toggleThreadsSeenStatus(
host.actionsViewModel.toggleThreadsSeenStatus(
threadsUids = listOf(thread.uid),
currentFolderId = mainViewModel.currentFolderId,
mailbox = currentMailbox
shouldRead = !thread.isSeen,
currentFolderId = thread.folderId,
Comment thread
solrubado marked this conversation as resolved.
Comment thread
solrubado marked this conversation as resolved.
mailbox = currentMailbox,
)
mainViewModel.currentFilter.value != ThreadFilter.UNSEEN
true
}
Comment thread
solrubado marked this conversation as resolved.

SwipeAction.SPAM -> {
actionsViewModel.toggleThreadsSpamStatus(
host.actionsViewModel.toggleThreadsSpamStatus(
threads = setOf(thread),
currentFolderId = mainViewModel.currentFolderId,
mailbox = currentMailbox
currentFolderId = thread.folderId,
mailbox = currentMailbox,
)
false
}

SwipeAction.SNOOZE -> {
val snoozeScheduleType = if (thread.isSnoozed()) {
SnoozeScheduleType.Modify(thread.uid)
} else {
SnoozeScheduleType.Snooze(thread.uid)
}
navigateToSnoozeBottomSheet(snoozeScheduleType, thread.snoozeEndDate)
host.navigateToSnoozeBottomSheet(snoozeScheduleType, thread.snoozeEndDate)
true
}

SwipeAction.NONE -> error("Cannot swipe on an action which is not set")
}
}

private fun ThreadListFragment.handleArchiveSwipe(
private fun handleArchiveSwipe(
host: SwipeActionHost,
thread: Thread,
position: Int,
folderRole: FolderRole?,
currentMailBox: Mailbox
currentMailBox: Mailbox,
): Boolean {
fun onCancel() {
// Notify only if the user cancelled the popup (e.g. the thread is not deleted),
// otherwise it will notify the next item in the list and make it slightly blink
if (threadListAdapter.dataSet.indexOfFirstThread(thread) == position) {
threadListAdapter.notifyItemChanged(position)
if (host.threadListAdapter.dataSet.indexOfFirstThread(thread) == position) {
host.threadListAdapter.notifyItemChanged(position)
}
}

fun onSuccess() {
actionsViewModel.archiveThreads(
host.actionsViewModel.archiveThreads(
threads = listOf(thread),
currentFolder = mainViewModel.currentFolder.value,
mailbox = currentMailBox
currentFolderId = thread.folderId,
mailbox = currentMailBox,
)
}

return descriptionDialog.archiveWithConfirmationPopup(
return host.descriptionDialog.archiveWithConfirmationPopup(
folderRole = folderRole,
count = 1,
displayLoader = false,
onCancel = ::onCancel,
onPositiveButtonClicked = ::onSuccess
onPositiveButtonClicked = ::onSuccess,
)
}

private fun ThreadListFragment.handleDeleteSwipe(
private fun handleDeleteSwipe(
host: SwipeActionHost,
thread: Thread,
position: Int,
isPermanentDeleteFolder: Boolean,
currentMailBox: Mailbox
currentMailBox: Mailbox,
): Boolean {
fun onCancel() {
// Notify only if the user cancelled the popup (e.g. the thread is not deleted),
// otherwise it will notify the next item in the list and make it slightly blink
if (threadListAdapter.dataSet.indexOfFirstThread(thread) == position) {
threadListAdapter.notifyItemChanged(position)
if (host.threadListAdapter.dataSet.indexOfFirstThread(thread) == position) {
host.threadListAdapter.notifyItemChanged(position)
}
}

fun onHandleDelete() {
if (isPermanentDeleteFolder) threadListAdapter.removeItem(position)
actionsViewModel.deleteThreads(
if (isPermanentDeleteFolder) host.threadListAdapter.removeItem(position)
host.actionsViewModel.deleteThreads(
threads = listOf(thread),
currentFolder = mainViewModel.currentFolder.value,
mailbox = currentMailBox
currentFolderId = thread.folderId,
mailbox = currentMailBox,
)
}

val folderRoles =
thread.messages.mapNotNull { message -> if (message.isSnoozed()) FolderRole.SNOOZED else message.folder.role }
return descriptionDialog.deleteWithConfirmationPopup(

return host.descriptionDialog.deleteWithConfirmationPopup(
messagesFolderRoles = folderRoles,
currentFolderRole = thread.folder.role,
count = 1,
Expand All @@ -224,6 +226,7 @@ object PerformSwipeActionManager {
)
}


private fun LocalSettings.setDefaultSwipeActions() {
if (swipeRight == SwipeAction.TUTORIAL) swipeRight = SwipeActionsSettingsFragment.DEFAULT_SWIPE_ACTION_RIGHT
if (swipeLeft == SwipeAction.TUTORIAL) swipeLeft = SwipeActionsSettingsFragment.DEFAULT_SWIPE_ACTION_LEFT
Expand Down
Loading
Loading