Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.useCases.MessagesActionsUseCase
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.FeatureAvailability
import com.infomaniak.mail.utils.NotificationPayload
import com.infomaniak.mail.utils.NotificationPayload.NotificationBehavior
import com.infomaniak.mail.utils.NotificationPayload.NotificationBehavior.NotificationType
import com.infomaniak.mail.utils.NotificationUtils
import com.infomaniak.mail.utils.SharedUtils
import com.infomaniak.mail.utils.extensions.atLeastOneSucceeded
import com.infomaniak.mail.utils.extensions.getApiException
import com.infomaniak.mail.utils.extensions.getUids
Expand Down Expand Up @@ -86,7 +86,7 @@ class NotificationActionsReceiver : BroadcastReceiver() {
lateinit var refreshController: RefreshController

@Inject
lateinit var sharedUtils: SharedUtils
lateinit var messagesActionsUseCase: MessagesActionsUseCase

@Inject
@IoDispatcher
Expand Down Expand Up @@ -171,7 +171,7 @@ class NotificationActionsReceiver : BroadcastReceiver() {
val threads = message.threads.filter { it.folderId == message.folderId }

val mailbox = mailboxController.getMailbox(userId, mailboxId) ?: return@launch
val messages = sharedUtils.getMessagesToMove(threads, message)
val messages = messagesActionsUseCase.getMessagesToMove(threads, message)
val destinationFolder = folderController.getFolder(folderRole) ?: return@launch
val okHttpClient = AccountUtils.getHttpClient(userId)

Expand Down
169 changes: 30 additions & 139 deletions app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,10 @@ import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.di.MailboxInfoRealm
import com.infomaniak.mail.ui.main.SnackbarManager
import com.infomaniak.mail.ui.main.SnackbarManager.UndoData
import com.infomaniak.mail.useCases.MessagesActionsUseCase
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.ContactUtils.getPhoneContacts
import com.infomaniak.mail.utils.ContactUtils.mergeApiContactsIntoPhoneContacts
import com.infomaniak.mail.utils.FeatureAvailability
import com.infomaniak.mail.utils.MyKSuiteDataUtils
import com.infomaniak.mail.utils.NetworkManager
import com.infomaniak.mail.utils.NotificationUtils.Companion.cancelNotification
Expand All @@ -86,10 +85,6 @@ import com.infomaniak.mail.utils.coroutineContext
import com.infomaniak.mail.utils.extensions.MergedContactDictionary
import com.infomaniak.mail.utils.extensions.allFailed
import com.infomaniak.mail.utils.extensions.appContext
import com.infomaniak.mail.utils.extensions.atLeastOneFailed
import com.infomaniak.mail.utils.extensions.atLeastOneSucceeded
import com.infomaniak.mail.utils.extensions.getFoldersIds
import com.infomaniak.mail.utils.extensions.getUids
import com.infomaniak.mail.utils.extensions.launchNoValidMailboxesActivity
import com.infomaniak.mail.utils.toFolderUiTree
import com.infomaniak.mail.views.itemViews.AvatarMergedContactData
Expand Down Expand Up @@ -141,6 +136,7 @@ class MainViewModel @Inject constructor(
private val mailboxController: MailboxController,
private val mergedContactController: MergedContactController,
private val messageController: MessageController,
private val messagesActionsUseCase: MessagesActionsUseCase,
private val myKSuiteDataUtils: MyKSuiteDataUtils,
private val networkManager: NetworkManager,
private val permissionsController: PermissionsController,
Expand Down Expand Up @@ -678,102 +674,9 @@ class MainViewModel @Inject constructor(
}
//endregion

//region Move
fun moveThreadsOrMessageTo(
destinationFolderId: String,
threadsUids: List<String>,
messagesUid: List<String>? = null,
) = viewModelScope.launch(ioCoroutineContext) {
val destinationFolder = folderController.getFolder(destinationFolderId)!!
val threads = threadController.getThreads(threadsUids).ifEmpty { return@launch }
val messages = messagesUid?.let { messageController.getMessages(it) }
val messagesToMove = sharedUtils.getMessagesToMove(threads, messages, currentFolderId)

moveThreadsOrMessageTo(destinationFolder, threadsUids, threads, null, messagesToMove)
}

private suspend fun moveThreadsOrMessageTo(
destinationFolder: Folder,
threadsUids: List<String>,
threads: List<Thread>,
message: Message? = null,
messagesToMove: List<Message>,
shouldDisplaySnackbar: Boolean = true,
) {
val mailbox = currentMailbox.value!!

moveOutThreadsLocally(threadsUids, threads, message)

val apiResponses = moveMessages(
mailbox = mailbox,
messagesToMove = messagesToMove,
destinationFolder = destinationFolder,
alsoMoveReactionMessages = FeatureAvailability.isReactionsAvailable(featureFlagsLive.value, localSettings),
)

if (apiResponses.atLeastOneSucceeded()) {
if (shouldAutoAdvance(message, threadsUids)) autoAdvanceThreadsUids.postValue(threadsUids)

refreshFoldersAsync(
mailbox = mailbox,
messagesFoldersIds = messagesToMove.getFoldersIds(exception = destinationFolder.id),
destinationFolderId = destinationFolder.id,
callbacks = RefreshCallbacks(onStart = ::onDownloadStart, onStop = { onDownloadStop(threadsUids) }),
)
}

if (apiResponses.atLeastOneFailed()) threadController.updateIsLocallyMovedOutStatus(threadsUids, hasBeenMovedOut = false)

if (shouldDisplaySnackbar) showMoveSnackbar(threads, message, messagesToMove, apiResponses, destinationFolder)
}

private suspend fun moveMessages(
mailbox: Mailbox,
messagesToMove: List<Message>,
destinationFolder: Folder,
alsoMoveReactionMessages: Boolean,
): List<ApiResponse<MoveResult>> {
val apiResponses = ApiRepository.moveMessages(
mailboxUuid = mailbox.uuid,
messagesUids = messagesToMove.getUids(),
destinationId = destinationFolder.id,
alsoMoveReactionMessages = alsoMoveReactionMessages,
)

// TODO: Will unsync permantly the mailbox if one message in one of the batches did succeed but some other messages in the
// same batch or in other batches that are target by emoji reactions did not
if (alsoMoveReactionMessages && apiResponses.atLeastOneSucceeded()) deleteEmojiReactionMessagesLocally(messagesToMove)

return apiResponses
}

/**
* When deleting a message targeted by emoji reactions inside of a thread, the emoji reaction messages from another folder
* that were targeting this message will display for a brief moment until we refresh their folders. This is because those
* messages don't have a target message anymore and emoji reactions messages with no target in their thread need to be
* displayed.
*
* Deleting them from the database in the first place will prevent them from being shown and the messages will be deleted by
* the api at the same time anyway.
*/
private suspend fun deleteEmojiReactionMessagesLocally(messagesToMove: List<Message>) {
for (messageToMove in messagesToMove) {
if (messageToMove.emojiReactions.isEmpty()) continue

mailboxContentRealm().write {
messageToMove.emojiReactions.forEach { reaction ->
reaction.authors.forEach { author ->
MessageController.deleteMessageByUidBlocking(author.sourceMessageUid, this)
}
}
}
}
}

private fun showMoveSnackbar(
threads: List<Thread>,
message: Message?,
messages: List<Message>,
fun showMoveSnackbar(
threadsMovedCount: Int,
messagesMoved: List<Message>,
apiResponses: List<ApiResponse<MoveResult>>,
destinationFolder: Folder,
) {
Expand All @@ -782,28 +685,17 @@ class MainViewModel @Inject constructor(

val snackbarTitle = when {
apiResponses.allFailed() -> appContext.getString(apiResponses.first().translateError())
message == null -> appContext.resources.getQuantityString(R.plurals.snackbarThreadMoved, threads.count(), destination)
else -> appContext.getString(R.string.snackbarMessageMoved, destination)
}

val undoResources = apiResponses.mapNotNull { it.data?.undoResource }
val undoData = if (undoResources.isEmpty()) {
null
} else {
val undoDestinationId = message?.folderId ?: threads.first().folderId
val foldersIds = messages.getFoldersIds(exception = undoDestinationId)
foldersIds += destinationFolder.id
UndoData(
resources = apiResponses.mapNotNull { it.data?.undoResource },
foldersIds = foldersIds,
destinationFolderId = undoDestinationId,
threadsMovedCount > 0 || messagesMoved.count() > 1 -> appContext.resources.getQuantityString(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add curly bracket and put the getQuantityString on a new line

R.plurals.snackbarThreadMoved,
threadsMovedCount,
destination
)
else -> appContext.getString(R.string.snackbarMessageMoved, destination)
}

val undoData = messagesActionsUseCase.getUndoData(messagesMoved, apiResponses, destinationFolder)
snackbarManager.postValue(snackbarTitle, undoData)
}
//endregion


//region Display problem
fun reportDisplayProblem(messageUid: String) = viewModelScope.launch(ioCoroutineContext) {
Expand Down Expand Up @@ -880,30 +772,29 @@ class MainViewModel @Inject constructor(
threadsUids: List<String>,
messagesUids: List<String>?,
) = viewModelScope.launch(ioCoroutineContext) {
val newFolderId = createNewFolderSync(name) ?: return@launch
moveThreadsOrMessageTo(newFolderId, threadsUids, messagesUids)
isMovedToNewFolder.postValue(true)
}
//endregion
val newFolderId = createNewFolderSync(name)
val mailbox = currentMailbox.value
if (newFolderId == null || mailbox == null) {
snackbarManager.postValue(appContext.getString(RCore.string.anErrorHasOccurred))
return@launch
}

private suspend fun moveOutThreadsLocally(
threadsUids: List<String>,
threads: List<Thread>,
message: Message?,
) {
val uidsToMove = if (message == null) {
threadsUids
val destinationFolder = folderController.getFolder(newFolderId)
if (destinationFolder == null) {
snackbarManager.postValue(appContext.getString(RCore.string.anErrorHasOccurred))
return@launch
}

val result =
messagesActionsUseCase.moveThreadsOrMessagesTo(newFolderId, threadsUids, messagesUids, mailbox, currentFolderId)
if (result != null) {
showMoveSnackbar(threadsUids.count(), result.messages, result.apiResponses, destinationFolder)
isMovedToNewFolder.postValue(true)
} else {
mutableListOf<String>().apply {
threads.forEach { thread ->
var nbMessagesInCurrentFolder = thread.messages.count { it.folderId == currentFolderId }
if (message.folderId == currentFolderId) nbMessagesInCurrentFolder--
if (nbMessagesInCurrentFolder == 0) add(thread.uid)
}
}
snackbarManager.postValue(appContext.getString(RCore.string.anErrorHasOccurred))
}
if (uidsToMove.isNotEmpty()) threadController.updateIsLocallyMovedOutStatus(uidsToMove, hasBeenMovedOut = true)
}
//endregion

private fun refreshFoldersAsync(
mailbox: Mailbox,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class SnackbarManager @Inject constructor() {
)

data class UndoData(
val resources: List<String>,
val resources: List<String>?,
val foldersIds: ImpactedFolders,
val destinationFolderId: String?,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ object PerformSwipeActionManager {
handleDeleteSwipe(thread, position, folderRole, isPermanentDeleteFolder, currentMailbox)
}
SwipeAction.FAVORITE -> {
actionsViewModel.toggleThreadsOrMessagesFavoriteStatus(threadsUids = listOf(thread.uid), mailbox = currentMailbox)
actionsViewModel.toggleThreadsFavoriteStatus(threadsUids = listOf(thread.uid), mailbox = currentMailbox)
true
}
SwipeAction.MOVE -> {
Expand All @@ -129,7 +129,7 @@ object PerformSwipeActionManager {
true
}
SwipeAction.READ_UNREAD -> {
actionsViewModel.toggleThreadsOrMessagesSeenStatus(
actionsViewModel.toggleThreadsSeenStatus(
threadsUids = listOf(thread.uid),
currentFolderId = mainViewModel.currentFolderId,
mailbox = currentMailbox
Expand All @@ -138,7 +138,7 @@ object PerformSwipeActionManager {
}

SwipeAction.SPAM -> {
actionsViewModel.toggleThreadsOrMessagesSpamStatus(
actionsViewModel.toggleThreadsSpamStatus(
threads = setOf(thread),
currentFolderId = mainViewModel.currentFolderId,
mailbox = currentMailbox
Expand Down Expand Up @@ -173,7 +173,7 @@ object PerformSwipeActionManager {
}

fun onSuccess() {
actionsViewModel.archiveThreadsOrMessages(
actionsViewModel.archiveThreads(
threads = listOf(thread),
currentFolder = mainViewModel.currentFolder.value,
mailbox = currentMailBox
Expand Down Expand Up @@ -206,7 +206,7 @@ object PerformSwipeActionManager {

fun onHandleDelete() {
if (isPermanentDeleteFolder) threadListAdapter.removeItem(position)
actionsViewModel.deleteThreadsOrMessages(
actionsViewModel.deleteThreads(
threads = listOf(thread),
currentFolder = mainViewModel.currentFolder.value,
mailbox = currentMailBox
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Infomaniak Mail - Android
* Copyright (C) 2023-2025 Infomaniak Network SA
* Copyright (C) 2023-2026 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -72,15 +72,16 @@ class ThreadListMultiSelection {
threadListFragment.binding.quickActionBar.setOnItemClickListener { menuId ->
val selectedThreadsUids = selectedThreads.map { it.uid }
val selectedThreadsCount = selectedThreadsUids.count()
val currentMailBox = currentMailbox.value ?: return@setOnItemClickListener

when (menuId) {
R.id.quickActionUnread -> {
trackMultiSelectActionEvent(MatomoName.MarkAsSeen, selectedThreadsCount)
actionsViewModel.toggleThreadsOrMessagesSeenStatus(
actionsViewModel.toggleThreadsSeenStatus(
threadsUids = selectedThreadsUids,
shouldRead = shouldMultiselectRead,
currentFolderId = currentFolderId,
mailbox = currentMailbox.value!!
mailbox = currentMailBox
)
isMultiSelectOn = false
}
Expand All @@ -90,19 +91,19 @@ class ThreadListMultiSelection {
count = selectedThreadsCount,
) {
trackMultiSelectActionEvent(MatomoName.Archive, selectedThreadsCount)
actionsViewModel.archiveThreadsOrMessages(
actionsViewModel.archiveThreads(
threads = selectedThreads.toList(),
currentFolder = currentFolder.value,
mailbox = currentMailbox.value!!
mailbox = currentMailBox
)
isMultiSelectOn = false
}
}
R.id.quickActionFavorite -> {
trackMultiSelectActionEvent(MatomoName.Favorite, selectedThreadsCount)
actionsViewModel.toggleThreadsOrMessagesFavoriteStatus(
actionsViewModel.toggleThreadsFavoriteStatus(
threadsUids = selectedThreadsUids,
mailbox = currentMailbox.value!!,
mailbox = currentMailBox,
shouldFavorite = shouldMultiselectFavorite
)
isMultiSelectOn = false
Expand All @@ -113,17 +114,18 @@ class ThreadListMultiSelection {
count = selectedThreadsCount,
) {
trackMultiSelectActionEvent(MatomoName.Delete, selectedThreadsCount)
actionsViewModel.deleteThreadsOrMessages(
actionsViewModel.deleteThreads(
threads = selectedThreads.toList(),
currentFolder = currentFolder.value,
mailbox = currentMailbox.value!!
mailbox = currentMailBox
)
isMultiSelectOn = false
}
}
R.id.quickActionMenu -> {
trackMultiSelectActionEvent(MatomoName.OpenBottomSheet, selectedThreadsCount)
val direction = if (selectedThreadsCount == 1) {
isMultiSelectOn = false
ThreadListFragmentDirections.actionThreadListFragmentToThreadActionsBottomSheetDialog(
threadUid = selectedThreadsUids.single(),
shouldLoadDistantResources = false,
Expand Down
Loading